4. Library Examples

All the following examples simulate a basic communication chain with a BPSK modem and a repetition code over an AWGN channel. The BER/FER results are calculated from 0.0 dB to 10.0 dB with a 1.0 dB step.

../../_images/simple_chain.svg

Fig. 4.1 Simulated communication chain.

Note

All the following examples of code are available in a dedicated GitHub repository: https://github.com/aff3ct/my_project_with_aff3ct. Sometime the full source codes in the repository may slighly differ from the ones on this page, but the philosophy remains the same.

4.1. Bootstrap

The bootstrap example is the easiest way to start using the AFF3CT library. It is based on C++ classes and methods that operate on buffers. Keep in mind that this is the simplest way to use AFF3CT, but not the most powerful way. More advanced features such as benchmarking, debugging, command line interfacing, and more are illustrated in the Tasks and Factory examples.

../../_images/simple_chain_code.svg

Fig. 4.2 Simulated communication chain: classes and methods.

Fig. 4.2 illustrates the source code: green boxes correspond to classes, blue boxes correspond to methods, and arrows represent buffers. Some of the AFF3CT classes inherit from the Module abstract class. Generally speaking, any class defining methods for a communication chain is a module (green boxes in Fig. 4.2).

Listing 4.1 Bootstrap: main function
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include <aff3ct.hpp>
using namespace aff3ct;

int main(int argc, char** argv)
{
        params1 p;  init_params1 (p   ); // create and initialize the parameters defined by the user
        modules1 m; init_modules1(p, m); // create and initialize modules
        buffers1 b; init_buffers1(p, b); // create and initialize the buffers required by the modules
        utils1 u;   init_utils1  (m, u); // create and initialize utils

        // display the legend in the terminal
        u.terminal->legend();

        // loop over SNRs range
        for (auto ebn0 = p.ebn0_min; ebn0 < p.ebn0_max; ebn0 += p.ebn0_step)
        {
                // compute the current sigma for the channel noise
                const auto esn0  = tools::ebn0_to_esn0 (ebn0, p.R);
                const auto sigma = tools::esn0_to_sigma(esn0     );

                u.noise->set_values(sigma, ebn0, esn0);

                // update the sigma of the modem and the channel
                m.modem  ->set_noise(*u.noise);
                m.channel->set_noise(*u.noise);

                // display the performance (BER and FER) in real time (in a separate thread)
                u.terminal->start_temp_report();

                // run the simulation chain
                while (!m.monitor->fe_limit_achieved())
                {
                        m.source ->generate    (                 b.ref_bits     );
                        m.encoder->encode      (b.ref_bits,      b.enc_bits     );
                        m.modem  ->modulate    (b.enc_bits,      b.symbols      );
                        m.channel->add_noise   (b.symbols,       b.noisy_symbols);
                        m.modem  ->demodulate  (b.noisy_symbols, b.LLRs         );
                        m.decoder->decode_siho (b.LLRs,          b.dec_bits     );
                        m.monitor->check_errors(b.dec_bits,      b.ref_bits     );
                }

                // display the performance (BER and FER) in the terminal
                u.terminal->final_report();

                // reset the monitor for the next SNR
                m.monitor->reset();
                u.terminal->reset();
        }

        return 0;
}

Listing 4.1 gives an overview of what can be achieved with the AFF3CT library. The firsts lines 6-9 are dedicated to the objects instantiations and buffers allocation through dedicated structures. p contains the simulation parameters, b contains the buffers required by the modules, m contains the modules of the communication chain and u is a set of convenient helper objects.

Line 15 loops over the desired SNRs range. Lines 31-40, the while loop iterates until 100 frame errors have been detected by the monitor. The AFF3CT communication chain methods are called inside this loop. Each AFF3CT method works on input(s) and/or output(s) buffer(s) that have been declared at line 8. Those buffers can be std::vector, or pointers to user-allocated memory areas. The sizes and the types of those buffers have to be set in accordance with the corresponding sizes and types of the AFF3CT modules declared at line 7. If there is a size and/or type mismatch, the AFF3CT library throws an exception. The AFF3CT modules are classes that use the C++ meta-programming technique (e.g. C++ templates). By default those templates are instantiated to int32_t or float.

