sdrangel/modemm17/Viterbi.h

242 wiersze
7.1 KiB
C++

// Copyright 2020 Mobilinkd LLC.
#pragma once
#include "Convolution.h"
#include "Util.h"
#include <array>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <iterator>
#include <limits>
namespace modemm17
{
/**
* Compile-time build of the trellis forward state transitions.
*
* @param is the trellis -- used only for type deduction.
* @return a 2-D array of source, dest, cost.
*/
template <typename Trellis_>
constexpr std::array<std::array<uint8_t, (1 << Trellis_::k)>, (1 << Trellis_::K)> makeNextState(Trellis_)
{
std::array<std::array<uint8_t, (1 << Trellis_::k)>, (1 << Trellis_::K)> result{};
for (size_t i = 0; i != (1 << Trellis_::K); ++i)
{
for (size_t j = 0; j != (1 << Trellis_::k); ++j)
{
result[i][j] = static_cast<uint8_t>(update_memory<Trellis_::K, Trellis_::k>(i, j) & ((1 << Trellis_::K) - 1));
}
}
return result;
}
/**
* Compile-time build of the trellis reverse state transitions, for efficient
* reverse traversal during chainback.
*
* @param is the trellis -- used only for type deduction.
* @return a 2-D array of dest, source, cost.
*/
template <typename Trellis_>
constexpr std::array<std::array<uint8_t, (1 << Trellis_::k)>, (1 << Trellis_::K)> makePrevState(Trellis_)
{
constexpr size_t NumStates = (1 << Trellis_::K);
constexpr size_t HalfStates = NumStates / 2;
std::array<std::array<uint8_t, (1 << Trellis_::k)>, (1 << Trellis_::K)> result{};
for (size_t i = 0; i != (1 << Trellis_::K); ++i)
{
size_t k = i >= HalfStates;
for (size_t j = 0; j != (1 << Trellis_::k); ++j)
{
size_t l = update_memory<Trellis_::K, Trellis_::k>(i, j) & (NumStates - 1);
result[l][k] = i;
}
}
return result;
}
/**
* Compile-time generation of the trellis path cost for LLR.
*
* @param trellis
* @return
*/
template <typename Trellis_, size_t LLR = 2>
constexpr auto makeCost(Trellis_ trellis)
{
constexpr size_t NumStates = (1 << Trellis_::K);
constexpr size_t NumOutputs = Trellis_::n;
std::array<std::array<int16_t, NumOutputs>, NumStates> result{};
for (uint32_t i = 0; i != NumStates; ++i)
{
for (uint32_t j = 0; j != NumOutputs; ++j)
{
auto bit = convolve_bit(trellis.polynomials[j], i << 1);
result[i][j] = to_int<int8_t, LLR>(((bit << 1) - 1) * ((1 << (LLR - 1)) - 1));
}
}
return result;
}
/**
* Soft decision Viterbi algorithm based on the trellis and LLR size.
*
*/
template <typename Trellis_, size_t LLR_ = 2>
struct Viterbi
{
static_assert(LLR_ < 7, "Need to be < 7 to avoid overflow errors");
static constexpr size_t K = Trellis_::K;
static constexpr size_t k = Trellis_::k;
static constexpr size_t n = Trellis_::n;
static constexpr size_t InputValues = 1 << n;
static constexpr size_t NumStates = (1 << K);
static constexpr int32_t METRIC = ((1 << (LLR_ - 1)) - 1) << 2;
using metrics_t = std::array<int32_t, NumStates>;
using cost_t = std::array<std::array<int16_t, n>, NumStates>;
using state_transition_t = std::array<std::array<uint8_t, 2>, NumStates>;
metrics_t pathMetrics_{};
cost_t cost_;
state_transition_t nextState_;
state_transition_t prevState_;
metrics_t prevMetrics, currMetrics;
// This is the maximum amount of storage needed for M17. If used for
// other modes, this may need to be increased. This will never overflow
// because of a static assertion in the decode() function.
std::array<std::bitset<NumStates>, 244> history_;
Viterbi(Trellis_ trellis)
: cost_(makeCost<Trellis_, LLR_>(trellis))
, nextState_(makeNextState(trellis))
, prevState_(makePrevState(trellis))
{}
void calculate_path_metric(
const std::array<int16_t, NumStates / 2>& cost0,
const std::array<int16_t, NumStates / 2>& cost1,
std::bitset<NumStates>& hist,
size_t j
) {
auto& i0 = nextState_[j][0];
auto& i1 = nextState_[j][1];
auto& c0 = cost0[j];
auto& c1 = cost1[j];
auto& p0 = prevMetrics[j];
auto& p1 = prevMetrics[j + NumStates / 2];
int32_t m0 = p0 + c0;
int32_t m1 = p0 + c1;
int32_t m2 = p1 + c1;
int32_t m3 = p1 + c0;
bool d0 = m0 > m2;
bool d1 = m1 > m3;
hist.set(i0, d0);
hist.set(i1, d1);
currMetrics[i0] = d0 ? m2 : m0;
currMetrics[i1] = d1 ? m3 : m1;
}
/**
* Viterbi soft decoder using LLR inputs where 0 == erasure.
*
* @return path metric for estimating BER.
*/
template <size_t IN2, size_t OUT2>
size_t decode(const std::array<int8_t, IN2>& in, std::array<uint8_t, OUT2>& out)
{
static_assert(sizeof(history_) >= IN2 / 2, "Invalid size");
constexpr auto MAX_METRIC = std::numeric_limits<typename metrics_t::value_type>::max() / 2;
prevMetrics.fill(MAX_METRIC);
prevMetrics[0] = 0; // Starting point.
auto hbegin = history_.begin();
auto hend = history_.begin() + IN2 / 2;
constexpr size_t BUTTERFLY_SIZE = NumStates / 2;
size_t hindex = 0;
std::array<int16_t, BUTTERFLY_SIZE> cost0;
std::array<int16_t, BUTTERFLY_SIZE> cost1;
for (size_t i = 0; i != IN2; i += 2, hindex += 1)
{
int16_t s0 = in[i];
int16_t s1 = in[i + 1];
cost0.fill(0);
cost1.fill(0);
for (size_t j = 0; j != BUTTERFLY_SIZE; ++j)
{
if (s0) // is not erased
{
cost0[j] = std::abs(cost_[j][0] - s0);
cost1[j] = std::abs(cost_[j][0] + s0);
}
if (s1) // is not erased
{
cost0[j] += std::abs(cost_[j][1] - s1);
cost1[j] += std::abs(cost_[j][1] + s1);
}
}
for (size_t j = 0; j != BUTTERFLY_SIZE; ++j)
{
calculate_path_metric(cost0, cost1, history_[hindex], j);
}
std::swap(currMetrics, prevMetrics);
}
// Find starting point. Should be 0 for properly flushed CCs.
// However, 0 may not be the path with the fewest errors.
size_t min_element = 0;
int32_t min_cost = prevMetrics[0];
for (size_t i = 0; i != NumStates; ++i)
{
if (prevMetrics[i] < min_cost)
{
min_cost = prevMetrics[i];
min_element = i;
}
}
size_t cost = std::round(min_cost / float(detail::llr_limit<LLR_>()));
// Do chainback.
auto oit = std::rbegin(out);
auto hit = std::make_reverse_iterator(hend); // rbegin
auto hrend = std::make_reverse_iterator(hbegin); // rend
size_t next_element = min_element;
size_t index = IN2 / 2;
while (oit != std::rend(out) && hit != hrend)
{
auto v = (*hit++)[next_element];
if (index-- <= OUT2) *oit++ = next_element & 1;
next_element = prevState_[next_element][v];
}
return cost;
}
};
} // modemm17