29template <std::
size_t N>
class SoftCombiner {
30 using Clock = std::chrono::steady_clock;
39 bool operator==(
Key const &other)
const noexcept {
40 return mode == other.mode && freqBin == other.freqBin &&
41 dtBin == other.dtBin && signature == other.signature;
47 std::array<float, N> llr0;
48 std::array<float, N> llr1;
53 SoftCombiner() : SoftCombiner(defaultEnabled(), true) {}
55 explicit SoftCombiner(
bool enabled,
bool runSelfTest =
true)
56 : m_enabled(enabled) {
59 <<
"soft-combining disabled (JS8_SOFT_COMBINING=0)";
65 Key makeKey(
int mode,
float f1,
float dt, std::array<float, N>
const &llr0,
66 std::array<float, N>
const &llr1)
const {
67 return Key{mode,
static_cast<int>(std::lround(f1)),
68 static_cast<int>(std::lround(dt * 10.0f)),
69 signature(llr0, llr1)};
72 Combined combine(
Key const &key, std::array<float, N>
const &llr0,
73 std::array<float, N>
const &llr1,
74 std::chrono::seconds ttl) {
78 return Combined{key, llr0, llr1, 1,
false};
81 auto &bucket = m_entries[keyForLookup(key)];
82 auto it = findEntry(bucket, key.signature);
84 if (it == bucket.end()) {
85 bucket.push_back(makeEntry(key.signature, llr0, llr1));
86 return Combined{key, llr0, llr1, 1,
false};
89 for (std::size_t i = 0; i < llr0.size(); ++i) {
90 it->llr0[i] += llr0[i];
91 it->llr1[i] += llr1[i];
95 it->lastSeen = Clock::now();
98 <<
"soft-combining repeats" << it->repeats <<
"mode" << key.mode
99 <<
"freq" << key.freqBin <<
"dtbin" << key.dtBin;
101 return Combined{key, it->llr0, it->llr1, it->repeats,
true};
104 void markDecoded(
Key const &key) {
108 auto lookup = keyForLookup(key);
109 auto it = m_entries.find(lookup);
111 if (it == m_entries.end())
114 auto &bucket = it->second;
115 bucket.erase(std::remove_if(bucket.begin(), bucket.end(),
116 [&key](Entry
const &entry) {
117 return entry.signature == key.signature;
125 void flush(std::chrono::seconds ttl) {
129 auto const now = Clock::now();
131 for (
auto it = m_entries.begin(); it != m_entries.end();) {
132 auto &bucket = it->second;
134 bucket.erase(std::remove_if(bucket.begin(), bucket.end(),
135 [now, ttl](Entry
const &entry) {
136 return now - entry.lastSeen > ttl;
141 it = m_entries.erase(it);
153 bool operator==(CoarseKey
const &other)
const noexcept {
154 return mode == other.mode && freqBin == other.freqBin &&
155 dtBin == other.dtBin;
160 std::size_t operator()(CoarseKey
const &key)
const noexcept {
161 std::size_t
const h1 = std::hash<int>{}(key.mode);
162 std::size_t
const h2 = std::hash<int>{}(key.freqBin);
163 std::size_t
const h3 = std::hash<int>{}(key.dtBin);
164 return h1 ^ (h2 << 1) ^ (h3 << 2);
170 std::array<float, N> llr0;
171 std::array<float, N> llr1;
173 Clock::time_point lastSeen;
176 using Bucket = std::vector<Entry>;
178 static constexpr auto signatureIndices() {
179 std::array<int, 32> indices{};
181 for (std::size_t i = 0; i < indices.size(); ++i) {
182 value = (value + 37) %
static_cast<int>(N);
188 static typename Bucket::iterator findEntry(Bucket &bucket, uint32_t signature) {
189 constexpr int MAX_HAMMING =
193 bucket.begin(), bucket.end(), [signature](Entry
const &entry) {
194 return hamming(signature, entry.signature) <= MAX_HAMMING;
198 static bool defaultEnabled() {
200 int value = qEnvironmentVariableIntValue(
"JS8_SOFT_COMBINING", &ok);
201 return ok ? value != 0 :
true;
204 static uint32_t signature(std::array<float, N>
const &llr0,
205 std::array<float, N>
const &llr1) {
206 static constexpr auto INDICES = signatureIndices();
209 for (std::size_t i = 0; i < INDICES.size(); ++i) {
210 auto const idx = INDICES[i];
211 float const v = 0.5f * (llr0[idx] + llr1[idx]);
218 static int hamming(uint32_t a, uint32_t b) {
228 static void maybeRunSelfTest() {
229 static std::once_flag once;
230 std::call_once(once, []() {
231 if (!qEnvironmentVariableIsSet(
"JS8_SOFT_COMBINING_TEST"))
234 SoftCombiner combiner(
true,
false);
236 std::array<float, N> baseline{};
237 for (std::size_t i = 0; i < baseline.size(); ++i) {
238 baseline[i] = (i % 2 == 0) ? 2.0f : -2.0f;
241 auto noisy = [](std::array<float, N> base,
int flipStride) {
242 for (std::size_t i = 0; i < base.size(); ++i) {
243 if (i % flipStride == 0) {
252 auto llrA = noisy(baseline, 7);
253 auto llrB = noisy(baseline, 11);
255 auto key = combiner.makeKey(0, 1500.0f, 1.0f, llrA, llrB);
257 combiner.combine(key, llrA, llrA, std::chrono::seconds{30});
259 combiner.combine(key, llrB, llrB, std::chrono::seconds{30});
261 auto countMatches = [](std::array<float, N>
const &llr,
262 std::array<float, N>
const &reference) {
264 for (std::size_t i = 0; i < llr.size(); ++i) {
265 if (llr[i] * reference[i] > 0)
271 int const matchesA = countMatches(first.llr0, baseline);
272 int const matchesB = countMatches(llrB, baseline);
273 int const matchesCombined = countMatches(second.llr0, baseline);
276 <<
"soft-combining self-test: A matches" << matchesA
277 <<
"B matches" << matchesB <<
"combined matches"
278 << matchesCombined <<
"repeats" << second.repeats;
282 static Entry makeEntry(uint32_t signature, std::array<float, N>
const &llr0,
283 std::array<float, N>
const &llr1) {
284 return Entry{signature, llr0, llr1, 1, Clock::now()};
287 CoarseKey keyForLookup(
Key const &key)
const {
288 return CoarseKey{key.mode, key.freqBin, key.dtBin};
291 std::unordered_map<CoarseKey, Bucket, CoarseHash> m_entries;