Listing 4.2 Bootstrap: parameters
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
struct params1
{
        int   K         =  32;     // number of information bits
        int   N         = 128;     // codeword size
        int   fe        = 100;     // number of frame errors
        int   seed      =   0;     // PRNG seed for the AWGN channel
        float ebn0_min  =   0.00f; // minimum SNR value
        float ebn0_max  =  10.01f; // maximum SNR value
        float ebn0_step =   1.00f; // SNR step
        float R;                   // code rate (R=K/N)
};

void init_params1(params1 &p)
{
        p.R = (float)p.K / (float)p.N;
}

Listing 4.2 describes the params1 simulation structure and the init_params1 function used at line 6 in Listing 4.1.

Listing 4.3 Bootstrap: modules
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
struct modules1
{
        std::unique_ptr<module::Source_random<>>          source;
        std::unique_ptr<module::Encoder_repetition_sys<>> encoder;
        std::unique_ptr<module::Modem_BPSK<>>             modem;
        std::unique_ptr<module::Channel_AWGN_LLR<>>       channel;
        std::unique_ptr<module::Decoder_repetition_std<>> decoder;
        std::unique_ptr<module::Monitor_BFER<>>           monitor;
};

void init_modules1(const params1 &p, modules1 &m)
{
        m.source  = std::unique_ptr<module::Source_random         <>>(new module::Source_random         <>(p.K        ));
        m.encoder = std::unique_ptr<module::Encoder_repetition_sys<>>(new module::Encoder_repetition_sys<>(p.K, p.N   ));
        m.modem   = std::unique_ptr<module::Modem_BPSK            <>>(new module::Modem_BPSK            <>(p.N        ));
        m.channel = std::unique_ptr<module::Channel_AWGN_LLR      <>>(new module::Channel_AWGN_LLR      <>(p.N, p.seed));
        m.decoder = std::unique_ptr<module::Decoder_repetition_std<>>(new module::Decoder_repetition_std<>(p.K, p.N   ));
        m.monitor = std::unique_ptr<module::Monitor_BFER          <>>(new module::Monitor_BFER          <>(p.K, p.fe  ));
};

Listing 4.1 describes the modules1 structure and the init_modules1 function used at line 7 in Listing 4.1. The init_modules1 function allocates the modules of the communication chain. Those modules are allocated on the heap and managed by smart pointers (std::unique_ptr). Note that the init_modules1 function takes a params1 structure from Listing 4.2 in parameter. These parameters are used to build the modules.

Listing 4.4 Bootstrap: buffers
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
struct buffers1
{
        std::vector<int  > ref_bits;
        std::vector<int  > enc_bits;
        std::vector<float> symbols;
        std::vector<float> noisy_symbols;
        std::vector<float> LLRs;
        std::vector<int  > dec_bits;
};

void init_buffers1(const params1 &p, buffers1 &b)
{
        b.ref_bits      = std::vector<int  >(p.K);
        b.enc_bits      = std::vector<int  >(p.N);
        b.symbols       = std::vector<float>(p.N);
        b.noisy_symbols = std::vector<float>(p.N);
        b.LLRs          = std::vector<float>(p.N);
        b.dec_bits      = std::vector<int  >(p.K);
}

Listing 4.4 describes the buffers1 structure and the init_buffers1 function used at line 8 in Listing 4.1. The init_buffers1 function allocates the buffers of the communication chain. Here, we chose to allocate buffers as instances of the std::vector C++ standard class. As for the modules in Listing 4.3, the size of the buffers is obtained from the params1 input structure (cf. Listing 4.2).

Listing 4.5 Bootstrap: utils
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
struct utils1
{
        std::unique_ptr<tools::Sigma<>>               noise;     // a sigma noise type
        std::vector<std::unique_ptr<tools::Reporter>> reporters; // list of reporters dispayed in the terminal
        std::unique_ptr<tools::Terminal_std>          terminal;  // manage the output text in the terminal
};

void init_utils1(const modules1 &m, utils1 &u)
{
        // create a sigma noise type
        u.noise = std::unique_ptr<tools::Sigma<>>(new tools::Sigma<>());
        // report the noise values (Es/N0 and Eb/N0)
        u.reporters.push_back(std::unique_ptr<tools::Reporter>(new tools::Reporter_noise<>(*u.noise)));
        // report the bit/frame error rates
        u.reporters.push_back(std::unique_ptr<tools::Reporter>(new tools::Reporter_BFER<>(*m.monitor)));
        // report the simulation throughputs
        u.reporters.push_back(std::unique_ptr<tools::Reporter>(new tools::Reporter_throughput<>(*m.monitor)));
        // create a terminal that will display the collected data from the reporters
        u.terminal = std::unique_ptr<tools::Terminal_std>(new tools::Terminal_std(u.reporters));
}

