CECCO: Linear Codes and Decoding

CECCO supports linear codes over finite fields and (partially) over the rationals. For details about finite field construction consult the demo on prime fields, the demo on finite extension fields, or the documentation

The main example here is based on the binary field F4 (as extension of F2 with irreducible polynomial 1 + x + x^2, runtime-generated LUTs). It can easily be modified to any other finite field.

At the end of the demo we show an example of pseudo-soft GMD decoding, which (in the form shown here) requires binary extension fields. CECCO also supports soft-input decoding for linear codes over arbitrary finite fields, cf. the demo on soft decoding.

code.cpp Lines 8–8
    using F = Ext<Fp<2>, MOD{1, 1, 1}>;

After selecting some code length n and code dimension k we calculate a random k x n matrix with full rank:

code.cpp Lines 10–15
    const size_t n = 13;
    const size_t k = 6;
    auto G = ZeroMatrix<F>(k, n);
    do {
        G.randomize();
    } while (G.rank() < k);

Based on the matrix we can set up a (random) linear code C'[F_2; n, k]:

code.cpp Lines 17–17
    auto Cp = LinearCode(n, k, G);

CECCO can determine a variety of properties of a linear code. For some properties, this requires extensive calculations. For that reason, the user can choose the level of detail for outputting a code to the console. With the IO manipulator CECCO::showbasic only field, length, and dimension are shown. The manipulator CECCO::showmost adds generator matrix G, parity check matrix H as well as some other properties. The manipulator CECCO::showall shows all properties, even the ones with expensive calculations such as the weight enumerator A(x):

code.cpp Lines 18–20
    // std::cout << showbasic << Cp << std::endl;
    // std::cout << showmost << Cp << std::endl;
    std::cout << showall << Cp << std::endl;

A possible output (with the CECCO::showall manipulator) is:

[F_2; 17, 9], dmin = 4
Linear code with properties: {
G = 
  ⌈1 1 0 1 0 0 0 1 1 0 1 1 0 1 1 1 0⌉
  |0 0 1 1 0 0 1 0 1 1 0 0 1 0 1 0 1|
  |0 1 1 1 0 0 1 1 1 1 0 1 1 1 0 0 0|
  |0 0 0 0 0 1 1 0 1 1 1 0 0 0 1 1 1|
  |1 0 0 1 0 0 0 0 0 0 1 0 1 0 0 0 0|
  |1 1 1 1 1 1 0 0 1 0 1 1 1 1 1 1 1|
  |0 0 0 0 1 0 0 1 1 0 0 0 1 0 1 1 0|
  |1 1 1 1 1 0 0 0 1 1 1 1 0 0 1 1 1|
  ⌊1 1 1 1 1 1 1 1 0 0 0 0 0 0 1 1 0⌋
H = 
  ⌈1 0 0 0 0 0 0 0 1 1 1 1 0 1 0 1 0⌉
  |0 1 0 0 0 0 0 0 0 1 1 1 1 0 1 0 1|
  |0 0 1 0 0 0 0 0 0 0 1 1 1 1 0 1 0|
  |0 0 0 1 0 0 0 0 0 0 0 1 1 1 1 0 1|
  |0 0 0 0 1 0 0 0 1 1 1 1 1 0 1 0 0|
  |0 0 0 0 0 1 0 0 0 1 1 1 1 1 0 1 0|
  |0 0 0 0 0 0 1 0 0 0 1 1 1 1 1 0 1|
  ⌊0 0 0 0 0 0 0 1 1 1 1 0 1 0 1 0 0⌋
A(x) = 1 + 24x^4 + 87x^6 + 185x^8 + 166x^10 + 46x^12 + 3x^14 tmax = 1 polynomial(gamma = 1 + x^4 + x^7 + x^8) 
}
            

We see the code parameters, the minimum distance, G and H, the weight enumerator A(x). The number of correctable errors with a BD decoder is shown as tmax=1 and CECCO also found that the random code happens to be polynomial with generator polynomial 1 + x^4 + x^7 + x^8 (cf. the demo on polynomial and cyclic codes).

We typically prefer systematic encoding with the message in the leftmost k components of each codeword (this requires a generator matrix in standard form, with a k x k identity matrix in the leftmost k columns). We can realize this by moving from code C' to code C:

code.cpp Lines 22–24
    auto C = Cp.get_equivalent_code_in_standard_form();
    assert(C.is_equivalent(Cp));
    std::cout << showall << C << std::endl;
