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::{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    /// Maximum number of bit errors that the BCH decoder can correct (0 means no BCH decoder)
76    #[arg(long, default_value = "0")]
77    pub bch_max_errors: u64,
78    /// Number of worker threads (defaults to the number of CPUs)
79    #[arg(long, default_value_t = num_cpus::get())]
80    pub num_threads: usize,
81}
82
83impl<Dec: DecoderFactory + ValueEnum> Run for Args<Dec> {
84    fn run(&self) -> Result<(), Box<dyn Error>> {
85        let puncturing_pattern = if let Some(p) = self.puncturing.as_ref() {
86            Some(parse_puncturing_pattern(p)?)
87        } else {
88            None
89        };
90        let h = SparseMatrix::from_alist(&std::fs::read_to_string(&self.alist)?)?;
91        let mut output_file = if let Some(f) = &self.output_file {
92            Some(File::create(f)?)
93        } else {
94            None
95        };
96        let mut output_file_ldpc = match (self.bch_max_errors > 0, &self.output_file_ldpc) {
97            (true, Some(f)) => Some(File::create(f)?),
98            _ => None,
99        };
100        let num_ebn0s = ((self.max_ebn0 - self.min_ebn0) / self.step_ebn0).floor() as usize + 1;
101        let ebn0s = (0..num_ebn0s)
102            .map(|k| (self.min_ebn0 + k as f64 * self.step_ebn0) as f32)
103            .collect::<Vec<_>>();
104        let (report_tx, report_rx) = mpsc::channel();
105        let reporter = Reporter {
106            tx: report_tx,
107            interval: Duration::from_millis(500),
108        };
109        let test = BerTestBuilder {
110            h,
111            decoder_implementation: self.decoder.clone(),
112            modulation: self.modulation,
113            puncturing_pattern: puncturing_pattern.as_ref().map(|v| &v[..]),
114            interleaving_columns: self.interleaving,
115            max_frame_errors: self.frame_errors,
116            max_iterations: self.max_iter,
117            ebn0s_db: &ebn0s,
118            reporter: Some(reporter),
119            bch_max_errors: self.bch_max_errors,
120            num_workers: self.num_threads,
121        }
122        .build()?;
123        self.write_details(std::io::stdout(), &*test)?;
124        if let Some(f) = &mut output_file {
125            self.write_details(&*f, &*test)?;
126            if self.bch_max_errors > 0 {
127                writeln!(f)?;
128                writeln!(f, "LDPC+BCH results")?;
129                writeln!(f)?;
130            }
131        }
132        if let Some(f) = &mut output_file_ldpc {
133            self.write_details(&*f, &*test)?;
134            writeln!(f)?;
135            writeln!(f, "LDPC-only results")?;
136            writeln!(f)?;
137        }
138        let mut progress = Progress::new(report_rx, output_file, output_file_ldpc);
139        let progress = std::thread::spawn(move || progress.run());
140        test.run()?;
141        // This block cannot actually be written with the ? operator
142        #[allow(clippy::question_mark)]
143        if let Err(e) = progress.join().unwrap() {
144            return Err(e);
145        }
146        Ok(())
147    }
148}
149
150impl<Dec: DecoderFactory + ValueEnum> Args<Dec> {
151    fn write_details<W: Write>(&self, mut f: W, test: &dyn Ber) -> std::io::Result<()> {
152        writeln!(f, "BER TEST PARAMETERS")?;
153        writeln!(f, "-------------------")?;
154        writeln!(f, "Simulation:")?;
155        writeln!(f, " - Minimum Eb/N0: {:.2} dB", self.min_ebn0)?;
156        writeln!(f, " - Maximum Eb/N0: {:.2} dB", self.max_ebn0)?;
157        writeln!(f, " - Eb/N0 step: {:.2} dB", self.step_ebn0)?;
158        writeln!(f, " - Number of frame errors: {}", self.frame_errors)?;
159        writeln!(f, " - Number of worker threads: {}", self.num_threads)?;
160        writeln!(f, "Channel:")?;
161        writeln!(f, " - Modulation: {}", self.modulation)?;
162        writeln!(f, "LDPC code:")?;
163        writeln!(f, " - alist: {}", self.alist.display())?;
164        if let Some(puncturing) = self.puncturing.as_ref() {
165            writeln!(f, " - Puncturing pattern: {puncturing}")?;
166        }
167        if let Some(interleaving) = self.interleaving.as_ref() {
168            writeln!(f, " - Interleaving columns: {interleaving}")?;
169        }
170        writeln!(f, " - Information bits (k): {}", test.k())?;
171        writeln!(f, " - Codeword size (N_cw): {}", test.n_cw())?;
172        writeln!(f, " - Frame size (N): {}", test.n())?;
173        writeln!(f, " - Code rate: {:.3}", test.rate())?;
174        writeln!(f, "LDPC decoder:")?;
175        writeln!(f, " - Implementation: {}", self.decoder)?;
176        writeln!(f, " - Maximum iterations: {}", self.max_iter)?;
177        if self.bch_max_errors > 0 {
178            writeln!(f, "BCH decoder:")?;
179            writeln!(
180                f,
181                " - Maximum bit errors correctable: {}",
182                self.bch_max_errors
183            )?;
184        }
185        writeln!(f)?;
186        Ok(())
187    }
188}
189
190/// Parses a puncturing pattern.
191///
192/// This function parses a punturing pattern given as a string, converting it
193/// into a vector of bools. The format for the puncturing pattern should be
194/// like `"1,1,1,0"`.
195pub fn parse_puncturing_pattern(s: &str) -> Result<Vec<bool>, &'static str> {
196    let mut v = Vec::new();
197    for a in s.split(',') {
198        v.push(match a {
199            "0" => false,
200            "1" => true,
201            _ => return Err("invalid puncturing pattern"),
202        });
203    }
204    Ok(v)
205}
206
207#[derive(Debug)]
208struct Progress {
209    rx: Receiver<Report>,
210    term: Term,
211    output_file: Option<File>,
212    output_file_ldpc: Option<File>,
213}
214
215impl Progress {
216    fn new(
217        rx: Receiver<Report>,
218        output_file: Option<File>,
219        output_file_ldpc: Option<File>,
220    ) -> Progress {
221        Progress {
222            rx,
223            term: Term::stdout(),
224            output_file,
225            output_file_ldpc,
226        }
227    }
228
229    fn run(&mut self) -> Result<(), Box<dyn Error + Send + Sync + 'static>> {
230        ctrlc::set_handler({
231            let term = self.term.clone();
232            move || {
233                let _ = term.write_line("");
234                let _ = term.show_cursor();
235                std::process::exit(0);
236            }
237        })?;
238
239        let ret = self.work();
240        self.term.write_line("")?;
241        self.term.show_cursor()?;
242        ret
243    }
244
245    fn work(&mut self) -> Result<(), Box<dyn Error + Send + Sync + 'static>> {
246        self.term.set_title("ldpc-toolbox ber");
247        self.term.hide_cursor()?;
248        self.term.write_line(Self::format_header())?;
249        if let Some(f) = &mut self.output_file {
250            writeln!(f, "{}", Self::format_header())?;
251        }
252        if let Some(f) = &mut self.output_file_ldpc {
253            writeln!(f, "{}", Self::format_header())?;
254        }
255        let mut last_stats = None;
256        loop {
257            let Report::Statistics(stats) = self.rx.recv().unwrap() else {
258                // BER test has finished
259                let last_stats = last_stats.unwrap();
260                if let Some(f) = &mut self.output_file {
261                    writeln!(f, "{}", &Self::format_progress(&last_stats, false))?;
262                }
263                if let Some(f) = &mut self.output_file_ldpc {
264                    writeln!(f, "{}", &Self::format_progress(&last_stats, true))?;
265                }
266                return Ok(());
267            };
268            if let Some(s) = &last_stats
269                && s.ebn0_db != stats.ebn0_db
270            {
271                if let Some(f) = &mut self.output_file {
272                    writeln!(f, "{}", &Self::format_progress(s, false))?;
273                }
274                if let Some(f) = &mut self.output_file_ldpc {
275                    writeln!(f, "{}", &Self::format_progress(s, true))?;
276                }
277            }
278            match &last_stats {
279                Some(s) if s.ebn0_db == stats.ebn0_db => {
280                    self.term.move_cursor_up(1)?;
281                    self.term.clear_line()?;
282                }
283                _ => (),
284            };
285            self.term
286                .write_line(&Self::format_progress(&stats, false))?;
287            last_stats = Some(stats);
288        }
289    }
290
291    fn format_header() -> &'static str {
292        "  Eb/N0 |   Frames | Bit errs | Frame er | False de |     BER |     FER | Avg iter | Avg corr | Throughp | Elapsed\n\
293         --------|----------|----------|----------|----------|---------|---------|----------|----------|----------|----------"
294    }
295
296    fn format_progress(stats: &Statistics, force_ldpc: bool) -> String {
297        let code_stats = match (force_ldpc, &stats.bch) {
298            (true, _) => &stats.ldpc,
299            (false, Some(bch)) => bch,
300            (false, None) => &stats.ldpc,
301        };
302        format!(
303            "{:7.2} | {:8} | {:8} | {:8} | {:8} | {:7.2e} | {:7.2e} | {:8.1} | {:8.1} | {:8.3} | {}",
304            stats.ebn0_db,
305            stats.num_frames,
306            code_stats.bit_errors,
307            code_stats.frame_errors,
308            stats.false_decodes,
309            code_stats.ber,
310            code_stats.fer,
311            stats.average_iterations,
312            code_stats.average_iterations_correct,
313            stats.throughput_mbps,
314            humantime::format_duration(Duration::from_secs(stats.elapsed.as_secs()))
315        )
316    }
317}