Listing 4.5 describes the utils1 structure and the init_utils1 function used at line 9 in Listing 4.1. The init_utils1 function allocates 1) the noise object that contains the type of noise we want to simulate (e.g. sigma), 2) a terminal object that takes care of printing the BER/FER to the console. Three reporters are created, one to print SNR, second one to print BER/FER, and the last one to report the simulation throughput in the terminal.

If you run the bootstrap example, the expected output is shown in Listing 4.6.

Listing 4.6 Bootstrap: output
# ---------------------||------------------------------------------------------||---------------------
#  Signal Noise Ratio  ||   Bit Error Rate (BER) and Frame Error Rate (FER)    ||  Global throughput
#         (SNR)        ||                                                      ||  and elapsed time
# ---------------------||------------------------------------------------------||---------------------
# ----------|----------||----------|----------|----------|----------|----------||----------|----------
#     Es/N0 |    Eb/N0 ||      FRA |       BE |       FE |      BER |      FER ||  SIM_THR |    ET/RT
#      (dB) |     (dB) ||          |          |          |          |          ||   (Mb/s) | (hhmmss)
# ----------|----------||----------|----------|----------|----------|----------||----------|----------
      -6.02 |     0.00 ||      108 |      262 |      100 | 7.58e-02 | 9.26e-01 ||    2.382 | 00h00'00
      -5.02 |     1.00 ||      125 |      214 |      100 | 5.35e-02 | 8.00e-01 ||    4.813 | 00h00'00
      -4.02 |     2.00 ||      136 |      179 |      100 | 4.11e-02 | 7.35e-01 ||    3.804 | 00h00'00
      -3.02 |     3.00 ||      210 |      135 |      100 | 2.01e-02 | 4.76e-01 ||    4.516 | 00h00'00
      -2.02 |     4.00 ||      327 |      122 |      100 | 1.17e-02 | 3.06e-01 ||    5.157 | 00h00'00
      -1.02 |     5.00 ||      555 |      112 |      100 | 6.31e-03 | 1.80e-01 ||    4.703 | 00h00'00
      -0.02 |     6.00 ||     1619 |      108 |      100 | 2.08e-03 | 6.18e-02 ||    4.110 | 00h00'00
       0.98 |     7.00 ||     4566 |      102 |      100 | 6.98e-04 | 2.19e-02 ||    4.974 | 00h00'00
       1.98 |     8.00 ||    15998 |      100 |      100 | 1.95e-04 | 6.25e-03 ||    4.980 | 00h00'00
       2.98 |     9.00 ||    93840 |      100 |      100 | 3.33e-05 | 1.07e-03 ||    5.418 | 00h00'00
       3.98 |    10.00 ||   866433 |      100 |      100 | 3.61e-06 | 1.15e-04 ||    4.931 | 00h00'05

4.2. Tasks

Inside a Module class, there can be many public methods; however, only some of them are directly used in the communication chain. A method usable in a chain is named a Task. A Task is characterized by its behavior and its data: the input and output data are declared via a collection of Socket objects.

Listing 4.7 Tasks: main function
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#include <aff3ct.hpp>
using namespace aff3ct;