[F_2; 17, 9], dmin = 4
Linear code with properties: {
G = 
  ⌈1 0 0 0 0 0 0 0 0 0 0 0 1 0 1 1 0⌉
  |0 1 0 0 0 0 0 0 0 0 0 0 0 1 0 1 1|
  |0 0 1 0 0 0 0 0 0 1 0 0 0 1 1 0 0|
  |0 0 0 1 0 0 0 0 0 0 1 0 0 0 1 1 0|
  |0 0 0 0 1 0 0 0 0 0 0 1 0 0 0 1 1|
  |0 0 0 0 0 1 0 0 0 1 0 0 1 1 0 0 0|
  |0 0 0 0 0 0 1 0 0 0 1 0 0 1 1 0 0|
  |0 0 0 0 0 0 0 1 0 0 0 1 0 0 1 1 0|
  ⌊0 0 0 0 0 0 0 0 1 0 0 0 1 0 0 1 1⌋
H = 
  ⌈1 0 0 0 0 0 0 0 1 1 1 1 0 1 0 1 0⌉
  |0 1 0 0 0 0 0 0 0 1 1 1 1 0 1 0 1|
  |0 0 1 0 0 0 0 0 0 0 1 1 1 1 0 1 0|
  |0 0 0 1 0 0 0 0 0 0 0 1 1 1 1 0 1|
  |0 0 0 0 1 0 0 0 1 1 1 1 1 0 1 0 0|
  |0 0 0 0 0 1 0 0 0 1 1 1 1 1 0 1 0|
  |0 0 0 0 0 0 1 0 0 0 1 1 1 1 1 0 1|
  ⌊0 0 0 0 0 0 0 1 1 1 1 0 1 0 1 0 0⌋
A(x) = 1 + 24x^4 + 87x^6 + 185x^8 + 166x^10 + 46x^12 + 3x^14 tmax = 1 polynomial(gamma = 1 + x^4 + x^7 + x^8) 
}
            

CECCO can deliver the dual code and applies MacWilliams' identity in order to calculate the weight enumerator. We see that the dual code is not polynomial (other than cyclicity, polynomiality is not preserved by the dual code).

code.cpp Lines 26–27
    auto Cd = C.get_dual();
    std::cout << showall << Cd << std::endl;
[F_2; 17, 8], dmin = 3
Linear code with properties: {
G = 
  ⌈1 0 0 0 0 0 0 0 1 1 1 1 0 1 0 1 0⌉
  |0 1 0 0 0 0 0 0 0 1 1 1 1 0 1 0 1|
  |0 0 1 0 0 0 0 0 0 0 1 1 1 1 0 1 0|
  |0 0 0 1 0 0 0 0 0 0 0 1 1 1 1 0 1|
  |0 0 0 0 1 0 0 0 1 1 1 1 1 0 1 0 0|
  |0 0 0 0 0 1 0 0 0 1 1 1 1 1 0 1 0|
  |0 0 0 0 0 0 1 0 0 0 1 1 1 1 1 0 1|
  ⌊0 0 0 0 0 0 0 1 1 1 1 0 1 0 1 0 0⌋
H = 
  ⌈1 0 0 0 0 0 0 0 0 0 0 0 1 0 1 1 0⌉
  |0 1 0 0 0 0 0 0 0 0 0 0 0 1 0 1 1|
  |0 0 1 0 0 0 0 0 0 1 0 0 0 1 1 0 0|
  |0 0 0 1 0 0 0 0 0 0 1 0 0 0 1 1 0|
  |0 0 0 0 1 0 0 0 0 0 0 1 0 0 0 1 1|
  |0 0 0 0 0 1 0 0 0 1 0 0 1 1 0 0 0|
  |0 0 0 0 0 0 1 0 0 0 1 0 0 1 1 0 0|
  |0 0 0 0 0 0 0 1 0 0 0 1 0 0 1 1 0|
  ⌊0 0 0 0 0 0 0 0 1 0 0 0 1 0 0 1 1⌋
A(x) = 1 + 3x^3 + 3x^4 + 7x^5 + 21x^6 + 40x^7 + 53x^8 + 53x^9 + 40x^10 + 21x^11 + 7x^12 + 3x^13 + 3x^14 + x^17 tmax = 1 
}
           

Besides outputting code properties we can also query them one by one. Some examples are shown below:

code.cpp Lines 29–39
    const auto A = C.get_weight_enumerator();
    const size_t dmin = C.get_dmin();
    const bool is_perfect = C.is_perfect();

    if (C.is_polynomial()) {
        std::cout << "C is polynomial ";
        if (C.is_cyclic()) std::cout << " and cyclic ";
        std::cout << " with generator polynomial " << C.get_gamma() << std::endl;
    } else {
        std::cout << "C is not polynomial" << std::endl;
    }
