ldpc_toolbox/cli/
ber.rs

1//! BER test CLI subcommand.
2//!
3//! This subcommand can be used to perform a BER test of an LDPC decoder.
4//!
5//! # Examples
6//!
7//! The CCSDS r=1/2, k=1024 LDPC code can be simulated with
8//! ```shell
9//! $ ldpc-toolbox ber --min-ebn0 0.0 --max-ebn0 2.05 --step-ebn0 0.1 \
10//!       --puncturing 1,1,1,1,0 ar4ja_1_2_1024.alist
11//! ```
12//!
13//! The alist file must have been generated previoulsy with the
14//! [ccsds](super::ccsds) subcommand.
15
16use crate::{
17    cli::*,
18    decoder::factory::{DecoderFactory, DecoderImplementation},
19    simulation::{
20        ber::{BerTestParameters, Report, Reporter, Statistics},
21        factory::{Ber, BerTestBuilder, Modulation},
22    },
23    sparse::SparseMatrix,
24};
25use clap::{Parser, ValueEnum};
26use console::Term;
27use std::{
28    error::Error,
29    fs::File,
30    io::Write,
31    path::PathBuf,
32    sync::mpsc::{self, Receiver},
33    time::Duration,
34};
35
36/// BER test CLI arguments.
37#[derive(Debug, Parser)]
38#[command(about = "Performs a BER simulation")]
39pub struct Args<Dec: DecoderFactory + ValueEnum = DecoderImplementation> {
40    /// alist file for the code
41    pub alist: PathBuf,
42    /// Output file for simulation results
43    #[arg(long)]
44    pub output_file: Option<PathBuf>,
45    /// Output file for LDPC-only results (only useful when using BCH)
46    #[arg(long)]
47    pub output_file_ldpc: Option<PathBuf>,
48    /// Decoder implementation
49    #[arg(long, default_value = "Phif64")]
50    pub decoder: Dec,
51    /// Modulation
52    #[arg(long, default_value_t = Modulation::Bpsk)]
53    pub modulation: Modulation,
54    /// Puncturing pattern (format "1,1,1,0")
55    #[arg(long)]
56    pub puncturing: Option<String>,
57    /// Interleaving columns (negative for backwards read)
58    #[arg(long)]
59    pub interleaving: Option<isize>,
60    /// Minimum Eb/N0 (dB)
61    #[arg(long)]
62    pub min_ebn0: f64,
63    /// Maximum Eb/N0 (dB)
64    #[arg(long)]
65    pub max_ebn0: f64,
66    /// Eb/N0 step (dB)
67    #[arg(long)]
68    pub step_ebn0: f64,
69    /// Maximum number of iterations
70    #[arg(long, default_value = "100")]
71    pub max_iter: usize,
72    /// Number of frame errors to collect
73    #[arg(long, default_value = "100")]
74    pub frame_errors: u64,
75    /// Minimum run time per Eb/N0 (if unset, the simulation finishes as soon as the frame errors are reached)
76    #[arg(long, value_parser = humantime::parse_duration)]
77    pub min_time: Option<Duration>,
78    /// Maximum run time per Eb/N0 (if unset, the simulation only finishes when the frame errors are reached)
79    #[arg(long, value_parser = humantime::parse_duration)]
80    pub max_time: Option<Duration>,
81    /// Maximum number of bit errors that the BCH decoder can correct (0 means no BCH decoder)
82    #[arg(long, default_value = "0")]
83    pub bch_max_errors: u64,
84    /// Number of worker threads (defaults to the number of CPUs)
85    #[arg(long, default_value_t = num_cpus::get())]
86    pub num_threads: usize,
87}
88
89impl<Dec: DecoderFactory + ValueEnum> Run for Args<Dec> {
90    fn run(&self) -> Result<(), Box<dyn Error>> {
91        let puncturing_pattern = if let Some(p) = self.puncturing.as_ref() {
92            Some(parse_puncturing_pattern(p)?)
93        } else {
94            None
95        };
96        let h = SparseMatrix::from_alist(&std::fs::read_to_string(&self.alist)?)?;
97        let mut output_file = if let Some(f) = &self.output_file {
98            Some(File::create(f)?)
99        } else {
100            None
101        };
102        let mut output_file_ldpc = match (self.bch_max_errors > 0, &self.output_file_ldpc) {
103            (true, Some(f)) => Some(File::create(f)?),
104            _ => None,
105        };
106        let num_ebn0s = ((self.max_ebn0 - self.min_ebn0) / self.step_ebn0).floor() as usize + 1;
107        let ebn0s = (0..num_ebn0s)
108            .map(|k| (self.min_ebn0 + k as f64 * self.step_ebn0) as f32)
109            .collect::<Vec<_>>();
110        let (report_tx, report_rx) = mpsc::channel();
111        let reporter = Reporter {
112            tx: report_tx,
113            interval: Duration::from_millis(500),
114        };
115        let test = BerTestBuilder {
116            modulation: self.modulation,
117            parameters: BerTestParameters {
118                h,
119                decoder_implementation: self.decoder.clone(),
120                puncturing_pattern: puncturing_pattern.as_ref().map(|v| &v[..]),
121                interleaving_columns: self.interleaving,
122                max_frame_errors: self.frame_errors,
123                min_run_time: self.min_time,
124                max_run_time: self.max_time,
125                max_iterations: self.max_iter,
126                ebn0s_db: &ebn0s,
127                reporter: Some(reporter),
128                bch_max_errors: self.bch_max_errors,
129                num_workers: self.num_threads,
130            },
131        }
132        .build()?;
133        self.write_details(std::io::stdout(), &*test)?;
134        if let Some(f) = &mut output_file {
135            self.write_details(&*f, &*test)?;
136            if self.bch_max_errors > 0 {
137                writeln!(f)?;
138                writeln!(f, "LDPC+BCH results")?;
139                writeln!(f)?;
140            }
141        }
142        if let Some(f) = &mut output_file_ldpc {
143            self.write_details(&*f, &*test)?;
144            writeln!(f)?;
145            writeln!(f, "LDPC-only results")?;
146            writeln!(f)?;
147        }
148        let mut progress = Progress::new(report_rx, output_file, output_file_ldpc);
149        let progress = std::thread::spawn(move || progress.run());
150        test.run()?;
151        // This block cannot actually be written with the ? operator
152        #[allow(clippy::question_mark)]
153        if let Err(e) = progress.join().unwrap() {
154            return Err(e);
155        }
156        Ok(())
157    }
158}
159
160impl<Dec: DecoderFactory + ValueEnum> Args<Dec> {
161    fn write_details<W: Write>(&self, mut f: W, test: &dyn Ber) -> std::io::Result<()> {
162        writeln!(f, "BER TEST PARAMETERS")?;
163        writeln!(f, "-------------------")?;
164        writeln!(f, "Simulation:")?;
165        writeln!(f, " - Minimum Eb/N0: {:.2} dB", self.min_ebn0)?;
166        writeln!(f, " - Maximum Eb/N0: {:.2} dB", self.max_ebn0)?;
167        writeln!(f, " - Eb/N0 step: {:.2} dB", self.step_ebn0)?;
168        writeln!(f, " - Number of frame errors: {}", self.frame_errors)?;
169        if let Some(min_time) = self.min_time {
170            writeln!(
171                f,
172                " - Minimum run time per Eb/N0: {}",
173                humantime::format_duration(min_time)
174            )?;
175        }
176        if let Some(max_time) = self.max_time {
177            writeln!(
178                f,
179                " - Maximum run time per Eb/N0: {}",
180                humantime::format_duration(max_time)
181            )?;
182        }
183        writeln!(f, " - Number of worker threads: {}", self.num_threads)?;
184        writeln!(f, "Channel:")?;
185        writeln!(f, " - Modulation: {}", self.modulation)?;
186        writeln!(f, "LDPC code:")?;
187        writeln!(f, " - alist: {}", self.alist.display())?;
188        if let Some(puncturing) = self.puncturing.as_ref() {
189            writeln!(f, " - Puncturing pattern: {puncturing}")?;
190        }
191        if let Some(interleaving) = self.interleaving.as_ref() {
192            writeln!(f, " - Interleaving columns: {interleaving}")?;
193        }
194        writeln!(f, " - Information bits (k): {}", test.k())?;
195        writeln!(f, " - Codeword size (N_cw): {}", test.n_cw())?;
196        writeln!(f, " - Frame size (N): {}", test.n())?;
197        writeln!(f, " - Code rate: {:.3}", test.rate())?;
198        writeln!(f, "LDPC decoder:")?;
199        writeln!(f, " - Implementation: {}", self.decoder)?;
200        writeln!(f, " - Maximum iterations: {}", self.max_iter)?;
201        if self.bch_max_errors > 0 {
202            writeln!(f, "BCH decoder:")?;
203            writeln!(
204                f,
205                " - Maximum bit errors correctable: {}",
206                self.bch_max_errors
207            )?;
208        }
209        writeln!(f)?;
210        Ok(())
211    }
212}
213
214/// Parses a puncturing pattern.
215///
216/// This function parses a punturing pattern given as a string, converting it
217/// into a vector of bools. The format for the puncturing pattern should be
218/// like `"1,1,1,0"`.
219pub fn parse_puncturing_pattern(s: &str) -> Result<Vec<bool>, &'static str> {
220    let mut v = Vec::new();
221    for a in s.split(',') {
222        v.push(match a {
223            "0" => false,
224            "1" => true,
225            _ => return Err("invalid puncturing pattern"),
226        });
227    }
228    Ok(v)
229}
230
231#[derive(Debug)]
232struct Progress {
233    rx: Receiver<Report>,
234    term: Term,
235    output_file: Option<File>,
236    output_file_ldpc: Option<File>,
237}
238
239impl Progress {
240    fn new(
241        rx: Receiver<Report>,
242        output_file: Option<File>,
243        output_file_ldpc: Option<File>,
244    ) -> Progress {
245        Progress {
246            rx,
247            term: Term::stdout(),
248            output_file,
249            output_file_ldpc,
250        }
251    }
252
253    fn run(&mut self) -> Result<(), Box<dyn Error + Send + Sync + 'static>> {
254        ctrlc::set_handler({
255            let term = self.term.clone();
256            move || {
257                let _ = term.write_line("");
258                let _ = term.show_cursor();
259                std::process::exit(0);
260            }
261        })?;
262
263        let ret = self.work();
264        self.term.write_line("")?;
265        self.term.show_cursor()?;
266        ret
267    }
268
269    fn work(&mut self) -> Result<(), Box<dyn Error + Send + Sync + 'static>> {
270        self.term.set_title("ldpc-toolbox ber");
271        self.term.hide_cursor()?;
272        self.term.write_line(Self::format_header())?;
273        if let Some(f) = &mut self.output_file {
274            writeln!(f, "{}", Self::format_header())?;
275        }
276        if let Some(f) = &mut self.output_file_ldpc {
277            writeln!(f, "{}", Self::format_header())?;
278        }
279        let mut last_stats = None;
280        loop {
281            let Report::Statistics(stats) = self.rx.recv().unwrap() else {
282                // BER test has finished
283                let last_stats = last_stats.unwrap();
284                if let Some(f) = &mut self.output_file {
285                    writeln!(f, "{}", &Self::format_progress(&last_stats, false))?;
286                }
287                if let Some(f) = &mut self.output_file_ldpc {
288                    writeln!(f, "{}", &Self::format_progress(&last_stats, true))?;
289                }
290                return Ok(());
291            };
292            if let Some(s) = &last_stats
293                && s.ebn0_db != stats.ebn0_db
294            {
295                if let Some(f) = &mut self.output_file {
296                    writeln!(f, "{}", &Self::format_progress(s, false))?;
297                }
298                if let Some(f) = &mut self.output_file_ldpc {
299                    writeln!(f, "{}", &Self::format_progress(s, true))?;
300                }
301            }
302            match &last_stats {
303                Some(s) if s.ebn0_db == stats.ebn0_db => {
304                    self.term.move_cursor_up(1)?;
305                    self.term.clear_line()?;
306                }
307                _ => (),
308            };
309            self.term
310                .write_line(&Self::format_progress(&stats, false))?;
311            last_stats = Some(stats);
312        }
313    }
314
315    fn format_header() -> &'static str {
316        "  Eb/N0 |   Frames | Bit errs | Frame er | False de |     BER |     FER | Avg iter | Avg corr | Throughp | Elapsed\n\
317         --------|----------|----------|----------|----------|---------|---------|----------|----------|----------|----------"
318    }
319
320    fn format_progress(stats: &Statistics, force_ldpc: bool) -> String {
321        let code_stats = match (force_ldpc, &stats.bch) {
322            (true, _) => &stats.ldpc,
323            (false, Some(bch)) => bch,
324            (false, None) => &stats.ldpc,
325        };
326        format!(
327            "{:7.2} | {:8} | {:8} | {:8} | {:8} | {:7.2e} | {:7.2e} | {:8.1} | {:8.1} | {:8.3} | {}",
328            stats.ebn0_db,
329            stats.num_frames,
330            code_stats.bit_errors,
331            code_stats.frame_errors,
332            stats.false_decodes,
333            code_stats.ber,
334            code_stats.fer,
335            stats.average_iterations,
336            code_stats.average_iterations_correct,
337            stats.throughput_mbps,
338            humantime::format_duration(Duration::from_secs(stats.elapsed.as_secs()))
339        )
340    }
341}