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#include <aff3ct.hpp>
 2using namespace aff3ct;
 3
 4int main(int argc, char** argv)
 5{
 6        params1 p;  init_params1 (p   ); // create and initialize the parameters defined by the user
 7        modules1 m; init_modules1(p, m); // create and initialize modules
 8        buffers1 b; init_buffers1(p, b); // create and initialize the buffers required by the modules
 9        utils1 u;   init_utils1  (m, u); // create and initialize utils
10
11        // display the legend in the terminal
12        u.terminal->legend();
13
14        // loop over SNRs range
15        for (auto ebn0 = p.ebn0_min; ebn0 < p.ebn0_max; ebn0 += p.ebn0_step)
16        {
17                // compute the current sigma for the channel noise
18                const auto esn0  = tools::ebn0_to_esn0 (ebn0, p.R);
19                const auto sigma = tools::esn0_to_sigma(esn0     );
20
21                u.noise->set_values(sigma, ebn0, esn0);
22
23                // update the sigma of the modem and the channel
24                m.modem  ->set_noise(*u.noise);
25                m.channel->set_noise(*u.noise);
26
27                // display the performance (BER and FER) in real time (in a separate thread)
28                u.terminal->start_temp_report();
29
30                // run the simulation chain
31                while (!m.monitor->fe_limit_achieved())
32                {
33                        m.source ->generate    (                 b.ref_bits     );
34                        m.encoder->encode      (b.ref_bits,      b.enc_bits     );
35                        m.modem  ->modulate    (b.enc_bits,      b.symbols      );
36                        m.channel->add_noise   (b.symbols,       b.noisy_symbols);
37                        m.modem  ->demodulate  (b.noisy_symbols, b.LLRs         );
38                        m.decoder->decode_siho (b.LLRs,          b.dec_bits     );
39                        m.monitor->check_errors(b.dec_bits,      b.ref_bits     );
40                }
41
42                // display the performance (BER and FER) in the terminal
43                u.terminal->final_report();
44
45                // reset the monitor for the next SNR
46                m.monitor->reset();
47                u.terminal->reset();
48        }
49
50        return 0;
51}

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
 1struct params1
 2{
 3        int   K         =  32;     // number of information bits
 4        int   N         = 128;     // codeword size
 5        int   fe        = 100;     // number of frame errors
 6        int   seed      =   0;     // PRNG seed for the AWGN channel
 7        float ebn0_min  =   0.00f; // minimum SNR value
 8        float ebn0_max  =  10.01f; // maximum SNR value
 9        float ebn0_step =   1.00f; // SNR step
10        float R;                   // code rate (R=K/N)
11};
12
13void init_params1(params1 &p)
14{
15        p.R = (float)p.K / (float)p.N;
16}

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
 1struct modules1
 2{
 3        std::unique_ptr<module::Source_random<>>          source;
 4        std::unique_ptr<module::Encoder_repetition_sys<>> encoder;
 5        std::unique_ptr<module::Modem_BPSK<>>             modem;
 6        std::unique_ptr<module::Channel_AWGN_LLR<>>       channel;
 7        std::unique_ptr<module::Decoder_repetition_std<>> decoder;
 8        std::unique_ptr<module::Monitor_BFER<>>           monitor;
 9};
10
11void init_modules1(const params1 &p, modules1 &m)
12{
13        m.source  = std::unique_ptr<module::Source_random         <>>(new module::Source_random         <>(p.K        ));
14        m.encoder = std::unique_ptr<module::Encoder_repetition_sys<>>(new module::Encoder_repetition_sys<>(p.K, p.N   ));
15        m.modem   = std::unique_ptr<module::Modem_BPSK            <>>(new module::Modem_BPSK            <>(p.N        ));
16        m.channel = std::unique_ptr<module::Channel_AWGN_LLR      <>>(new module::Channel_AWGN_LLR      <>(p.N, p.seed));
17        m.decoder = std::unique_ptr<module::Decoder_repetition_std<>>(new module::Decoder_repetition_std<>(p.K, p.N   ));
18        m.monitor = std::unique_ptr<module::Monitor_BFER          <>>(new module::Monitor_BFER          <>(p.K, p.fe  ));
19};

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
 1struct buffers1
 2{
 3        std::vector<int  > ref_bits;
 4        std::vector<int  > enc_bits;
 5        std::vector<float> symbols;
 6        std::vector<float> noisy_symbols;
 7        std::vector<float> LLRs;
 8        std::vector<int  > dec_bits;
 9};