int main(int argc, char** argv)
{
        params1  p; init_params1 (p   ); // create and initialize the parameters defined by the user
        modules1 m; init_modules2(p, m); // create and initialize modules
        // the 'init_buffers1' function is not required anymore
        utils1   u; init_utils1  (m, u); // create and initialize the utils

        // display the legend in the terminal
        u.terminal->legend();

        // sockets binding (connect sockets of successive tasks in the chain: the output socket of a task fills the input socket of the next task in the chain)
        using namespace module;
        (*m.encoder)[enc::sck::encode      ::U_K ].bind((*m.source )[src::sck::generate   ::U_K ]);
        (*m.modem  )[mdm::sck::modulate    ::X_N1].bind((*m.encoder)[enc::sck::encode     ::X_N ]);
        (*m.channel)[chn::sck::add_noise   ::X_N ].bind((*m.modem  )[mdm::sck::modulate   ::X_N2]);
        (*m.modem  )[mdm::sck::demodulate  ::Y_N1].bind((*m.channel)[chn::sck::add_noise  ::Y_N ]);
        (*m.decoder)[dec::sck::decode_siho ::Y_N ].bind((*m.modem  )[mdm::sck::demodulate ::Y_N2]);
        (*m.monitor)[mnt::sck::check_errors::U   ].bind((*m.encoder)[enc::sck::encode     ::U_K ]);
        (*m.monitor)[mnt::sck::check_errors::V   ].bind((*m.decoder)[dec::sck::decode_siho::V_K ]);

        // loop over the range of SNRs
        for (auto ebn0 = p.ebn0_min; ebn0 < p.ebn0_max; ebn0 += p.ebn0_step)
        {
                // compute the current sigma for the channel noise
                const auto esn0  = tools::ebn0_to_esn0 (ebn0, p.R);
                const auto sigma = tools::esn0_to_sigma(esn0     );

                u.noise->set_values(sigma, ebn0, esn0);

                // update the sigma of the modem and the channel
                m.modem  ->set_noise(*u.noise);
                m.channel->set_noise(*u.noise);

                // display the performance (BER and FER) in real time (in a separate thread)
                u.terminal->start_temp_report();

                // run the simulation chain
                while (!m.monitor->fe_limit_achieved())
                {
                        (*m.source )[src::tsk::generate    ].exec();
                        (*m.encoder)[enc::tsk::encode      ].exec();
                        (*m.modem  )[mdm::tsk::modulate    ].exec();
                        (*m.channel)[chn::tsk::add_noise   ].exec();
                        (*m.modem  )[mdm::tsk::demodulate  ].exec();
                        (*m.decoder)[dec::tsk::decode_siho ].exec();
                        (*m.monitor)[mnt::tsk::check_errors].exec();
                }

                // display the performance (BER and FER) in the terminal
                u.terminal->final_report();

                // reset the monitor and the terminal for the next SNR
                m.monitor->reset();
                u.terminal->reset();
        }

        // display the statistics of the tasks (if enabled)
        tools::Stats::show({ m.source.get(), m.encoder.get(), m.modem.get(), m.channel.get(), m.decoder.get(), m.monitor.get() }, true);

        return 0;
}

Listing 4.7 shows how the Module, Task and Socket objects work together. Line 7, init_modules2 differs slightly from the previous init_modules1 function, Listing 4.8 details the changes.

Thanks to the use of Task and Socket objects, it is now possible to skip the buffer allocation part (see line 8), which is handled transparently by these objects. For that, the connections between the sockets of successive tasks in the chain have to be established explicitly: this is the binding process shown at lines 14-22, using the bind method. In return, to execute the tasks (lines 43-49), we now only need to call the exec method, without any parameters.

Using the bind and exec methods bring new useful features for debugging and benchmarking. In Listing 4.7, some statistics about tasks are collected and reported at lines 60-61 (see the --sim-stats section for more informations about the statistics output).

Listing 4.8 Tasks: modules
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void init_modules2(const params1 &p, modules1 &m)
{
        m.source  = std::unique_ptr<module::Source_random         <>>(new module::Source_random         <>(p.K        ));
        m.encoder = std::unique_ptr<module::Encoder_repetition_sys<>>(new module::Encoder_repetition_sys<>(p.K, p.N   ));
        m.modem   = std::unique_ptr<module::Modem_BPSK            <>>(new module::Modem_BPSK            <>(p.N        ));
        m.channel = std::unique_ptr<module::Channel_AWGN_LLR      <>>(new module::Channel_AWGN_LLR      <>(p.N, p.seed));
        m.decoder = std::unique_ptr<module::Decoder_repetition_std<>>(new module::Decoder_repetition_std<>(p.K, p.N   ));
        m.monitor = std::unique_ptr<module::Monitor_BFER          <>>(new module::Monitor_BFER          <>(p.K, p.fe  ));

        // configuration of the module tasks
        std::vector<const module::Module*> modules_list = { m.source.get(), m.encoder.get(), m.modem.get(), m.channel.get(), m.decoder.get(), m.monitor.get() };
        for (auto& mod : modules_list)
                for (auto& tsk : mod->tasks)
                {
                        tsk->set_autoalloc  (true ); // enable the automatic allocation of data buffers in the tasks
                        tsk->set_debug      (false); // disable the debug mode
                        tsk->set_debug_limit(16   ); // display only the 16 first bits if the debug mode is enabled
                        tsk->set_stats      (true ); // enable statistics collection

                        // enable fast mode (= disable optional checks in the tasks) if there is no debug and stats modes
                        if (!tsk->is_debug() && !tsk->is_stats())
                                tsk->set_fast(true);
                }
}