C is polynomial with generator polynomial 1 + x^4 + x^7 + x^8
            

Some properties are only defined in special cases. One example is the Bhattacharryya bound for binary codes. It requires the Bhattacharyya parameter of a binary channel:

code.cpp Lines 41–46
    if constexpr (std::is_same_v<F, Fp<2>>) {
        const double pe = 0.1;
        BSC channel(pe);
        std::cout << "Word error probability for BSC with pe=" << pe << " is at most "
                  << C.Bhattacharyya_bound(channel.get_Bhattacharyya_param()) << std::endl;
    }
Word error probability of ML decoding for BSC with pe=0.01 is at most 0.0435097
             

CECCO can calculate minimal trellises for codes over arbitrary finite fields. These trellises are used by the Viterbi and BCJR decoding algorithms (cf. the demo on soft decoding). They can be exported in TiKZ format for use in LaTeX documents:

code.cpp Lines 48–49
    auto T = C.get_minimal_trellis();
    T.export_as_tikz("trellis.tex");

For general linear codes, CECCO supports a variety of hard-input decoders. Standard array-based bounded-distance BD and ML decoding with and without erasures (dec_BD, dec_BD_EE, dec_ML, dec_ML_EE), hard-input Viterbi ML decoding with and without erasures (dec_Viterbi, dec_Viterbi_EE), and pseudo-soft-input GMD decoding (dec_GMD). Note that the EE decoders as well as GMD decoding require #define CECCO_ERASURE_SUPPORT.

First, we generate a random codeword of C':

code.cpp Lines 52–54
    auto u = Vector<F>(k).randomize();
    std::cout << "Random message:               " << u << std::endl;
    auto c = C.enc(u);
Random message:               (0, 1, 0, 0, 1, 0, 1, 0, 1)
                     

Then we transmit over a discrete memoryless channel (a BSC in case F = Fp<2>) for a hard-input decoding scenario:

code.cpp Lines 57–61
        const double pe = 0.05;
        SDMC<F> channel(pe);

        // RX
        auto r = channel(c);

BD decoding can fail with a CECCO::decoding_failure so we have to wrap the decoder in a try/catch block:

code.cpp Lines 62–75
        Vector<F> c_est, u_est;
        try {
            c_est = C.dec_BD(r);
            u_est = C.encinv(c_est);
            std::cout << "BD decoding message estimate: " << u_est << std::endl;
            if (u_est == u) {
                std::cout << "... this is correct decoding." << std::endl;
            } else {
                std::cout << "... this is wrong decoding/a word error with " << dH(u, u_est)
                          << " (message) symbol errors" << std::endl;
            }
        } catch (const decoding_failure& e) {
            std::cout << "BD decoding failure!" << std::endl;
        }

ML decoding can never fail:

code.cpp Lines 76–84
        c_est = C.dec_ML(r);
        u_est = C.encinv(c_est);
        std::cout << "ML decoding message estimate: " << u_est << std::endl;
        if (u_est == u) {
            std::cout << "... this is correct decoding." << std::endl;
        } else {
            std::cout << "... this is wrong decoding/a word error with " << dH(u, u_est) << " (message) symbol errors"
                      << std::endl;
        }

For and exemplary transmission, both decoders may provide the correct message estimate:

BD decoding message estimate: ( 0, 1, 0, 0, 1, 0, 1, 0, 1 )
... this is correct decoding.
ML decoding message estimate: ( 0, 1, 0, 0, 1, 0, 1, 0, 1 )
... this is correct decoding.
                     

In case the received vector r contains more than tmax errors, different ML decoders may come up with different codeword estimates (due to random tie breaking or implementational details). However, it is guaranteed that the Hamming distance of both codeword estimates to r is identical. Thus, we can assert the following (since Viterbi is an alternative ML decoder):

code.cpp Lines 85–85
        assert(dH(r, c_est) == dH(r, C.dec_Viterbi(r)));

Transmitting over a discrete memoryless channel with erasures looks almost identical (using the corresponding EE decoder):

code.cpp Lines 89–118
        const double pe = 0.025;
        const double px = 0.05;
        SDMEC<F> channel(pe, px);

        // RX
        auto r = channel(c);
        Vector<F> c_est, u_est;
        try {
            c_est = C.dec_BD_EE(r);
            u_est = C.encinv(c_est);
            std::cout << "BD EE decoding message estimate: " << u_est << std::endl;
            if (u_est == u) {
                std::cout << "... this is correct decoding." << std::endl;
            } else {
                std::cout << "... this is wrong decoding/a word error with " << dH(u, u_est)
                          << " (message) symbol errors" << std::endl;
            }
        } catch (const decoding_failure& e) {
            std::cout << "BD EE decoding failure!" << std::endl;
        }
        c_est = C.dec_ML_EE(r);
        u_est = C.encinv(c_est);
        std::cout << "ML EE decoding message estimate: " << u_est << std::endl;
        if (u_est == u) {
            std::cout << "... this is correct decoding." << std::endl;
        } else {
            std::cout << "... this is wrong decoding/a word error with " << dH(u, u_est) << " (message) symbol errors"
                      << std::endl;
        }
        assert(dH(r, c_est) == dH(r, C.dec_Viterbi_EE(r)));