10
11void init_buffers1(const params1 &p, buffers1 &b)
12{
13        b.ref_bits      = std::vector<int  >(p.K);
14        b.enc_bits      = std::vector<int  >(p.N);
15        b.symbols       = std::vector<float>(p.N);
16        b.noisy_symbols = std::vector<float>(p.N);
17        b.LLRs          = std::vector<float>(p.N);
18        b.dec_bits      = std::vector<int  >(p.K);
19}

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
 1struct utils1
 2{
 3        std::unique_ptr<tools::Sigma<>>               noise;     // a sigma noise type
 4        std::vector<std::unique_ptr<tools::Reporter>> reporters; // list of reporters dispayed in the terminal
 5        std::unique_ptr<tools::Terminal_std>          terminal;  // manage the output text in the terminal
 6};
 7
 8void init_utils1(const modules1 &m, utils1 &u)
 9{
10        // create a sigma noise type
11        u.noise = std::unique_ptr<tools::Sigma<>>(new tools::Sigma<>());
12        // report the noise values (Es/N0 and Eb/N0)
13        u.reporters.push_back(std::unique_ptr<tools::Reporter>(new tools::Reporter_noise<>(*u.noise)));
14        // report the bit/frame error rates
15        u.reporters.push_back(std::unique_ptr<tools::Reporter>(new tools::Reporter_BFER<>(*m.monitor)));
16        // report the simulation throughputs
17        u.reporters.push_back(std::unique_ptr<tools::Reporter>(new tools::Reporter_throughput<>(*m.monitor)));
18        // create a terminal that will display the collected data from the reporters
19        u.terminal = std::unique_ptr<tools::Terminal_std>(new tools::Terminal_std(u.reporters));
20}

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#include <aff3ct.hpp>
 2using namespace aff3ct;
 3
 4int main(int argc, char** argv)
 5{
 6        params1  p; init_params1 (p   ); // create and initialize the parameters defined by the user
 7        modules1 m; init_modules2(p, m); // create and initialize modules
 8        // the 'init_buffers1' function is not required anymore
 9        utils1   u; init_utils1  (m, u); // create and initialize the utils
10
11        // display the legend in the terminal
12        u.terminal->legend();
13
14        // 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)
15        using namespace module;
16        (*m.encoder)[enc::sck::encode      ::U_K ].bind((*m.source )[src::sck::generate   ::U_K ]);
17        (*m.modem  )[mdm::sck::modulate    ::X_N1].bind((*m.encoder)[enc::sck::encode     ::X_N ]);
18        (*m.channel)[chn::sck::add_noise   ::X_N ].bind((*m.modem  )[mdm::sck::modulate   ::X_N2]);
19        (*m.modem  )[mdm::sck::demodulate  ::Y_N1].bind((*m.channel)[chn::sck::add_noise  ::Y_N ]);
20        (*m.decoder)[dec::sck::decode_siho ::Y_N ].bind((*m.modem  )[mdm::sck::demodulate ::Y_N2]);
21        (*m.monitor)[mnt::sck::check_errors::U   ].bind((*m.encoder)[enc::sck::encode     ::U_K ]);
22        (*m.monitor)[mnt::sck::check_errors::V   ].bind((*m.decoder)[dec::sck::decode_siho::V_K ]);
23
24        // loop over the range of SNRs
25        for (auto ebn0 = p.ebn0_min; ebn0 < p.ebn0_max; ebn0 += p.ebn0_step)
26        {
27                // compute the current sigma for the channel noise
28                const auto esn0  = tools::ebn0_to_esn0 (ebn0, p.R);
29                const auto sigma = tools::esn0_to_sigma(esn0     );
30
31                u.noise->set_values(sigma, ebn0, esn0);
32
33                // update the sigma of the modem and the channel
34                m.modem  ->set_noise(*u.noise);
35                m.channel->set_noise(*u.noise);
36
37                // display the performance (BER and FER) in real time (in a separate thread)
38                u.terminal->start_temp_report();
39
40                // run the simulation chain
41                while (!m.monitor->fe_limit_achieved())
42                {
43                        (*m.source )[src::tsk::generate    ].exec();
44                        (*m.encoder)[enc::tsk::encode      ].exec();
45                        (*m.modem  )[mdm::tsk::modulate    ].exec();
46                        (*m.channel)[chn::tsk::add_noise   ].exec();
47                        (*m.modem  )[mdm::tsk::demodulate  ].exec();
48                        (*m.decoder)[dec::tsk::decode_siho ].exec();
49                        (*m.monitor)[mnt::tsk::check_errors].exec();
50                }
51
52                // display the performance (BER and FER) in the terminal
53                u.terminal->final_report();
54
55                // reset the monitor and the terminal for the next SNR
56                m.monitor->reset();
57                u.terminal->reset();
58        }
59
60        // display the statistics of the tasks (if enabled)
61        tools::Stats::show({ m.source.get(), m.encoder.get(), m.modem.get(), m.channel.get(), m.decoder.get(), m.monitor.get() }, true);
62
63        return 0;
64}

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
 1void init_modules2(const params1 &p, modules1 &m)
 2{
 3        m.source  = std::unique_ptr<module::Source_random         <>>(new module::Source_random         <>(p.K        ));
 4        m.encoder = std::unique_ptr<module::Encoder_repetition_sys<>>(new module::Encoder_repetition_sys<>(p.K, p.N   ));
 5        m.modem   = std::unique_ptr<module::Modem_BPSK            <>>(new module::Modem_BPSK            <>(p.N        ));
 6        m.channel = std::unique_ptr<module::Channel_AWGN_LLR      <>>(new module::Channel_AWGN_LLR      <>(p.N, p.seed));
 7        m.decoder = std::unique_ptr<module::Decoder_repetition_std<>>(new module::Decoder_repetition_std<>(p.K, p.N   ));
 8        m.monitor = std::unique_ptr<module::Monitor_BFER          <>>(new module::Monitor_BFER          <>(p.K, p.fe  ));
 9
10        // configuration of the module tasks
11        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() };
12        for (auto& mod : modules_list)
13                for (auto& tsk : mod->tasks)
14                {
15                        tsk->set_autoalloc  (true ); // enable the automatic allocation of data buffers in the tasks
16                        tsk->set_debug      (false); // disable the debug mode
17                        tsk->set_debug_limit(16   ); // display only the 16 first bits if the debug mode is enabled
18                        tsk->set_stats      (true ); // enable statistics collection
19
20                        // enable fast mode (= disable optional checks in the tasks) if there is no debug and stats modes
21                        if (!tsk->is_debug() && !tsk->is_stats())
22                                tsk->set_fast(true);
23                }
24}

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#include <aff3ct.hpp>
 2using namespace aff3ct;
 3
 4int main(int argc, char** argv)
 5{
 6        params3  p; init_params3 (argc, argv, p); // create and initialize the parameters from the command line with factories
 7        modules3 m; init_modules3(p, m         ); // create and initialize modules
 8        utils1   u; init_utils3  (p, m, u      ); // create and initialize utils
 9
10        // [...]
11
12        // display the statistics of the tasks (if enabled)
13        tools::Stats::show({ m.source.get(), m.modem.get(), m.channel.get(), m.monitor.get(), m.encoder, m.decoder }, true);
14
15        return 0;
16}

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