The beginning of the init_modules2 function (Listing 4.8) is the same as the init_module1 function (Listing 4.3). At lines 10-23, each Module is parsed to get its tasks, each Task is configured to automatically allocate its outputs Socket memory (line 15) and collect statistics on the Task execution (line 19). It is also possible to print debug information by toggling boolean to true at line 17.

4.3. Factory

In the previous Bootstrap and Tasks examples, the AFF3CT Module classes were built statically in the source code. In the Factory example, factory classes are used instead, to build modules dynamically from command line arguments.

Listing 4.9 Factory: main function
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#include <aff3ct.hpp>
using namespace aff3ct;

int main(int argc, char** argv)
{
        params3  p; init_params3 (argc, argv, p); // create and initialize the parameters from the command line with factories
        modules3 m; init_modules3(p, m         ); // create and initialize modules
        utils1   u; init_utils3  (p, m, u      ); // create and initialize utils

        // [...]

        // display the statistics of the tasks (if enabled)
        tools::Stats::show({ m.source.get(), m.modem.get(), m.channel.get(), m.monitor.get(), m.encoder, m.decoder }, true);

        return 0;
}

The main function in Listing 4.9 is almost unchanged from the main function in Listing 4.7.

Listing 4.10 Factory: parameters
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
struct params3
{
        float ebn0_min  =  0.00f; // minimum SNR value
        float ebn0_max  = 10.01f; // maximum SNR value
        float ebn0_step =  1.00f; // SNR step
        float R;                  // code rate (R=K/N)

        std::unique_ptr<factory::Source          > source;
        std::unique_ptr<factory::Codec_repetition> codec;
        std::unique_ptr<factory::Modem           > modem;
        std::unique_ptr<factory::Channel         > channel;
        std::unique_ptr<factory::Monitor_BFER    > monitor;
        std::unique_ptr<factory::Terminal        > terminal;
};

void init_params3(int argc, char** argv, params3 &p)
{
        p.source   = std::unique_ptr<factory::Source          >(new factory::Source          ());
        p.codec    = std::unique_ptr<factory::Codec_repetition>(new factory::Codec_repetition());
        p.modem    = std::unique_ptr<factory::Modem           >(new factory::Modem           ());
        p.channel  = std::unique_ptr<factory::Channel         >(new factory::Channel         ());
        p.monitor  = std::unique_ptr<factory::Monitor_BFER    >(new factory::Monitor_BFER    ());
        p.terminal = std::unique_ptr<factory::Terminal        >(new factory::Terminal        ());

        std::vector<factory::Factory*> params_list = { p.source .get(), p.codec  .get(), p.modem   .get(),
                                                       p.channel.get(), p.monitor.get(), p.terminal.get() };

        // parse command line arguments for the given parameters and fill them
        tools::Command_parser cp(argc, argv, params_list, true);
        if (cp.parsing_failed())
        {
                cp.print_help    ();
                cp.print_warnings();
                cp.print_errors  ();
                std::exit(1);
        }

        std::cout << "# Simulation parameters: " << std::endl;
        tools::Header::print_parameters(params_list); // display the headers (= print the AFF3CT parameters on the screen)
        std::cout << "#" << std::endl;
        cp.print_warnings();

        p.R = (float)p.codec->enc->K / (float)p.codec->enc->N_cw; // compute the code rate
}

The params3 structure from Listing 4.10 contains some pointers to factory objects (lines 8-13). SNR parameters remain static is this examples.

The init_params3 function takes two new input arguments from the command line: argc and argv. The function first allocates the factories (lines 18-23) and then those factories are supplied with parameters from the command line (line 29) thanks to the tools::Command_parser class. Lines 38-41, the parameters from the factories are printed to the terminal.

Note that in this example a repetition code is used, however it is very easy to select another code type, for instance by replacing repetition line 9 and line 19 by polar to work with polar code.

Listing 4.11 Factory: modules
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
struct modules3
{
        std::unique_ptr<module::Source<>>       source;
        std::unique_ptr<tools ::Codec_SIHO<>>   codec;
        std::unique_ptr<module::Modem<>>        modem;
        std::unique_ptr<module::Channel<>>      channel;
        std::unique_ptr<module::Monitor_BFER<>> monitor;
                        module::Encoder<>*      encoder;
                        module::Decoder_SIHO<>* decoder;
};

