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.
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.
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).
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
.
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.
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.
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).
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.
# ---------------------||------------------------------------------------------||---------------------
# 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 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).
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
.
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 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.
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.
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
).
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.
./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.
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 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
.
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
.
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.
Note
The full source code is available here: https://github.com/aff3ct/my_project_with_aff3ct/blob/master/examples/openmp/src/main.cpp.