Listing 4.10 Factory: parameters
 1struct params3
 2{
 3        float ebn0_min  =  0.00f; // minimum SNR value
 4        float ebn0_max  = 10.01f; // maximum SNR value
 5        float ebn0_step =  1.00f; // SNR step
 6        float R;                  // code rate (R=K/N)
 7
 8        std::unique_ptr<factory::Source          > source;
 9        std::unique_ptr<factory::Codec_repetition> codec;
10        std::unique_ptr<factory::Modem           > modem;
11        std::unique_ptr<factory::Channel         > channel;
12        std::unique_ptr<factory::Monitor_BFER    > monitor;
13        std::unique_ptr<factory::Terminal        > terminal;
14};
15
16void init_params3(int argc, char** argv, params3 &p)
17{
18        p.source   = std::unique_ptr<factory::Source          >(new factory::Source          ());
19        p.codec    = std::unique_ptr<factory::Codec_repetition>(new factory::Codec_repetition());
20        p.modem    = std::unique_ptr<factory::Modem           >(new factory::Modem           ());
21        p.channel  = std::unique_ptr<factory::Channel         >(new factory::Channel         ());
22        p.monitor  = std::unique_ptr<factory::Monitor_BFER    >(new factory::Monitor_BFER    ());
23        p.terminal = std::unique_ptr<factory::Terminal        >(new factory::Terminal        ());
24
25        std::vector<factory::Factory*> params_list = { p.source .get(), p.codec  .get(), p.modem   .get(),
26                                                       p.channel.get(), p.monitor.get(), p.terminal.get() };
27
28        // parse command line arguments for the given parameters and fill them
29        tools::Command_parser cp(argc, argv, params_list, true);
30        if (cp.parsing_failed())
31        {
32                cp.print_help    ();
33                cp.print_warnings();
34                cp.print_errors  ();
35                std::exit(1);
36        }
37
38        std::cout << "# Simulation parameters: " << std::endl;
39        tools::Header::print_parameters(params_list); // display the headers (= print the AFF3CT parameters on the screen)
40        std::cout << "#" << std::endl;
41        cp.print_warnings();
42
43        p.R = (float)p.codec->enc->K / (float)p.codec->enc->N_cw; // compute the code rate
44}

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
 1struct modules3
 2{
 3        std::unique_ptr<module::Source<>>       source;
 4        std::unique_ptr<tools ::Codec_SIHO<>>   codec;
 5        std::unique_ptr<module::Modem<>>        modem;
 6        std::unique_ptr<module::Channel<>>      channel;
 7        std::unique_ptr<module::Monitor_BFER<>> monitor;
 8                        module::Encoder<>*      encoder;
 9                        module::Decoder_SIHO<>* decoder;
10};
11
12void init_modules3(const params3 &p, modules3 &m)
13{
14        m.source  = std::unique_ptr<module::Source      <>>(p.source ->build());
15        m.codec   = std::unique_ptr<tools ::Codec_SIHO  <>>(p.codec  ->build());
16        m.modem   = std::unique_ptr<module::Modem       <>>(p.modem  ->build());
17        m.channel = std::unique_ptr<module::Channel     <>>(p.channel->build());
18        m.monitor = std::unique_ptr<module::Monitor_BFER<>>(p.monitor->build());
19        m.encoder = m.codec->get_encoder().get();
20        m.decoder = m.codec->get_decoder_siho().get();
21
22        // configuration of the module tasks
23        std::vector<const module::Module*> modules_list = { m.source.get(), m.modem.get(), m.channel.get(), m.monitor.get(), m.encoder, m.decoder };
24        for (auto& mod : modules_list)
25                for (auto& tsk : mod->tasks)
26                {
27                        tsk->set_autoalloc  (true ); // enable the automatic allocation of the data in the tasks
28                        tsk->set_debug      (false); // disable the debug mode
29                        tsk->set_debug_limit(16   ); // display only the 16 first bits if the debug mode is enabled
30                        tsk->set_stats      (true ); // enable the statistics
31
32                        // enable the fast mode (= disable the useless verifs in the tasks) if there is no debug and stats modes
33                        if (!tsk->is_debug() && !tsk->is_stats())
34                                tsk->set_fast(true);
35                }
36}

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

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
 1int main(int argc, char** argv)
 2{
 3        params3 p; init_params3(argc, argv, p); // create and initialize the parameters from the command line with factories
 4        utils4 u; // create an 'utils4' structure
 5
 6#pragma omp parallel
 7{
 8#pragma omp single
 9{
10        // get the number of available threads from OpenMP
11        const size_t n_threads = (size_t)omp_get_num_threads();
12        u.monitors.resize(n_threads);
13        u.modules .resize(n_threads);
14}
15        modules4 m; init_modules_and_utils4(p, m, u); // create and initialize the modules and initialize a part of the utils
16
17#pragma omp barrier
18#pragma omp single
19{
20        init_utils4(p, u); // finalize the utils initialization
21
22        // display the legend in the terminal
23        u.terminal->legend();
24}
25        // sockets binding (connect the sockets of the tasks = fill the input sockets with the output sockets)
26        using namespace module;
27        (*m.encoder)[enc::sck::encode      ::U_K ].bind((*m.source )[src::sck::generate   ::U_K ]);
28        (*m.modem  )[mdm::sck::modulate    ::X_N1].bind((*m.encoder)[enc::sck::encode     ::X_N ]);
29        (*m.channel)[chn::sck::add_noise   ::X_N ].bind((*m.modem  )[mdm::sck::modulate   ::X_N2]);
30        (*m.modem  )[mdm::sck::demodulate  ::Y_N1].bind((*m.channel)[chn::sck::add_noise  ::Y_N ]);
31        (*m.decoder)[dec::sck::decode_siho ::Y_N ].bind((*m.modem  )[mdm::sck::demodulate ::Y_N2]);
32        (*m.monitor)[mnt::sck::check_errors::U   ].bind((*m.encoder)[enc::sck::encode     ::U_K ]);
33        (*m.monitor)[mnt::sck::check_errors::V   ].bind((*m.decoder)[dec::sck::decode_siho::V_K ]);
34
35        // loop over the SNRs range
36        for (auto ebn0 = p.ebn0_min; ebn0 < p.ebn0_max; ebn0 += p.ebn0_step)
37        {
38                // compute the current sigma for the channel noise
39                const auto esn0  = tools::ebn0_to_esn0 (ebn0, p.R);
40                const auto sigma = tools::esn0_to_sigma(esn0     );
41
42#pragma omp single
43                u.noise->set_values(sigma, ebn0, esn0);
44
45                // update the sigma of the modem and the channel
46                m.modem  ->set_noise(*u.noise);
47                m.channel->set_noise(*u.noise);
48
49#pragma omp single
50                // display the performance (BER and FER) in real time (in a separate thread)
51                u.terminal->start_temp_report();
52
53                // run the simulation chain
54                while (!u.monitor_red->is_done())
55                {
56                        (*m.source )[src::tsk::generate    ].exec();
57                        (*m.encoder)[enc::tsk::encode      ].exec();
58                        (*m.modem  )[mdm::tsk::modulate    ].exec();
59                        (*m.channel)[chn::tsk::add_noise   ].exec();
60                        (*m.modem  )[mdm::tsk::demodulate  ].exec();
61                        (*m.decoder)[dec::tsk::decode_siho ].exec();
62                        (*m.monitor)[mnt::tsk::check_errors].exec();
63                }
64
65// need to wait all the threads here before to reset the 'monitors' and 'terminal' states
66#pragma omp barrier
67#pragma omp single
68{
69                // final reduction
70                u.monitor_red->reduce();
71
72                // display the performance (BER and FER) in the terminal
73                u.terminal->final_report();
74
75                // reset the monitor and the terminal for the next SNR
76                u.monitor_red->reset();
77                u.terminal->reset();
78}
79        }
80
81#pragma omp single
82{
83        // display the statistics of the tasks (if enabled)
84        tools::Stats::show(u.modules_stats, true);
85}
86}
87        return 0;
88}

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
 1struct modules4
 2{
 3        std::unique_ptr<module::Source<>>       source;
 4        std::unique_ptr<tools ::Codec_SIHO<>>   codec;
 5        std::unique_ptr<module::Modem<>>        modem;
 6        std::unique_ptr<module::Channel<>>      channel;
 7                        module::Monitor_BFER<>* monitor;
 8                        module::Encoder<>*      encoder;
 9                        module::Decoder_SIHO<>* decoder;
10};
11
12struct utils4
13{
14        std::unique_ptr<tools::Sigma<>>                      noise;         // a sigma noise type
15        std::vector<std::unique_ptr<tools::Reporter>>        reporters;     // list of reporters displayed in the terminal
16        std::unique_ptr<tools::Terminal>                     terminal;      // manage the output text in the terminal
17        std::vector<std::unique_ptr<module::Monitor_BFER<>>> monitors;      // list of the monitors from all the threads
18        std::unique_ptr<tools::Monitor_BFER_reduction>       monitor_red;   // main monitor object that reduce all the thread monitors
19        std::vector<std::vector<const module::Module*>>      modules;       // list of the allocated modules
20        std::vector<std::vector<const module::Module*>>      modules_stats; // list of the allocated modules reorganized for the statistics
21};
22
23void init_modules_and_utils4(const params3 &p, modules4 &m, utils4 &u)
24{
25        // get the thread id from OpenMP
26        const int tid = omp_get_thread_num();
27
28        // set different seeds for different threads when the module use a PRNG
29        p.source->seed += tid;
30        p.channel->seed += tid;
31
32        m.source        = std::unique_ptr<module::Source      <>>(p.source ->build());
33        m.codec         = std::unique_ptr<tools ::Codec_SIHO  <>>(p.codec  ->build());
34        m.modem         = std::unique_ptr<module::Modem       <>>(p.modem  ->build());
35        m.channel       = std::unique_ptr<module::Channel     <>>(p.channel->build());
36        u.monitors[tid] = std::unique_ptr<module::Monitor_BFER<>>(p.monitor->build());
37        m.monitor       = u.monitors[tid].get();
38        m.encoder       = m.codec->get_encoder().get();
39        m.decoder       = m.codec->get_decoder_siho().get();
40
41        // configuration of the module tasks
42        std::vector<const module::Module*> modules_list = { m.source.get(), m.modem.get(), m.channel.get(), m.monitor, m.encoder, m.decoder };
43        for (auto& mod : modules_list)
44                for (auto& tsk : mod->tasks)
45                {
46                        tsk->set_autoalloc  (true ); // enable the automatic allocation of the data in the tasks
47                        tsk->set_debug      (false); // disable the debug mode
48                        tsk->set_debug_limit(16   ); // display only the 16 first bits if the debug mode is enabled
49                        tsk->set_stats      (true ); // enable the statistics
50
51                        // enable the fast mode (= disable the useless verifs in the tasks) if there is no debug and stats modes
52                        if (!tsk->is_debug() && !tsk->is_stats())
53                                tsk->set_fast(true);
54                }
55
56        u.modules[tid] = modules_list;
57}

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

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.