ldpc_toolbox/simulation/
ber.rs

1//! BER simulation
2//!
3//! This module contains utilities for BER simulation.
4
5use super::{
6    channel::{AwgnChannel, Channel},
7    factory::Ber,
8    interleaving::Interleaver,
9    modulation::{Demodulator, Modulation, Modulator},
10    puncturing::Puncturer,
11};
12use crate::{
13    decoder::{
14        LdpcDecoder,
15        factory::{DecoderFactory, DecoderImplementation},
16    },
17    encoder::{Encoder, Error},
18    gf2::GF2,
19    sparse::SparseMatrix,
20};
21use ndarray::Array1;
22use num_traits::{One, Zero};
23use rand::{Rng, distr::StandardUniform};
24use std::{
25    sync::mpsc::{self, Receiver, Sender, SyncSender, TryRecvError},
26    time::{Duration, Instant},
27};
28
29/// BER test.
30///
31/// This struct is used to configure and run a BER test.
32#[derive(Debug)]
33pub struct BerTest<Mod: Modulation, Dec = DecoderImplementation> {
34    decoder_implementation: Dec,
35    h: SparseMatrix,
36    num_workers: usize,
37    k: usize,
38    n: usize,
39    n_cw: usize,
40    rate: f64,
41    encoder: Encoder,
42    puncturer: Option<Puncturer>,
43    interleaver: Option<Interleaver>,
44    modulator: Mod::Modulator,
45    ebn0s_db: Vec<f32>,
46    statistics: Vec<Statistics>,
47    bch_max_errors: u64,
48    max_iterations: usize,
49    max_frame_errors: u64,
50    reporter: Option<Reporter>,
51    last_reported: Instant,
52}
53
54#[derive(Debug)]
55struct Worker<Mod: Modulation> {
56    terminate_rx: Receiver<()>,
57    results_tx: Sender<WorkerResult>,
58    k: usize,
59    encoder: Encoder,
60    puncturer: Option<Puncturer>,
61    interleaver: Option<Interleaver>,
62    modulator: Mod::Modulator,
63    channel: AwgnChannel,
64    demodulator: Mod::Demodulator,
65    decoder: Box<dyn LdpcDecoder>,
66    max_iterations: usize,
67}
68
69#[derive(Debug, Clone)]
70struct WorkerResultOk {
71    bit_errors: u64,
72    frame_error: bool,
73    false_decode: bool,
74    iterations: u64,
75}
76
77type WorkerResult = Result<WorkerResultOk, ()>;
78
79#[derive(Debug, Clone, PartialEq)]
80struct CurrentStatistics {
81    num_frames: u64,
82    false_decodes: u64,
83    total_iterations: u64,
84    start: Instant,
85    ldpc: CurrentCodeStatistics,
86    bch: Option<CurrentCodeStatistics>,
87}
88
89#[derive(Debug, Clone, PartialEq)]
90struct CurrentCodeStatistics {
91    bit_errors: u64,
92    frame_errors: u64,
93    correct_iterations: u64,
94}
95
96/// BER test statistics.
97///
98/// This structure contains the statistics for a single Eb/N0 case in a BER
99/// test.
100#[derive(Debug, Clone, PartialEq)]
101pub struct Statistics {
102    /// Eb/N0 in dB units.
103    pub ebn0_db: f32,
104    /// Number of frames tested.
105    pub num_frames: u64,
106    /// Total number of iterations.
107    pub total_iterations: u64,
108    /// Number of frames falsely decoded.
109    ///
110    /// These are frames for which the decoder has converged to a valid
111    /// codeword, but the codeword is different from the transmitted codeword.
112    pub false_decodes: u64,
113    /// Average iterations per frame.
114    pub average_iterations: f64,
115    /// Elapsed time for this test case.
116    pub elapsed: Duration,
117    /// Throughput in Mbps (referred to information bits).
118    pub throughput_mbps: f64,
119    /// Statistics of the inner LDPC decoder.
120    pub ldpc: CodeStatistics,
121    /// Statistics of the combined inner LDPC decoder plus outer BCH decoder (if it exists).
122    pub bch: Option<CodeStatistics>,
123}
124
125/// BER test statistics for a particular code.
126///
127/// This is a substructure of [`Statistics`] that includes all the elements that
128/// depend on whether we are using only the inner LDPC decoder or LDPC plus
129/// BCH. `Statistics` has two instances of this structure: one for LDPC-only and
130/// another for LDPC plus BCH (which is only present when BCH is enabled).
131#[derive(Debug, Clone, PartialEq)]
132pub struct CodeStatistics {
133    /// Number of bit errors.
134    pub bit_errors: u64,
135    /// Number of frame errors.
136    pub frame_errors: u64,
137    /// Sum of iterations in correct frames.
138    pub correct_iterations: u64,
139    /// Bit error rate.
140    pub ber: f64,
141    /// Frame error rate.
142    pub fer: f64,
143    /// Average iterations per correct frame.
144    pub average_iterations_correct: f64,
145}
146
147/// Progress reporter.
148///
149/// A reporter can optionally be supplied to the BER test on contruction in
150/// order to receive periodic messages reporting the test progress.
151#[derive(Debug, Clone)]
152pub struct Reporter {
153    /// Sender element of a channel used to send the reports.
154    pub tx: Sender<Report>,
155    /// Reporting interval.
156    pub interval: Duration,
157}
158
159/// BER test progress report.
160///
161/// Progress reports are optionally sent out periodically by the BER test. These
162/// can be used to update a UI to show the progress.
163#[derive(Debug, Clone, PartialEq)]
164pub enum Report {
165    /// Statistics for the current Eb/N0 being tested.
166    ///
167    /// This is sent periodically, and also when the Eb/N0 is finished.
168    Statistics(Statistics),
169    /// The complete BER test has finished.
170    ///
171    /// This is sent when all the Eb/N0 cases have been done.
172    Finished,
173}
174
175macro_rules! report {
176    ($self:expr, $current_statistics:expr, $ebn0_db:expr, $final:expr) => {
177        if let Some(reporter) = $self.reporter.as_ref() {
178            let now = Instant::now();
179            if $final || $self.last_reported + reporter.interval < now {
180                reporter
181                    .tx
182                    .send(Report::Statistics(Statistics::from_current(
183                        &$current_statistics,
184                        $ebn0_db,
185                        $self.k,
186                    )))
187                    .unwrap();
188                $self.last_reported = now;
189            }
190        }
191    };
192}
193
194impl<Mod: Modulation, Dec: DecoderFactory> BerTest<Mod, Dec> {
195    /// Creates a new BER test.
196    ///
197    /// The parameters required to define the test are the parity check matrix
198    /// `h`, an optional puncturing pattern (which uses the semantics of
199    /// [`Puncturer`]), an optional interleaving pattern, the maximum number of
200    /// frame errors at which to stop the simulation for each Eb/N0, the maximum
201    /// number of iterations of the LDPC decoder, a list of Eb/N0's in dB units,
202    /// and an optional [`Reporter`] to send messages about the test progress.
203    ///
204    /// This function only defines the BER test. To run it it is necessary to
205    /// call the [`BerTest::run`] method.
206    #[allow(clippy::too_many_arguments)]
207    pub fn new(
208        h: SparseMatrix,
209        decoder_implementation: Dec,
210        puncturing_pattern: Option<&[bool]>,
211        interleaving_columns: Option<isize>,
212        max_frame_errors: u64,
213        max_iterations: usize,
214        ebn0s_db: &[f32],
215        reporter: Option<Reporter>,
216        bch_max_errors: u64,
217        num_workers: usize,
218    ) -> Result<BerTest<Mod, Dec>, Error> {
219        let k = h.num_cols() - h.num_rows();
220        let n_cw = h.num_cols();
221        let puncturer = puncturing_pattern.map(Puncturer::new);
222        let interleaver = interleaving_columns.map(|n| Interleaver::new(n.unsigned_abs(), n < 0));
223        let puncturer_rate = if let Some(p) = puncturer.as_ref() {
224            p.rate()
225        } else {
226            1.0
227        };
228        let n = (n_cw as f64 / puncturer_rate).round() as usize;
229        let rate = k as f64 / n as f64;
230        Ok(BerTest {
231            decoder_implementation,
232            num_workers,
233            k,
234            n,
235            n_cw,
236            rate,
237            encoder: Encoder::from_h(&h)?,
238            h,
239            puncturer,
240            interleaver,
241            modulator: Mod::Modulator::default(),
242            ebn0s_db: ebn0s_db.to_owned(),
243            statistics: Vec::with_capacity(ebn0s_db.len()),
244            bch_max_errors,
245            max_iterations,
246            max_frame_errors,
247            reporter,
248            last_reported: Instant::now(),
249        })
250    }
251
252    /// Runs the BER test.
253    ///
254    /// This function runs the BER test until completion. It returns a list of
255    /// statistics for each Eb/N0, or an error.
256    pub fn run(mut self) -> Result<Vec<Statistics>, Box<dyn std::error::Error>> {
257        let ret = self.do_run();
258        if let Some(reporter) = self.reporter.as_ref() {
259            reporter.tx.send(Report::Finished).unwrap();
260        }
261        ret?;
262        Ok(self.statistics)
263    }
264
265    fn do_run(&mut self) -> Result<(), Box<dyn std::error::Error>> {
266        self.last_reported = Instant::now();
267        for &ebn0_db in &self.ebn0s_db {
268            let ebn0 = 10.0_f64.powf(0.1 * f64::from(ebn0_db));
269            let esn0 = self.rate * Mod::BITS_PER_SYMBOL * ebn0;
270            let noise_sigma = (0.5 / esn0).sqrt();
271            let (results_tx, results_rx) = mpsc::channel();
272            let workers = std::iter::repeat_with(|| {
273                let (mut worker, terminate_tx) = self.make_worker(noise_sigma, results_tx.clone());
274                let handle = std::thread::spawn(move || worker.work());
275                (handle, terminate_tx)
276            })
277            .take(self.num_workers)
278            .collect::<Vec<_>>();
279
280            let mut current_statistics = CurrentStatistics::new(self.bch_max_errors > 0);
281            while current_statistics.errors_for_termination() < self.max_frame_errors {
282                match results_rx.recv().unwrap() {
283                    Ok(result) => {
284                        current_statistics.ldpc.bit_errors += result.bit_errors;
285                        current_statistics.ldpc.frame_errors += u64::from(result.frame_error);
286                        current_statistics.false_decodes += u64::from(result.false_decode);
287                        current_statistics.total_iterations += result.iterations;
288                        if !result.frame_error {
289                            current_statistics.ldpc.correct_iterations += result.iterations;
290                        }
291                        current_statistics.num_frames += 1;
292                        if let Some(bch) = &mut current_statistics.bch {
293                            if result.bit_errors > self.bch_max_errors {
294                                // BCH cannot decode codeword
295                                bch.bit_errors += result.bit_errors;
296                                bch.frame_errors += 1;
297                            } else {
298                                // BCH can decode codeword
299                                bch.correct_iterations += result.iterations;
300                            }
301                        }
302                    }
303                    Err(()) => break,
304                }
305                report!(self, current_statistics, ebn0_db, false);
306            }
307            report!(self, current_statistics, ebn0_db, true);
308
309            for (_, terminate_tx) in workers.iter() {
310                // we don't care if this fails because the worker has terminated
311                // and dropped the channel.
312                let _ = terminate_tx.send(());
313            }
314
315            let mut join_error = None;
316            for (handle, _) in workers.into_iter() {
317                if let Err(e) = handle.join().unwrap() {
318                    join_error = Some(e);
319                }
320            }
321            if let Some(e) = join_error {
322                return Err(e);
323            }
324
325            self.statistics.push(Statistics::from_current(
326                &current_statistics,
327                ebn0_db,
328                self.k,
329            ));
330        }
331        Ok(())
332    }
333
334    fn make_worker(
335        &self,
336        noise_sigma: f64,
337        results_tx: Sender<WorkerResult>,
338    ) -> (Worker<Mod>, SyncSender<()>) {
339        let (terminate_tx, terminate_rx) = mpsc::sync_channel(1);
340        (
341            Worker {
342                terminate_rx,
343                results_tx,
344                k: self.k,
345                encoder: self.encoder.clone(),
346                puncturer: self.puncturer.clone(),
347                interleaver: self.interleaver.clone(),
348                modulator: self.modulator.clone(),
349                channel: AwgnChannel::new(noise_sigma),
350                demodulator: Mod::Demodulator::from_noise_sigma(noise_sigma),
351                decoder: self.decoder_implementation.build_decoder(self.h.clone()),
352                max_iterations: self.max_iterations,
353            },
354            terminate_tx,
355        )
356    }
357}
358
359impl<Mod: Modulation, Dec: DecoderFactory> Ber for BerTest<Mod, Dec> {
360    fn run(self: Box<Self>) -> Result<Vec<Statistics>, Box<dyn std::error::Error>> {
361        BerTest::run(*self)
362    }
363
364    fn n(&self) -> usize {
365        self.n
366    }
367
368    fn n_cw(&self) -> usize {
369        self.n_cw
370    }
371
372    fn k(&self) -> usize {
373        self.k
374    }
375
376    fn rate(&self) -> f64 {
377        self.rate
378    }
379}
380
381impl<Mod: Modulation> Worker<Mod> {
382    fn work(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
383        let mut rng = rand::rng();
384        loop {
385            match self.terminate_rx.try_recv() {
386                Ok(()) => return Ok(()),
387                Err(TryRecvError::Disconnected) => panic!(),
388                Err(TryRecvError::Empty) => (),
389            };
390            let result = self.simulate(&mut rng);
391            let to_send = match result.as_ref() {
392                Ok(r) => Ok(r.clone()),
393                Err(_) => Err(()),
394            };
395            self.results_tx.send(to_send).unwrap();
396            result?;
397        }
398    }
399
400    fn simulate<R: Rng>(
401        &mut self,
402        rng: &mut R,
403    ) -> Result<WorkerResultOk, Box<dyn std::error::Error + Send + Sync + 'static>> {
404        let message = Self::random_message(rng, self.k);
405        let codeword = self.encoder.encode(&Self::gf2_array(&message));
406        let transmitted = match self.puncturer.as_ref() {
407            Some(p) => p.puncture(&codeword)?,
408            None => codeword,
409        };
410        let transmitted = match self.interleaver.as_ref() {
411            Some(i) => i.interleave(&transmitted),
412            None => transmitted,
413        };
414        let mut symbols = self.modulator.modulate(&transmitted);
415        self.channel.add_noise(rng, &mut symbols);
416        let llrs_demod = self.demodulator.demodulate(&symbols);
417        let llrs_decoder = match self.interleaver.as_ref() {
418            Some(i) => i.deinterleave(&llrs_demod),
419            None => llrs_demod,
420        };
421        let llrs_decoder = match self.puncturer.as_ref() {
422            Some(p) => p.depuncture(&llrs_decoder)?,
423            None => llrs_decoder,
424        };
425
426        let (decoded, iterations, success) =
427            match self.decoder.decode(&llrs_decoder, self.max_iterations) {
428                Ok(output) => (output.codeword, output.iterations, true),
429                Err(output) => (output.codeword, output.iterations, false),
430            };
431        // Count only bit errors in the systematic part of the codeword
432        let bit_errors = message
433            .iter()
434            .zip(decoded.iter())
435            .filter(|(a, b)| a != b)
436            .count() as u64;
437        let frame_error = bit_errors > 0;
438        let false_decode = frame_error && success;
439        Ok(WorkerResultOk {
440            bit_errors,
441            frame_error,
442            false_decode,
443            iterations: iterations as u64,
444        })
445    }
446
447    fn random_message<R: Rng>(rng: &mut R, size: usize) -> Vec<u8> {
448        rng.sample_iter(StandardUniform)
449            .map(<u8 as From<bool>>::from)
450            .take(size)
451            .collect()
452    }
453
454    fn gf2_array(bits: &[u8]) -> Array1<GF2> {
455        Array1::from_iter(
456            bits.iter()
457                .map(|&b| if b == 1 { GF2::one() } else { GF2::zero() }),
458        )
459    }
460}
461
462impl CurrentStatistics {
463    fn new(has_bch: bool) -> CurrentStatistics {
464        CurrentStatistics {
465            num_frames: 0,
466            false_decodes: 0,
467            total_iterations: 0,
468            start: Instant::now(),
469            ldpc: CurrentCodeStatistics::new(),
470            bch: if has_bch {
471                Some(CurrentCodeStatistics::new())
472            } else {
473                None
474            },
475        }
476    }
477
478    fn errors_for_termination(&self) -> u64 {
479        if let Some(bch) = &self.bch {
480            bch.frame_errors
481        } else {
482            self.ldpc.frame_errors
483        }
484    }
485}
486
487impl CurrentCodeStatistics {
488    fn new() -> CurrentCodeStatistics {
489        CurrentCodeStatistics {
490            bit_errors: 0,
491            frame_errors: 0,
492            correct_iterations: 0,
493        }
494    }
495}
496
497impl Default for CurrentCodeStatistics {
498    fn default() -> CurrentCodeStatistics {
499        CurrentCodeStatistics::new()
500    }
501}
502
503impl CodeStatistics {
504    fn from_current(stats: &CurrentCodeStatistics, num_frames: u64, k: usize) -> CodeStatistics {
505        CodeStatistics {
506            bit_errors: stats.bit_errors,
507            frame_errors: stats.frame_errors,
508            correct_iterations: stats.correct_iterations,
509            ber: stats.bit_errors as f64 / (k as f64 * num_frames as f64),
510            fer: stats.frame_errors as f64 / num_frames as f64,
511            average_iterations_correct: stats.correct_iterations as f64
512                / (num_frames - stats.frame_errors) as f64,
513        }
514    }
515}
516
517impl Statistics {
518    fn from_current(stats: &CurrentStatistics, ebn0_db: f32, k: usize) -> Statistics {
519        let elapsed = Instant::now() - stats.start;
520        Statistics {
521            ebn0_db,
522            num_frames: stats.num_frames,
523            false_decodes: stats.false_decodes,
524            total_iterations: stats.total_iterations,
525            average_iterations: stats.total_iterations as f64 / stats.num_frames as f64,
526            elapsed,
527            throughput_mbps: 1e-6 * (k as f64 * stats.num_frames as f64) / elapsed.as_secs_f64(),
528            ldpc: CodeStatistics::from_current(&stats.ldpc, stats.num_frames, k),
529            bch: stats
530                .bch
531                .as_ref()
532                .map(|b| CodeStatistics::from_current(b, stats.num_frames, k)),
533        }
534    }
535}