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    ) -> Result<BerTest<Mod, Dec>, Error> {
218        let k = h.num_cols() - h.num_rows();
219        let n_cw = h.num_cols();
220        let puncturer = puncturing_pattern.map(Puncturer::new);
221        let interleaver = interleaving_columns.map(|n| Interleaver::new(n.unsigned_abs(), n < 0));
222        let puncturer_rate = if let Some(p) = puncturer.as_ref() {
223            p.rate()
224        } else {
225            1.0
226        };
227        let n = (n_cw as f64 / puncturer_rate).round() as usize;
228        let rate = k as f64 / n as f64;
229        Ok(BerTest {
230            decoder_implementation,
231            num_workers: num_cpus::get(),
232            k,
233            n,
234            n_cw,
235            rate,
236            encoder: Encoder::from_h(&h)?,
237            h,
238            puncturer,
239            interleaver,
240            modulator: Mod::Modulator::default(),
241            ebn0s_db: ebn0s_db.to_owned(),
242            statistics: Vec::with_capacity(ebn0s_db.len()),
243            bch_max_errors,
244            max_iterations,
245            max_frame_errors,
246            reporter,
247            last_reported: Instant::now(),
248        })
249    }
250
251    /// Runs the BER test.
252    ///
253    /// This function runs the BER test until completion. It returns a list of
254    /// statistics for each Eb/N0, or an error.
255    pub fn run(mut self) -> Result<Vec<Statistics>, Box<dyn std::error::Error>> {
256        let ret = self.do_run();
257        if let Some(reporter) = self.reporter.as_ref() {
258            reporter.tx.send(Report::Finished).unwrap();
259        }
260        ret?;
261        Ok(self.statistics)
262    }
263
264    fn do_run(&mut self) -> Result<(), Box<dyn std::error::Error>> {
265        self.last_reported = Instant::now();
266        for &ebn0_db in &self.ebn0s_db {
267            let ebn0 = 10.0_f64.powf(0.1 * f64::from(ebn0_db));
268            let esn0 = self.rate * Mod::BITS_PER_SYMBOL * ebn0;
269            let noise_sigma = (0.5 / esn0).sqrt();
270            let (results_tx, results_rx) = mpsc::channel();
271            let workers = std::iter::repeat_with(|| {
272                let (mut worker, terminate_tx) = self.make_worker(noise_sigma, results_tx.clone());
273                let handle = std::thread::spawn(move || worker.work());
274                (handle, terminate_tx)
275            })
276            .take(self.num_workers)
277            .collect::<Vec<_>>();
278
279            let mut current_statistics = CurrentStatistics::new(self.bch_max_errors > 0);
280            while current_statistics.errors_for_termination() < self.max_frame_errors {
281                match results_rx.recv().unwrap() {
282                    Ok(result) => {
283                        current_statistics.ldpc.bit_errors += result.bit_errors;
284                        current_statistics.ldpc.frame_errors += u64::from(result.frame_error);
285                        current_statistics.false_decodes += u64::from(result.false_decode);
286                        current_statistics.total_iterations += result.iterations;
287                        if !result.frame_error {
288                            current_statistics.ldpc.correct_iterations += result.iterations;
289                        }
290                        current_statistics.num_frames += 1;
291                        if let Some(bch) = &mut current_statistics.bch {
292                            if result.bit_errors > self.bch_max_errors {
293                                // BCH cannot decode codeword
294                                bch.bit_errors += result.bit_errors;
295                                bch.frame_errors += 1;
296                            } else {
297                                // BCH can decode codeword
298                                bch.correct_iterations += result.iterations;
299                            }
300                        }
301                    }
302                    Err(()) => break,
303                }
304                report!(self, current_statistics, ebn0_db, false);
305            }
306            report!(self, current_statistics, ebn0_db, true);
307
308            for (_, terminate_tx) in workers.iter() {
309                // we don't care if this fails because the worker has terminated
310                // and dropped the channel.
311                let _ = terminate_tx.send(());
312            }
313
314            let mut join_error = None;
315            for (handle, _) in workers.into_iter() {
316                if let Err(e) = handle.join().unwrap() {
317                    join_error = Some(e);
318                }
319            }
320            if let Some(e) = join_error {
321                return Err(e);
322            }
323
324            self.statistics.push(Statistics::from_current(
325                &current_statistics,
326                ebn0_db,
327                self.k,
328            ));
329        }
330        Ok(())
331    }
332
333    fn make_worker(
334        &self,
335        noise_sigma: f64,
336        results_tx: Sender<WorkerResult>,
337    ) -> (Worker<Mod>, SyncSender<()>) {
338        let (terminate_tx, terminate_rx) = mpsc::sync_channel(1);
339        (
340            Worker {
341                terminate_rx,
342                results_tx,
343                k: self.k,
344                encoder: self.encoder.clone(),
345                puncturer: self.puncturer.clone(),
346                interleaver: self.interleaver.clone(),
347                modulator: self.modulator.clone(),
348                channel: AwgnChannel::new(noise_sigma),
349                demodulator: Mod::Demodulator::from_noise_sigma(noise_sigma),
350                decoder: self.decoder_implementation.build_decoder(self.h.clone()),
351                max_iterations: self.max_iterations,
352            },
353            terminate_tx,
354        )
355    }
356}
357
358impl<Mod: Modulation, Dec: DecoderFactory> Ber for BerTest<Mod, Dec> {
359    fn run(self: Box<Self>) -> Result<Vec<Statistics>, Box<dyn std::error::Error>> {
360        BerTest::run(*self)
361    }
362
363    fn n(&self) -> usize {
364        self.n
365    }
366
367    fn n_cw(&self) -> usize {
368        self.n_cw
369    }
370
371    fn k(&self) -> usize {
372        self.k
373    }
374
375    fn rate(&self) -> f64 {
376        self.rate
377    }
378}
379
380impl<Mod: Modulation> Worker<Mod> {
381    fn work(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
382        let mut rng = rand::rng();
383        loop {
384            match self.terminate_rx.try_recv() {
385                Ok(()) => return Ok(()),
386                Err(TryRecvError::Disconnected) => panic!(),
387                Err(TryRecvError::Empty) => (),
388            };
389            let result = self.simulate(&mut rng);
390            let to_send = match result.as_ref() {
391                Ok(r) => Ok(r.clone()),
392                Err(_) => Err(()),
393            };
394            self.results_tx.send(to_send).unwrap();
395            result?;
396        }
397    }
398
399    fn simulate<R: Rng>(
400        &mut self,
401        rng: &mut R,
402    ) -> Result<WorkerResultOk, Box<dyn std::error::Error + Send + Sync + 'static>> {
403        let message = Self::random_message(rng, self.k);
404        let codeword = self.encoder.encode(&Self::gf2_array(&message));
405        let transmitted = match self.puncturer.as_ref() {
406            Some(p) => p.puncture(&codeword)?,
407            None => codeword,
408        };
409        let transmitted = match self.interleaver.as_ref() {
410            Some(i) => i.interleave(&transmitted),
411            None => transmitted,
412        };
413        let mut symbols = self.modulator.modulate(&transmitted);
414        self.channel.add_noise(rng, &mut symbols);
415        let llrs_demod = self.demodulator.demodulate(&symbols);
416        let llrs_decoder = match self.interleaver.as_ref() {
417            Some(i) => i.deinterleave(&llrs_demod),
418            None => llrs_demod,
419        };
420        let llrs_decoder = match self.puncturer.as_ref() {
421            Some(p) => p.depuncture(&llrs_decoder)?,
422            None => llrs_decoder,
423        };
424
425        let (decoded, iterations, success) =
426            match self.decoder.decode(&llrs_decoder, self.max_iterations) {
427                Ok(output) => (output.codeword, output.iterations, true),
428                Err(output) => (output.codeword, output.iterations, false),
429            };
430        // Count only bit errors in the systematic part of the codeword
431        let bit_errors = message
432            .iter()
433            .zip(decoded.iter())
434            .filter(|(a, b)| a != b)
435            .count() as u64;
436        let frame_error = bit_errors > 0;
437        let false_decode = frame_error && success;
438        Ok(WorkerResultOk {
439            bit_errors,
440            frame_error,
441            false_decode,
442            iterations: iterations as u64,
443        })
444    }
445
446    fn random_message<R: Rng>(rng: &mut R, size: usize) -> Vec<u8> {
447        rng.sample_iter(StandardUniform)
448            .map(<u8 as From<bool>>::from)
449            .take(size)
450            .collect()
451    }
452
453    fn gf2_array(bits: &[u8]) -> Array1<GF2> {
454        Array1::from_iter(
455            bits.iter()
456                .map(|&b| if b == 1 { GF2::one() } else { GF2::zero() }),
457        )
458    }
459}
460
461impl CurrentStatistics {
462    fn new(has_bch: bool) -> CurrentStatistics {
463        CurrentStatistics {
464            num_frames: 0,
465            false_decodes: 0,
466            total_iterations: 0,
467            start: Instant::now(),
468            ldpc: CurrentCodeStatistics::new(),
469            bch: if has_bch {
470                Some(CurrentCodeStatistics::new())
471            } else {
472                None
473            },
474        }
475    }
476
477    fn errors_for_termination(&self) -> u64 {
478        if let Some(bch) = &self.bch {
479            bch.frame_errors
480        } else {
481            self.ldpc.frame_errors
482        }
483    }
484}
485
486impl CurrentCodeStatistics {
487    fn new() -> CurrentCodeStatistics {
488        CurrentCodeStatistics {
489            bit_errors: 0,
490            frame_errors: 0,
491            correct_iterations: 0,
492        }
493    }
494}
495
496impl Default for CurrentCodeStatistics {
497    fn default() -> CurrentCodeStatistics {
498        CurrentCodeStatistics::new()
499    }
500}
501
502impl CodeStatistics {
503    fn from_current(stats: &CurrentCodeStatistics, num_frames: u64, k: usize) -> CodeStatistics {
504        CodeStatistics {
505            bit_errors: stats.bit_errors,
506            frame_errors: stats.frame_errors,
507            correct_iterations: stats.correct_iterations,
508            ber: stats.bit_errors as f64 / (k as f64 * num_frames as f64),
509            fer: stats.frame_errors as f64 / num_frames as f64,
510            average_iterations_correct: stats.correct_iterations as f64
511                / (num_frames - stats.frame_errors) as f64,
512        }
513    }
514}
515
516impl Statistics {
517    fn from_current(stats: &CurrentStatistics, ebn0_db: f32, k: usize) -> Statistics {
518        let elapsed = Instant::now() - stats.start;
519        Statistics {
520            ebn0_db,
521            num_frames: stats.num_frames,
522            false_decodes: stats.false_decodes,
523            total_iterations: stats.total_iterations,
524            average_iterations: stats.total_iterations as f64 / stats.num_frames as f64,
525            elapsed,
526            throughput_mbps: 1e-6 * (k as f64 * stats.num_frames as f64) / elapsed.as_secs_f64(),
527            ldpc: CodeStatistics::from_current(&stats.ldpc, stats.num_frames, k),
528            bch: stats
529                .bch
530                .as_ref()
531                .map(|b| CodeStatistics::from_current(b, stats.num_frames, k)),
532        }
533    }
534}