Besides the hard received vector r, pseudo-soft GMD decoding also requires a vector of reliabilities. Reliability 1 means full reliability, 0 means completely unreliable. One way to obtain r and the reliability vector is to calculate it based on the per-symbol LLR matrix obtained from transmitting the binary image of the codeword over a BI-AWGN channel. In this approach, we find the two F-symbols with the smallest cost (smallest entry in the per-symbol LLR matrix column) and then map their difference to the interval [0, 1] using the tanh function.

code.cpp Lines 121–174
    // note: this only works if F has characteristic 2

    DEMUX<F, Fp<2>> demux;
    const double EbNodB = 0;
    BI_AWGN channel(EbNodB);
    LLRCalculator<F> llrcalculator(channel);

    // RX
    auto y = channel(demux(c));
    auto llrs = llrcalculator(y);
    std::cout << "Per-symbol LLR matrix: " << std::endl << llrs << std::endl;

    auto r = Vector<F>(n);
    auto reliabilities = Vector<double>(n);

    for (size_t i = 0; i < n; ++i) {
        size_t best_label = 0; // initialization, F(0) is best
        double best = 0.0;
        double second = std::numeric_limits<double>::infinity();
        for (size_t a = 1; a < F::get_q(); ++a) {
            const double cost = llrs(a - 1, i);
            if (cost < best) {
                second = best;
                best = cost;
                best_label = a;
            } else if (cost < second) {
                second = cost;
            }
        }

        // hard decision: most probable symbol
        r.set_component(i, F(best_label));

        // reliability in [0, 1]: best-vs-second through tanh
        const double lambda = second - best;
        reliabilities.set_component(i, std::tanh(lambda / 2.0));
    }

    std::cout << "Hard decision r: " << std::endl << r << std::endl;
    std::cout << "Reliabilities: " << std::endl << reliabilities << std::endl;

    try {
        auto c_est = C.dec_GMD(r, reliabilities);
        auto u_est = C.encinv(c_est);
        std::cout << "GMD decoding message estimate: " << u_est << std::endl;
        if (u_est == u) {
            std::cout << "... this is correct decoding." << std::endl;
        } else {
            std::cout << "... this is wrong decoding/a word error with " << dH(u, u_est) << " (message) symbol errors"
                      << std::endl;
        }
    } catch (const decoding_failure& e) {
        std::cout << "GMD decoding failure: " << e.what() << std::endl;
    }

We stress that GMD decoding can fail in case all of its internal BD EE decoders fail.

A complete, compilable demo is shown below:

code.cpp
#include <iostream>

#define CECCO_ERASURE_SUPPORT
#include "cecco.hpp"
using namespace CECCO;