void init_modules3(const params3 &p, modules3 &m)
{
        m.source  = std::unique_ptr<module::Source      <>>(p.source ->build());
        m.codec   = std::unique_ptr<tools ::Codec_SIHO  <>>(p.codec  ->build());
        m.modem   = std::unique_ptr<module::Modem       <>>(p.modem  ->build());
        m.channel = std::unique_ptr<module::Channel     <>>(p.channel->build());
        m.monitor = std::unique_ptr<module::Monitor_BFER<>>(p.monitor->build());
        m.encoder = m.codec->get_encoder().get();
        m.decoder = m.codec->get_decoder_siho().get();

        // configuration of the module tasks
        std::vector<const module::Module*> modules_list = { m.source.get(), m.modem.get(), m.channel.get(), m.monitor.get(), m.encoder, m.decoder };
        for (auto& mod : modules_list)
                for (auto& tsk : mod->tasks)
                {
                        tsk->set_autoalloc  (true ); // enable the automatic allocation of the data in the tasks
                        tsk->set_debug      (false); // disable the debug mode
                        tsk->set_debug_limit(16   ); // display only the 16 first bits if the debug mode is enabled
                        tsk->set_stats      (true ); // enable the statistics

                        // enable the fast mode (= disable the useless verifs in the tasks) if there is no debug and stats modes
                        if (!tsk->is_debug() && !tsk->is_stats())
                                tsk->set_fast(true);
                }
}

In Listing 4.11 the modules3 structure changes a little bit because a Codec class is used to aggregate the Encoder and the Decoder together. In the init_modules3 the factories allocated in Listing 4.10 are used to build the modules (lines 14-18).

Listing 4.12 Factory: utils
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
void init_utils3(const params3 &p, const modules3 &m, utils1 &u)
{
        // create a sigma noise type
        u.noise = std::unique_ptr<tools::Sigma<>>(new tools::Sigma<>());
        // report noise values (Es/N0 and Eb/N0)
        u.reporters.push_back(std::unique_ptr<tools::Reporter>(new tools::Reporter_noise<>(*u.noise)));
        // report bit/frame error rates
        u.reporters.push_back(std::unique_ptr<tools::Reporter>(new tools::Reporter_BFER<>(*m.monitor)));
        // report simulation throughputs
        u.reporters.push_back(std::unique_ptr<tools::Reporter>(new tools::Reporter_throughput<>(*m.monitor)));
        // create a terminal object that will display the collected data from the reporters
        u.terminal = std::unique_ptr<tools::Terminal>(p.terminal->build(u.reporters));
}

In the Listing 4.12, the init_utils3 changes a little bit from the init_utils1 function (Listing 4.5) because at line 12 a factory is used to build the terminal.

To execute the binary it is now required to specify the number of information bits K and the frame size N as shown in Listing 4.13.

Listing 4.13 Factory: execute the binary
./bin/my_project -K 32 -N 128

Be aware that many other parameters can be set from the command line. The parameters list can be seen using -h as shown in Listing 4.14.

Listing 4.14 Factory: display the command line parameters
./bin/my_project -h

Those parameters are documented in the Parameters section.

4.4. OpenMP

In the previous examples the code is mono-threaded. To take advantage of the today multi-core CPUs some modifications have to be made. This example starts from the previous Factory example and adapts it to work on multi-threaded architectures using pragma directives of the well-known OpenMP language.

