ldpc_toolbox/
mackay_neal.rs

1//! # MacKay-Neal pseudorandom LDPC construction
2//!
3//! This implements the algorithms from *MacKay, D.J. and Neal, R.M., 1996.
4//! Near Shannon limit performance of low density parity check codes.
5//! Electronics letters, 32(18), p.1645.* and variations on this idea.
6//!
7//! The algorithm works by adding column by column to the parity check
8//! matrix. At each step, `wc` rows from the subset of rows that have not yet
9//! achieved the total row weight `wr` are randomly chosen, and ones are inserted
10//! in those positions.
11//!
12//! Optionally, to enforce a minimum girth, at each step the candidate
13//! column is checked to see if it maintains the girth of the graph at or above
14//! the minimum. If not, another random candidate column is
15//! chosen according to the available rows. The algorithm aborts if after
16//! a fixed number of trials it is unable to yield a new column satisfying
17//! the required properties.
18//!
19//! # Examples
20//! To run a MacKay-Neal LDPC generation algorithm, it is necessary
21//! to create a [`Config`] and then use the `run()` method.
22//! ```
23//! # use ldpc_toolbox::mackay_neal::{Config, FillPolicy};
24//! let conf = Config {
25//!     nrows: 4,
26//!     ncols: 8,
27//!     wr: 4,
28//!     wc: 2,
29//!     backtrack_cols: 0,
30//!     backtrack_trials: 0,
31//!     min_girth: None,
32//!     girth_trials: 0,
33//!     fill_policy: FillPolicy::Uniform,
34//!  };
35//!  let seed = 42;
36//!  let h = conf.run(seed).unwrap();
37//!  print!("{}", h.alist());
38//!  ```
39
40use crate::rand::{Rng, *};
41use crate::sparse::{Node, SparseMatrix};
42use crate::util::*;
43use rand::seq::IteratorRandom;
44use rayon::prelude::*;
45use std::fmt;
46use std::fmt::{Display, Formatter};
47
48/// Runtime errors of the MacKay-Neal construction.
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum Error {
51    /// No rows available.
52    NoAvailRows,
53    /// Girth is too small (should not be returned to the user).
54    GirthTooSmall,
55    /// Exceeded backtrack trials.
56    NoMoreBacktrack,
57    /// Exceeded girth trials.
58    NoMoreTrials,
59}
60
61impl Display for Error {
62    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
63        match self {
64            Error::NoAvailRows => write!(f, "no rows available"),
65            Error::GirthTooSmall => write!(f, "girth is too small"),
66            Error::NoMoreBacktrack => write!(f, "exceeded backtrack trials"),
67            Error::NoMoreTrials => write!(f, "exceeded girth trials"),
68        }
69    }
70}
71
72impl std::error::Error for Error {}
73
74/// Result type used to indicate MacKay-Neal runtime errors.
75pub type Result<T> = std::result::Result<T, Error>;
76
77/// Configuration for the MacKay-Neal construction.
78///
79/// This configuration is used to set the parameters of the
80/// LDPC code to construct as well as some options that affect
81/// the exectution of the algorithm.
82#[derive(Debug, Clone, PartialEq, Eq)]
83pub struct Config {
84    /// Number of rows of the parity check matrix.
85    pub nrows: usize,
86    /// Number of columns of the parity check matrix.
87    pub ncols: usize,
88    /// Maximum row weight of the parity check matrix.
89    pub wr: usize,
90    /// Column weight of the parity check matrix.
91    pub wc: usize,
92    /// Number of columns to backtrack when there are not enough
93    /// available columns with weight smaller than the maximum row
94    /// weight.
95    pub backtrack_cols: usize,
96    /// Number of times to attempt backtracking before aborting.
97    pub backtrack_trials: usize,
98    /// Minimum girth of the Tanner graph; `None` indicates that
99    /// no constraints are imposed on the girth.
100    pub min_girth: Option<usize>,
101    /// Number of times to re-try generating a column to satisfy
102    /// the minimum girth constraint before aborting.
103    pub girth_trials: usize,
104    /// Policy used to select the rows to fill.
105    pub fill_policy: FillPolicy,
106}
107
108impl Config {
109    /// Runs the MacKay-Neal algorith using a random seed `seed`.
110    pub fn run(&self, seed: u64) -> Result<SparseMatrix> {
111        MacKayNeal::new(self, seed).run()
112    }
113
114    /// Searches for a seed for a successful MacKay-Neal construction
115    /// by trying several seeds.
116    ///
117    /// The search is performed in parallel using a parallel iterator
118    /// from the rayon crate. This function returns the successful seed
119    /// and the corresponding parity check matrix, if one is found, or
120    /// `None` otherwise.
121    pub fn search(&self, start_seed: u64, max_tries: u64) -> Option<(u64, SparseMatrix)> {
122        (start_seed..start_seed + max_tries)
123            .into_par_iter()
124            .filter_map(|s| self.run(s).ok().map(|x| (s, x)))
125            .find_any(|_| true)
126    }
127}
128
129/// Policy used to select the rows to fill when adding a new column
130/// in the MacKay-Neal algorith.
131///
132/// The `Random` policy chooses rows completely randomly, and only
133/// imposes the maximum row weigth constraint of the configuration.
134/// This has the drawback that near the end of the algorithm too
135/// many rows can be full, while other rows have too many missing
136/// items. Therefore, the algorithm will fail. Even if backtracking
137/// is used, it is unlikely that the algorithm suceeds when the
138/// matrix is large and the LDPC code is regular, so that all the
139/// rows will necessarily end up with the same row weight.
140///
141/// The `Uniform` policy solves this problem by always picking
142/// rows which have the lower weight possible. There is still some
143/// randomness involved in the selection of rows, but the only
144/// choices that are considered are those with the property that
145/// it is not possible to exchange one of the choosen rows by
146/// another row that was not chosen and has stricly less weight.
147#[derive(Debug, Clone, Copy, PartialEq, Eq)]
148pub enum FillPolicy {
149    /// Choose randomly from the set of rows whose weight is less
150    /// than the maximum row weight.
151    Random,
152    /// Try to choose only among the rows which have lower weight.
153    Uniform,
154}
155
156struct MacKayNeal {
157    wr: usize,
158    wc: usize,
159    h: SparseMatrix,
160    rng: Rng,
161    backtrack_cols: usize,
162    backtrack_trials: usize,
163    min_girth: Option<usize>,
164    girth_trials: usize,
165    fill_policy: FillPolicy,
166    current_col: usize,
167}
168
169impl MacKayNeal {
170    fn new(conf: &Config, seed: u64) -> MacKayNeal {
171        MacKayNeal {
172            wr: conf.wr,
173            wc: conf.wc,
174            h: SparseMatrix::new(conf.nrows, conf.ncols),
175            rng: Rng::seed_from_u64(seed),
176            backtrack_cols: conf.backtrack_cols,
177            backtrack_trials: conf.backtrack_trials,
178            min_girth: conf.min_girth,
179            girth_trials: conf.girth_trials,
180            fill_policy: conf.fill_policy,
181            current_col: 0,
182        }
183    }
184
185    fn try_insert_column(&mut self) -> Result<()> {
186        let rows = self.select_rows()?;
187        self.h.insert_col(self.current_col, rows.into_iter());
188        if let Some(g) = self.min_girth
189            && self
190                .h
191                .girth_at_node_with_max(Node::Col(self.current_col), g - 1)
192                .is_some()
193        {
194            self.h.clear_col(self.current_col);
195            return Err(Error::GirthTooSmall);
196        }
197        Ok(())
198    }
199
200    fn select_rows(&mut self) -> Result<Vec<usize>> {
201        match self.fill_policy {
202            FillPolicy::Random => {
203                let h = &self.h;
204                let wr = self.wr;
205                let avail_rows = (0..self.h.num_rows()).filter(|&r| h.row_weight(r) < wr);
206                let select_rows = avail_rows.choose_multiple(&mut self.rng, self.wc);
207                if select_rows.len() < self.wc {
208                    return Err(Error::NoAvailRows);
209                }
210                Ok(select_rows)
211            }
212            FillPolicy::Uniform => {
213                let avail_rows: Vec<(usize, usize)> = (0..self.h.num_rows())
214                    .filter_map(|r| {
215                        let w = self.h.row_weight(r);
216                        if w < self.wr { Some((r, w)) } else { None }
217                    })
218                    .collect();
219                avail_rows
220                    .sort_by_random_sel(self.wc, |(_, x), (_, y)| x.cmp(y), &mut self.rng)
221                    .map(|a| a.into_iter().map(|(x, _)| x).collect())
222                    .ok_or(Error::NoAvailRows)
223            }
224        }
225    }
226
227    fn backtrack(&mut self) -> Result<()> {
228        if self.backtrack_trials == 0 {
229            return Err(Error::NoMoreBacktrack);
230        }
231        self.backtrack_trials -= 1;
232        let b = std::cmp::min(self.current_col, self.backtrack_cols);
233        let a = self.current_col - b;
234        for col in a..self.current_col {
235            self.h.clear_col(col);
236        }
237        self.current_col = a;
238        Ok(())
239    }
240
241    fn retry_girth(&mut self) -> Result<()> {
242        if self.girth_trials == 0 {
243            return Err(Error::NoMoreTrials);
244        }
245        self.girth_trials -= 1;
246        Ok(())
247    }
248
249    fn run(mut self) -> Result<SparseMatrix> {
250        while self.current_col < self.h.num_cols() {
251            match self.try_insert_column() {
252                Ok(_) => self.current_col += 1,
253                Err(Error::NoAvailRows) => self.backtrack()?,
254                Err(Error::GirthTooSmall) => self.retry_girth()?,
255                Err(e) => return Err(e),
256            };
257        }
258        Ok(self.h)
259    }
260}
261
262#[cfg(test)]
263mod tests {
264    use super::*;
265
266    #[test]
267    fn small_matrix() {
268        let conf = Config {
269            nrows: 4,
270            ncols: 8,
271            wr: 4,
272            wc: 2,
273            backtrack_cols: 0,
274            backtrack_trials: 0,
275            min_girth: None,
276            girth_trials: 0,
277            fill_policy: FillPolicy::Random,
278        };
279        let h = conf.run(187).unwrap();
280        let alist = "8 4
2812 4
2822 2 2 2 2 2 2 2
2834 4 4 4
2841 3
2852 4
2862 3
2871 4
2881 4
2891 4
2902 3
2912 3
2921 4 5 6
2932 3 7 8
2941 3 7 8
2952 4 5 6
296";
297        assert_eq!(h.alist(), alist);
298    }
299}