JS8Call-Improved master
Loading...
Searching...
No Matches
whitening_processor.h
1#pragma once
2
3#include <QDebug>
4#include <QLoggingCategory>
5
6#include <algorithm>
7#include <array>
8#include <cmath>
9#include <cstdlib>
10#include <numeric>
11#include <optional>
12#include <sstream>
13#include <vector>
14
15Q_DECLARE_LOGGING_CATEGORY(decoder_js8);
16
17namespace js8 {
26template <int NROWS, int ND, int N> class WhiteningProcessor {
27 public:
28 struct Result {
29 std::array<float, 3 * ND> llr0;
30 std::array<float, 3 * ND> llr1;
31 bool whiteningApplied;
32 bool erasureApplied;
33 std::size_t erasures;
34 double avgAbsPre;
35 double avgAbsPost;
36 };
37
38 static Result process(std::array<std::array<float, ND>, NROWS> const &s1,
39 std::array<int, ND> const &symbolWinners,
40 float erasureThreshold, bool debug) {
41 auto const median =
42 [](std::vector<float> &values) -> std::optional<float> {
43 if (values.empty())
44 return std::nullopt;
45
46 auto const mid = values.size() / 2;
47
48 std::nth_element(values.begin(), values.begin() + mid,
49 values.end());
50 float med = values[mid];
51
52 if ((values.size() % 2) == 0 && mid > 0) {
53 std::nth_element(values.begin(), values.begin() + (mid - 1),
54 values.end());
55 med = 0.5f * (med + values[mid - 1]);
56 }
57
58 return med;
59 };
60
61 // Estimate per-tone noise using non-winning tone magnitudes across the
62 // frame.
63 auto const toneNoise =
64 [&]() -> std::optional<std::array<float, NROWS>> {
65 std::array<std::vector<float>, NROWS> toneSamples;
66 std::array<float, NROWS> noise = {};
67
68 // Collect non-winning magnitudes for each tone.
69 for (int j = 0; j < ND; ++j) {
70 int const winner = symbolWinners[j];
71
72 for (int i = 0; i < NROWS; ++i) {
73 if (i != winner)
74 toneSamples[i].push_back(s1[i][j]);
75 }
76 }
77
78 bool ok = true;
79
80 for (int i = 0; i < NROWS; ++i) {
81 if (auto m = median(toneSamples[i]); m) {
82 noise[i] = *m;
83 } else {
84 ok = false;
85 break;
86 }
87 }
88
89 if (!ok)
90 return std::nullopt;
91
92 return noise;
93 }();
94
95 if (toneNoise && debug) {
96 std::ostringstream oss;
97
98 oss << "toneNoise:";
99 for (auto const value : *toneNoise)
100 oss << ' ' << value;
101
102 qCDebug(decoder_js8).noquote() << oss.str().c_str();
103 }
104
105 // Estimate per-symbol noise using non-winning tone magnitudes per
106 // symbol.
107 auto const symbolNoise = [&]() -> std::optional<std::vector<float>> {
108 std::vector<float> noise;
109 noise.reserve(ND);
110
111 for (int j = 0; j < ND; ++j) {
112 std::vector<float> bins;
113 bins.reserve(NROWS - 1);
114
115 int const winner = symbolWinners[j];
116
117 for (int i = 0; i < NROWS; ++i) {
118 if (i != winner)
119 bins.push_back(s1[i][j]);
120 }
121
122 if (auto m = median(bins); m) {
123 noise.push_back(*m);
124 } else {
125 return std::nullopt;
126 }
127 }
128
129 return noise;
130 }();
131
132 if (symbolNoise && !symbolNoise->empty() && debug) {
133 auto const [minIt, maxIt] =
134 std::minmax_element(symbolNoise->begin(), symbolNoise->end());
135 float const avg = std::accumulate(symbolNoise->begin(),
136 symbolNoise->end(), 0.0f) /
137 static_cast<float>(symbolNoise->size());
138
139 qCDebug(decoder_js8)
140 << "symbolNoise avg/min/max" << avg << *minIt << *maxIt;
141 }
142
143 Result result{};
144
145 bool const disableWhitening =
146 std::getenv("JS8_DISABLE_WHITENING") != nullptr;
147 bool const whiteningAvailable = toneNoise && symbolNoise &&
148 !symbolNoise->empty() &&
149 !disableWhitening;
150 bool const applyErasureInWhitening =
151 whiteningAvailable && erasureThreshold > 0.0f;
152 double sumAbsPre = 0.0;
153 double sumAbsPost = 0.0;
154 std::size_t erasures = 0;
155
156 for (int j = 0; j < ND; ++j) {
157 int const i1 = 3 * j; // First column (matches Fortran's i1)
158 int const i2 = 3 * j + 1; // Second column (matches Fortran's i2)
159 int const i4 = 3 * j + 2; // Third column (matches Fortran's i4)
160
161 std::array<float, NROWS> ps;
162
163 for (int i = 0; i < NROWS; ++i)
164 ps[i] = s1[i][j];
165
166 // Assign to `bmeta` in column order, with correct values
167 result.llr0[i1] = std::max({ps[4], ps[5], ps[6], ps[7]}) -
168 std::max({ps[0], ps[1], ps[2], ps[3]}); // r4
169 result.llr0[i2] = std::max({ps[2], ps[3], ps[6], ps[7]}) -
170 std::max({ps[0], ps[1], ps[4], ps[5]}); // r2
171 result.llr0[i4] = std::max({ps[1], ps[3], ps[5], ps[7]}) -
172 std::max({ps[0], ps[2], ps[4], ps[6]}); // r1
173
174 for (auto &x : ps)
175 x = std::log(x + 1e-32f);
176
177 // Assign to `bmetb` in column order, with correct values
178 result.llr1[i1] = std::max({ps[4], ps[5], ps[6], ps[7]}) -
179 std::max({ps[0], ps[1], ps[2], ps[3]}); // r4
180 result.llr1[i2] = std::max({ps[2], ps[3], ps[6], ps[7]}) -
181 std::max({ps[0], ps[1], ps[4], ps[5]}); // r2
182 result.llr1[i4] = std::max({ps[1], ps[3], ps[5], ps[7]}) -
183 std::max({ps[0], ps[2], ps[4], ps[6]}); // r1
184
185 if (whiteningAvailable) {
186 int const winner = symbolWinners[j];
187 float const tn = std::max(0.0f, (*toneNoise)[winner]);
188 float const sn = std::max(0.0f, (*symbolNoise)[j]);
189 float const localNoise = std::sqrt(tn * sn + 1e-12f);
190
191 auto const applyWhitening = [&](float &value) {
192 float const pre = std::abs(value);
193 sumAbsPre += pre;
194
195 if (localNoise > 0.0f && std::isfinite(localNoise)) {
196 value /= localNoise;
197 }
198
199 if (applyErasureInWhitening &&
200 std::abs(value) < erasureThreshold) {
201 value = 0.0f;
202 ++erasures;
203 }
204
205 sumAbsPost += std::abs(value);
206 };
207
208 applyWhitening(result.llr0[i1]);
209 applyWhitening(result.llr0[i2]);
210 applyWhitening(result.llr0[i4]);
211 applyWhitening(result.llr1[i1]);
212 applyWhitening(result.llr1[i2]);
213 applyWhitening(result.llr1[i4]);
214 }
215 }
216
217 auto const normalizeLLR = [](auto &llr) {
218 float sum = 0.0f;
219 float sum_of_squares = 0.0f;
220
221 for (auto const value : llr) {
222 sum += value;
223 sum_of_squares += value * value;
224 }
225
226 float const llrav = sum / llr.size();
227 float const llr2av = sum_of_squares / llr.size();
228 float const variance = llr2av - llrav * llrav;
229 float const llrsig = std::sqrt(variance > 0.0f ? variance : llr2av);
230
231 for (float &val : llr)
232 val = (val / llrsig) * 2.83f;
233 };
234
235 // Normalize and process metrics
236
237 normalizeLLR(result.llr0);
238 normalizeLLR(result.llr1);
239
240 if (whiteningAvailable && debug) {
241 auto const total =
242 static_cast<double>(result.llr0.size() + result.llr1.size());
243 double const avgPre = total > 0.0 ? sumAbsPre / total : 0.0;
244 double const avgPost = total > 0.0 ? sumAbsPost / total : 0.0;
245
246 qCDebug(decoder_js8) << "LLR whitening applied"
247 << "avg|LLR| pre/post:" << avgPre << avgPost
248 << "erasures:" << erasures;
249 }
250
251 result.whiteningApplied = whiteningAvailable;
252 result.erasureApplied = applyErasureInWhitening;
253 result.erasures = erasures;
254 result.avgAbsPre = sumAbsPre;
255 result.avgAbsPost = sumAbsPost;
256 return result;
257 }
258};
259} // namespace js8
Compute per-tone/symbol noise medians and whiten LLRs for a JS8 frame.
Definition whitening_processor.h:26
JS8 namespace for Kalman filter-based trackers.
Definition FrequencyTracker.cpp:14
Definition whitening_processor.h:28