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}