Listing 4.15 OpenMP: main function
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
int main(int argc, char** argv)
{
        params3 p; init_params3(argc, argv, p); // create and initialize the parameters from the command line with factories
        utils4 u; // create an 'utils4' structure

#pragma omp parallel
{
#pragma omp single
{
        // get the number of available threads from OpenMP
        const size_t n_threads = (size_t)omp_get_num_threads();
        u.monitors.resize(n_threads);
        u.modules .resize(n_threads);
}
        modules4 m; init_modules_and_utils4(p, m, u); // create and initialize the modules and initialize a part of the utils

#pragma omp barrier
#pragma omp single
{
        init_utils4(p, u); // finalize the utils initialization

        // display the legend in the terminal
        u.terminal->legend();
}
        // sockets binding (connect the sockets of the tasks = fill the input sockets with the output sockets)
        using namespace module;
        (*m.encoder)[enc::sck::encode      ::U_K ].bind((*m.source )[src::sck::generate   ::U_K ]);
        (*m.modem  )[mdm::sck::modulate    ::X_N1].bind((*m.encoder)[enc::sck::encode     ::X_N ]);
        (*m.channel)[chn::sck::add_noise   ::X_N ].bind((*m.modem  )[mdm::sck::modulate   ::X_N2]);
        (*m.modem  )[mdm::sck::demodulate  ::Y_N1].bind((*m.channel)[chn::sck::add_noise  ::Y_N ]);
        (*m.decoder)[dec::sck::decode_siho ::Y_N ].bind((*m.modem  )[mdm::sck::demodulate ::Y_N2]);
        (*m.monitor)[mnt::sck::check_errors::U   ].bind((*m.encoder)[enc::sck::encode     ::U_K ]);
        (*m.monitor)[mnt::sck::check_errors::V   ].bind((*m.decoder)[dec::sck::decode_siho::V_K ]);

        // loop over the SNRs range
        for (auto ebn0 = p.ebn0_min; ebn0 < p.ebn0_max; ebn0 += p.ebn0_step)
        {
                // compute the current sigma for the channel noise
                const auto esn0  = tools::ebn0_to_esn0 (ebn0, p.R);
                const auto sigma = tools::esn0_to_sigma(esn0     );

#pragma omp single
                u.noise->set_values(sigma, ebn0, esn0);

                // update the sigma of the modem and the channel
                m.modem  ->set_noise(*u.noise);
                m.channel->set_noise(*u.noise);

#pragma omp single
                // display the performance (BER and FER) in real time (in a separate thread)
                u.terminal->start_temp_report();

                // run the simulation chain
                while (!u.monitor_red->is_done())
                {
                        (*m.source )[src::tsk::generate    ].exec();
                        (*m.encoder)[enc::tsk::encode      ].exec();
                        (*m.modem  )[mdm::tsk::modulate    ].exec();
                        (*m.channel)[chn::tsk::add_noise   ].exec();
                        (*m.modem  )[mdm::tsk::demodulate  ].exec();
                        (*m.decoder)[dec::tsk::decode_siho ].exec();
                        (*m.monitor)[mnt::tsk::check_errors].exec();
                }

// need to wait all the threads here before to reset the 'monitors' and 'terminal' states
#pragma omp barrier
#pragma omp single
{
                // final reduction
                u.monitor_red->reduce();

                // display the performance (BER and FER) in the terminal
                u.terminal->final_report();

                // reset the monitor and the terminal for the next SNR
                u.monitor_red->reset();
                u.terminal->reset();
}
        }

#pragma omp single
{
        // display the statistics of the tasks (if enabled)
        tools::Stats::show(u.modules_stats, true);
}
}
        return 0;
}

Listing 4.15 depicts how to use OpenMP pragmas to parallelize the whole communication chain. As a remainder:

  • #pragma omp parallel: all the code after in the braces is executed by all the threads,
  • #pragma omp barrier: all the threads wait all the others at this point,
  • #pragma omp single: only one thread executes the code below (there is an implicit barrier at the end of the single zone).

In this example, a params3 and an utils4 structure are allocated in p and u respectively, before the parallel region (lines 3-4). As a consequence, p and u are shared among all the threads. On the contrary, a modules4 structure is allocated in m inside the parallel region, thus each threads gets its own local m.

Listing 4.16 OpenMP: modules and utils
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
struct modules4
{
        std::unique_ptr<module::Source<>>       source;
        std::unique_ptr<tools ::Codec_SIHO<>>   codec;
        std::unique_ptr<module::Modem<>>        modem;
        std::unique_ptr<module::Channel<>>      channel;
                        module::Monitor_BFER<>* monitor;
                        module::Encoder<>*      encoder;
                        module::Decoder_SIHO<>* decoder;
};

struct utils4
{
        std::unique_ptr<tools::Sigma<>>                      noise;         // a sigma noise type
        std::vector<std::unique_ptr<tools::Reporter>>        reporters;     // list of reporters displayed in the terminal
        std::unique_ptr<tools::Terminal>                     terminal;      // manage the output text in the terminal
        std::vector<std::unique_ptr<module::Monitor_BFER<>>> monitors;      // list of the monitors from all the threads
        std::unique_ptr<tools::Monitor_BFER_reduction>       monitor_red;   // main monitor object that reduce all the thread monitors
        std::vector<std::vector<const module::Module*>>      modules;       // list of the allocated modules
        std::vector<std::vector<const module::Module*>>      modules_stats; // list of the allocated modules reorganized for the statistics
};