int main(void) {
    using F = Ext<Fp<2>, MOD{1, 1, 1}>;

    const size_t n = 13;
    const size_t k = 6;
    auto G = ZeroMatrix<F>(k, n);
    do {
        G.randomize();
    } while (G.rank() < k);

    auto Cp = LinearCode(n, k, G);
    // std::cout << showbasic << Cp << std::endl;
    // std::cout << showmost << Cp << std::endl;
    std::cout << showall << Cp << std::endl;

    auto C = Cp.get_equivalent_code_in_standard_form();
    assert(C.is_equivalent(Cp));
    std::cout << showall << C << std::endl;

    auto Cd = C.get_dual();
    std::cout << showall << Cd << std::endl;

    const auto A = C.get_weight_enumerator();
    const size_t dmin = C.get_dmin();
    const bool is_perfect = C.is_perfect();

    if (C.is_polynomial()) {
        std::cout << "C is polynomial ";
        if (C.is_cyclic()) std::cout << " and cyclic ";
        std::cout << " with generator polynomial " << C.get_gamma() << std::endl;
    } else {
        std::cout << "C is not polynomial" << std::endl;
    }

    if constexpr (std::is_same_v<F, Fp<2>>) {
        const double pe = 0.1;
        BSC channel(pe);
        std::cout << "Word error probability for BSC with pe=" << pe << " is at most "
                  << C.Bhattacharyya_bound(channel.get_Bhattacharyya_param()) << std::endl;
    }

    auto T = C.get_minimal_trellis();
    T.export_as_tikz("trellis.tex");

    // TX
    auto u = Vector<F>(k).randomize();
    std::cout << "Random message:               " << u << std::endl;
    auto c = C.enc(u);

    {
        const double pe = 0.05;
        SDMC<F> channel(pe);

        // RX
        auto r = channel(c);
        Vector<F> c_est, u_est;
        try {
            c_est = C.dec_BD(r);
            u_est = C.encinv(c_est);
            std::cout << "BD decoding message estimate: " << u_est << std::endl;
            if (u_est == u) {
                std::cout << "... this is correct decoding." << std::endl;
            } else {
                std::cout << "... this is wrong decoding/a word error with " << dH(u, u_est)
                          << " (message) symbol errors" << std::endl;
            }
        } catch (const decoding_failure& e) {
            std::cout << "BD decoding failure!" << std::endl;
        }
        c_est = C.dec_ML(r);
        u_est = C.encinv(c_est);
        std::cout << "ML decoding message estimate: " << u_est << std::endl;
        if (u_est == u) {
            std::cout << "... this is correct decoding." << std::endl;
        } else {
            std::cout << "... this is wrong decoding/a word error with " << dH(u, u_est) << " (message) symbol errors"
                      << std::endl;
        }
        assert(dH(r, c_est) == dH(r, C.dec_Viterbi(r)));
    }

    {
        const double pe = 0.025;
        const double px = 0.05;
        SDMEC<F> channel(pe, px);

        // RX
        auto r = channel(c);
        Vector<F> c_est, u_est;
        try {
            c_est = C.dec_BD_EE(r);
            u_est = C.encinv(c_est);
            std::cout << "BD EE decoding message estimate: " << u_est << std::endl;
            if (u_est == u) {
                std::cout << "... this is correct decoding." << std::endl;
            } else {
                std::cout << "... this is wrong decoding/a word error with " << dH(u, u_est)
                          << " (message) symbol errors" << std::endl;
            }
        } catch (const decoding_failure& e) {
            std::cout << "BD EE decoding failure!" << std::endl;
        }
        c_est = C.dec_ML_EE(r);
        u_est = C.encinv(c_est);
        std::cout << "ML EE decoding message estimate: " << u_est << std::endl;
        if (u_est == u) {
            std::cout << "... this is correct decoding." << std::endl;
        } else {
            std::cout << "... this is wrong decoding/a word error with " << dH(u, u_est) << " (message) symbol errors"
                      << std::endl;
        }
        assert(dH(r, c_est) == dH(r, C.dec_Viterbi_EE(r)));
    }

    // note: this only works if F has characteristic 2

    DEMUX<F, Fp<2>> demux;
    const double EbNodB = 0;
    BI_AWGN channel(EbNodB);
    LLRCalculator<F> llrcalculator(channel);

    // RX
    auto y = channel(demux(c));
    auto llrs = llrcalculator(y);
    std::cout << "Per-symbol LLR matrix: " << std::endl << llrs << std::endl;

    auto r = Vector<F>(n);
    auto reliabilities = Vector<double>(n);

    for (size_t i = 0; i < n; ++i) {
        size_t best_label = 0; // initialization, F(0) is best
        double best = 0.0;
        double second = std::numeric_limits<double>::infinity();
        for (size_t a = 1; a < F::get_q(); ++a) {
            const double cost = llrs(a - 1, i);
            if (cost < best) {
                second = best;
                best = cost;
                best_label = a;
            } else if (cost < second) {
                second = cost;
            }
        }

        // hard decision: most probable symbol
        r.set_component(i, F(best_label));

        // reliability in [0, 1]: best-vs-second through tanh
        const double lambda = second - best;
        reliabilities.set_component(i, std::tanh(lambda / 2.0));
    }

    std::cout << "Hard decision r: " << std::endl << r << std::endl;
    std::cout << "Reliabilities: " << std::endl << reliabilities << std::endl;

    try {
        auto c_est = C.dec_GMD(r, reliabilities);
        auto u_est = C.encinv(c_est);
        std::cout << "GMD decoding message estimate: " << u_est << std::endl;
        if (u_est == u) {
            std::cout << "... this is correct decoding." << std::endl;
        } else {
            std::cout << "... this is wrong decoding/a word error with " << dH(u, u_est) << " (message) symbol errors"
                      << std::endl;
        }
    } catch (const decoding_failure& e) {
        std::cout << "GMD decoding failure: " << e.what() << std::endl;
    }

    return 0;
}