JS8Call-Improved master
Loading...
Searching...
No Matches
soft_combiner.h
Go to the documentation of this file.
1
5
6#pragma once
7
8#include <QDebug>
9#include <QLoggingCategory>
10#include <QtGlobal>
11
12#include <algorithm>
13#include <array>
14#include <chrono>
15#include <cmath>
16#include <cstdint>
17#include <mutex>
18#include <optional>
19#include <unordered_map>
20#include <utility>
21#include <vector>
22
23Q_DECLARE_LOGGING_CATEGORY(decoder_js8);
24
25namespace js8 {
37template <std::size_t N> class SoftCombiner {
38 using Clock = std::chrono::steady_clock;
39
40 public:
47 struct Key {
48 int mode;
49 int freqBin;
50 int dtBin;
51 uint32_t signature;
52
53 bool operator==(Key const &other) const noexcept {
54 return mode == other.mode && freqBin == other.freqBin &&
55 dtBin == other.dtBin && signature == other.signature;
56 }
57 };
58
66 struct Combined {
68 std::array<float, N> llr0;
69 std::array<float, N> llr1;
70 int repeats;
71 bool combined;
72 };
73
80 SoftCombiner() : SoftCombiner(defaultEnabled(), true) {}
81
87 explicit SoftCombiner(bool enabled, bool runSelfTest = true)
88 : m_enabled(enabled) {
89 if (!m_enabled) {
90 qCDebug(decoder_js8)
91 << "soft-combining disabled (JS8_SOFT_COMBINING=0)";
92 }
93 if (runSelfTest)
94 maybeRunSelfTest();
95 }
96
106 Key makeKey(int mode, float f1, float dt, std::array<float, N> const &llr0,
107 std::array<float, N> const &llr1) const {
108 return Key{mode, static_cast<int>(std::lround(f1)),
109 static_cast<int>(std::lround(dt * 10.0f)), // 100 ms bins
110 signature(llr0, llr1)};
111 }
112
121 Combined combine(Key const &key, std::array<float, N> const &llr0,
122 std::array<float, N> const &llr1,
123 std::chrono::seconds ttl) {
124 flush(ttl);
125
126 if (!m_enabled) {
127 return Combined{key, llr0, llr1, 1, false};
128 }
129
130 auto &bucket = m_entries[keyForLookup(key)];
131 auto it = findEntry(bucket, key.signature);
132
133 if (it == bucket.end()) {
134 bucket.push_back(makeEntry(key.signature, llr0, llr1));
135 return Combined{key, llr0, llr1, 1, false};
136 }
137
138 for (std::size_t i = 0; i < llr0.size(); ++i) {
139 it->llr0[i] += llr0[i];
140 it->llr1[i] += llr1[i];
141 }
142
143 ++it->repeats;
144 it->lastSeen = Clock::now();
145
146 qCDebug(decoder_js8)
147 << "soft-combining repeats" << it->repeats << "mode" << key.mode
148 << "freq" << key.freqBin << "dtbin" << key.dtBin;
149
150 return Combined{key, it->llr0, it->llr1, it->repeats, true};
151 }
152
157 void markDecoded(Key const &key) {
158 if (!m_enabled)
159 return;
160
161 auto lookup = keyForLookup(key);
162 auto it = m_entries.find(lookup);
163
164 if (it == m_entries.end())
165 return;
166
167 auto &bucket = it->second;
168 bucket.erase(std::remove_if(bucket.begin(), bucket.end(),
169 [&key](Entry const &entry) {
170 return entry.signature == key.signature;
171 }),
172 bucket.end());
173
174 if (bucket.empty())
175 m_entries.erase(it);
176 }
177
182 void flush(std::chrono::seconds ttl) {
183 if (!m_enabled)
184 return;
185
186 auto const now = Clock::now();
187
188 for (auto it = m_entries.begin(); it != m_entries.end();) {
189 auto &bucket = it->second;
190
191 bucket.erase(std::remove_if(bucket.begin(), bucket.end(),
192 [now, ttl](Entry const &entry) {
193 return now - entry.lastSeen > ttl;
194 }),
195 bucket.end());
196
197 if (bucket.empty())
198 it = m_entries.erase(it);
199 else
200 ++it;
201 }
202 }
203
204 private:
206 struct CoarseKey {
207 int mode;
208 int freqBin;
209 int dtBin;
210
211 bool operator==(CoarseKey const &other) const noexcept {
212 return mode == other.mode && freqBin == other.freqBin &&
213 dtBin == other.dtBin;
214 }
215 };
216
217 struct CoarseHash {
218 std::size_t operator()(CoarseKey const &key) const noexcept {
219 std::size_t const h1 = std::hash<int>{}(key.mode);
220 std::size_t const h2 = std::hash<int>{}(key.freqBin);
221 std::size_t const h3 = std::hash<int>{}(key.dtBin);
222 return h1 ^ (h2 << 1) ^ (h3 << 2);
223 }
224 };
225
229 struct Entry {
230 uint32_t signature;
231 std::array<float, N> llr0;
232 std::array<float, N> llr1;
233 int repeats;
234 Clock::time_point lastSeen;
235 };
236
237 using Bucket = std::vector<Entry>;
238
245 static constexpr auto signatureIndices() {
246 std::array<int, 32> indices{};
247 int value = 0;
248 for (std::size_t i = 0; i < indices.size(); ++i) {
249 value = (value + 37) % static_cast<int>(N);
250 indices[i] = value;
251 }
252 return indices;
253 }
254
261 static typename Bucket::iterator findEntry(Bucket &bucket, uint32_t signature) {
262 constexpr int MAX_HAMMING =
263 4; // allow small differences between noisy repeats
264
265 return std::find_if(
266 bucket.begin(), bucket.end(), [signature](Entry const &entry) {
267 return hamming(signature, entry.signature) <= MAX_HAMMING;
268 });
269 }
270
275 static bool defaultEnabled() {
276 bool ok = false;
277 int value = qEnvironmentVariableIntValue("JS8_SOFT_COMBINING", &ok);
278 return ok ? value != 0 : true;
279 }
280
284 static uint32_t signature(std::array<float, N> const &llr0,
285 std::array<float, N> const &llr1) {
286 static constexpr auto INDICES = signatureIndices();
287
288 uint32_t sig = 0;
289 for (std::size_t i = 0; i < INDICES.size(); ++i) {
290 auto const idx = INDICES[i];
291 float const v = 0.5f * (llr0[idx] + llr1[idx]);
292 if (v >= 0.0f)
293 sig |= (1u << i);
294 }
295 return sig;
296 }
297
299 static int hamming(uint32_t a, uint32_t b) {
300 uint32_t v = a ^ b;
301 int c = 0;
302 while (v) {
303 v &= (v - 1);
304 ++c;
305 }
306 return c;
307 }
308
312 static void maybeRunSelfTest() {
313 static std::once_flag once;
314 std::call_once(once, []() {
315 if (!qEnvironmentVariableIsSet("JS8_SOFT_COMBINING_TEST"))
316 return;
317
318 SoftCombiner combiner(true, false);
319
320 std::array<float, N> baseline{};
321 for (std::size_t i = 0; i < baseline.size(); ++i) {
322 baseline[i] = (i % 2 == 0) ? 2.0f : -2.0f;
323 }
324
325 auto noisy = [](std::array<float, N> base, int flipStride) {
326 for (std::size_t i = 0; i < base.size(); ++i) {
327 if (i % flipStride == 0) {
328 base[i] *= -0.4f; // flip sign and reduce magnitude
329 } else {
330 base[i] *= 0.8f; // weaken but keep sign
331 }
332 }
333 return base;
334 };
335
336 auto llrA = noisy(baseline, 7);
337 auto llrB = noisy(baseline, 11);
338
339 auto key = combiner.makeKey(0, 1500.0f, 1.0f, llrA, llrB);
340 auto first =
341 combiner.combine(key, llrA, llrA, std::chrono::seconds{30});
342 auto second =
343 combiner.combine(key, llrB, llrB, std::chrono::seconds{30});
344
345 auto countMatches = [](std::array<float, N> const &llr,
346 std::array<float, N> const &reference) {
347 int matches = 0;
348 for (std::size_t i = 0; i < llr.size(); ++i) {
349 if (llr[i] * reference[i] > 0)
350 ++matches;
351 }
352 return matches;
353 };
354
355 int const matchesA = countMatches(first.llr0, baseline);
356 int const matchesB = countMatches(llrB, baseline);
357 int const matchesCombined = countMatches(second.llr0, baseline);
358
359 qCDebug(decoder_js8)
360 << "soft-combining self-test: A matches" << matchesA
361 << "B matches" << matchesB << "combined matches"
362 << matchesCombined << "repeats" << second.repeats;
363 });
364 }
365
367 static Entry makeEntry(uint32_t signature, std::array<float, N> const &llr0,
368 std::array<float, N> const &llr1) {
369 return Entry{signature, llr0, llr1, 1, Clock::now()};
370 }
371
373 CoarseKey keyForLookup(Key const &key) const {
374 return CoarseKey{key.mode, key.freqBin, key.dtBin};
375 }
376 std::unordered_map<CoarseKey, Bucket, CoarseHash> m_entries;
377 bool m_enabled;
378};
379} // namespace js8
void markDecoded(Key const &key)
Remove cached repeats for a candidate that has been decoded.
Definition soft_combiner.h:157
void flush(std::chrono::seconds ttl)
Remove cached entries older than ttl.
Definition soft_combiner.h:182
Combined combine(Key const &key, std::array< float, N > const &llr0, std::array< float, N > const &llr1, std::chrono::seconds ttl)
Attempt to combine incoming LLRs with cached repeats.
Definition soft_combiner.h:121
SoftCombiner(bool enabled, bool runSelfTest=true)
Construct a SoftCombiner with explicit enable flag.
Definition soft_combiner.h:87
Key makeKey(int mode, float f1, float dt, std::array< float, N > const &llr0, std::array< float, N > const &llr1) const
Create a Key for a candidate from its parameters and LLRs.
Definition soft_combiner.h:106
SoftCombiner()
Construct a SoftCombiner with defaults.
Definition soft_combiner.h:80
JS8 namespace for Kalman filter-based trackers.
Definition FrequencyTracker.cpp:14
Result returned by combine() describing combined LLRs.
Definition soft_combiner.h:66
bool combined
True when combined from prior entries.
Definition soft_combiner.h:71
int repeats
Repeat count.
Definition soft_combiner.h:70
std::array< float, N > llr1
Combined LLR1 values.
Definition soft_combiner.h:69
std::array< float, N > llr0
Combined LLR0 values.
Definition soft_combiner.h:68
Key key
Candidate key.
Definition soft_combiner.h:67
Lookup key describing a decode candidate.
Definition soft_combiner.h:47
int dtBin
Coarse timing bin.
Definition soft_combiner.h:50
uint32_t signature
Compact LLR signature.
Definition soft_combiner.h:51
int freqBin
Coarse frequency bin.
Definition soft_combiner.h:49
int mode
Submode/mode index.
Definition soft_combiner.h:48