void init_modules_and_utils4(const params3 &p, modules4 &m, utils4 &u)
{
        // get the thread id from OpenMP
        const int tid = omp_get_thread_num();

        // set different seeds for different threads when the module use a PRNG
        p.source->seed += tid;
        p.channel->seed += tid;

        m.source        = std::unique_ptr<module::Source      <>>(p.source ->build());
        m.codec         = std::unique_ptr<tools ::Codec_SIHO  <>>(p.codec  ->build());
        m.modem         = std::unique_ptr<module::Modem       <>>(p.modem  ->build());
        m.channel       = std::unique_ptr<module::Channel     <>>(p.channel->build());
        u.monitors[tid] = std::unique_ptr<module::Monitor_BFER<>>(p.monitor->build());
        m.monitor       = u.monitors[tid].get();
        m.encoder       = m.codec->get_encoder().get();
        m.decoder       = m.codec->get_decoder_siho().get();

        // configuration of the module tasks
        std::vector<const module::Module*> modules_list = { m.source.get(), m.modem.get(), m.channel.get(), m.monitor, m.encoder, m.decoder };
        for (auto& mod : modules_list)
                for (auto& tsk : mod->tasks)
                {
                        tsk->set_autoalloc  (true ); // enable the automatic allocation of the data in the tasks
                        tsk->set_debug      (false); // disable the debug mode
                        tsk->set_debug_limit(16   ); // display only the 16 first bits if the debug mode is enabled
                        tsk->set_stats      (true ); // enable the statistics

                        // enable the fast mode (= disable the useless verifs in the tasks) if there is no debug and stats modes
                        if (!tsk->is_debug() && !tsk->is_stats())
                                tsk->set_fast(true);
                }

        u.modules[tid] = modules_list;
}

In Listing 4.16, there is a change in the modules4 structure compared to the modules3 structure (Listing 4.11): at line 7 the monitor is not allocated in this structure anymore, thus a standard pointer is used instead of a smart pointer. The monitor is now allocated in the utils4 structure at line 17, because all the monitors from all the threads have to be passed to build a common aggregated monitor for all of them: the monitor_red at line 18. monitor_red is able to perform the reduction of all the per-thread monitors. In the example, the monitor_red is the only member from u called by all the threads, to check whether the simulation has to continue or not (see line 54 in the main function, Listing 4.15).

In the init_modules_and_utils4 function, lines 25-30, a different seed is assigned to the modules using a PRNG. It is important to give a distinct seed to each thread. If the seed is the same for all threads, they all simulate the same frame contents and apply the same noise over it.

Lines 36-37, the monitors are allocated in u and the resulting pointer is assigned to m. At line 56 a list of the modules is stored in u.

Listing 4.17 OpenMP: utils
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
void init_utils4(const params3 &p, utils4 &u)
{
        // allocate a common monitor module to reduce all the monitors
        u.monitor_red = std::unique_ptr<tools::Monitor_BFER_reduction>(new tools::Monitor_BFER_reduction(u.monitors));
        u.monitor_red->set_reduce_frequency(std::chrono::milliseconds(500));
        // create a sigma noise type
        u.noise = std::unique_ptr<tools::Sigma<>>(new tools::Sigma<>());
        // report noise values (Es/N0 and Eb/N0)
        u.reporters.push_back(std::unique_ptr<tools::Reporter>(new tools::Reporter_noise<>(*u.noise)));
        // report bit/frame error rates
        u.reporters.push_back(std::unique_ptr<tools::Reporter>(new tools::Reporter_BFER<>(*u.monitor_red)));
        // report simulation throughputs
        u.reporters.push_back(std::unique_ptr<tools::Reporter>(new tools::Reporter_throughput<>(*u.monitor_red)));
        // create a terminal that will display the collected data from the reporters
        u.terminal = std::unique_ptr<tools::Terminal>(p.terminal->build(u.reporters));

        u.modules_stats.resize(u.modules[0].size());
        for (size_t m = 0; m < u.modules[0].size(); m++)
                for (size_t t = 0; t < u.modules.size(); t++)
                        u.modules_stats[m].push_back(u.modules[t][m]);
}

In Listing 4.17, the init_utils4 function allocates and configure the monitor_red at lines 3-5. Note that the allocation of monitor_red is possible because the monitors have been allocated previously in the init_modules_and_utils4 function (Listing 4.16).

Lines 17-20, the u.modules list is reordered in the u.modules_stats to be used for the statistics of the tasks in the main function (Listing 4.15 line 84). In the u.modules list the first dimension is the number of threads and the second is the number of different modules while in u.modules_stats the two dimension are switched.