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.
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.
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).
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
.
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.
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.
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).
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.
# ---------------------||------------------------------------------------------||---------------------
# 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
Note
The full source code is available here: https://github.com/aff3ct/my_project_with_aff3ct/blob/master/examples/bootstrap/src/main.cpp.
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.
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).
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
.
Note
The full source code is available here: https://github.com/aff3ct/my_project_with_aff3ct/blob/master/examples/tasks/src/main.cpp.
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.
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.
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.
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
).
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.
./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.
./bin/my_project -h
Those parameters are documented in the Parameters section.
Note
The full source code is available here: https://github.com/aff3ct/my_project_with_aff3ct/blob/master/examples/factory/src/main.cpp.
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.
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 thesingle
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
.
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
.
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.
Note
The full source code is available here: https://github.com/aff3ct/my_project_with_aff3ct/blob/master/examples/openmp/src/main.cpp.