comnum_fec/ccdm.rs
1//! Constant-composition distribution matching (CCDM): uniform bits to sequences of
2//! symbols of a prescribed distribution, the shaping stage of probabilistic amplitude
3//! shaping (`comnum_systems::pas`).
4//!
5//! # Model
6//!
7//! A composition $(n_{1}, \dots, n_{A})$, $\sum_{a} n_{a} = n$, defines the set of the
8//! $\binom{n}{n_{1}, \dots, n_{A}} = n! / \prod_{a} n_{a}!$ sequences of $n$ symbols in which
9//! symbol $a$ appears $n_{a}$ times: their empirical distribution is exactly
10//! $n_{a} / n$. The matcher maps $k = \lfloor \log_{2} \binom{n}{n_{1}, \dots, n_{A}}
11//! \rfloor$ uniform bits, read as an integer, to the sequence of that rank in the
12//! lexicographic order of the set (enumerative coding, Cover 1973), and the dematcher
13//! computes the rank back. The rate $k/n$ approaches the entropy of the target
14//! distribution as $n$ grows, with a rate loss of the order of $\log n / n$ (Schulte &
15//! Böcherer 2016, who implement the same set by arithmetic coding).
16//!
17//! The ranks are exact integers of about $n H$ bits ([`comnum_math::bigint::BigUint`]); a
18//! symbol costs $O(A)$ big-integer products and quotients by machine integers.
19//!
20//! [`Ccdm::from_distribution`] chooses the composition nearest to a target
21//! distribution (largest remainders). [`Ccdm::dematch`] accepts any sequence: a symbol
22//! that the composition has used up (after a decoding error) is replaced by the nearest
23//! available one, so that wrong sequences still give $k$ bits.
24//!
25//! # Examples
26//! ```
27//! use comnum_fec::ccdm::Ccdm;
28//! // 4 amplitudes with probabilities 0.4, 0.3, 0.2, 0.1, blocks of 100 amplitudes
29//! let dm = Ccdm::from_distribution(&[0.4, 0.3, 0.2, 0.1], 100)?;
30//! assert_eq!(dm.composition(), &[40, 30, 20, 10]);
31//! assert_eq!(dm.k(), 175); // log2 of 100!/(40! 30! 20! 10!) = 175.03; 100 H = 184.6
32//! let bits: Vec<u8> = (0..dm.k()).map(|i| ((i * 7) % 3 == 0) as u8).collect();
33//! let a = dm.match_bits(&bits)?;
34//! assert_eq!(a.iter().filter(|&&s| s == 0).count(), 40);
35//! assert_eq!(dm.dematch(&a)?, bits);
36//! # Ok::<(), comnum_core::Error>(())
37//! ```
38//!
39//! # References
40//!
41//! 1. P. Schulte and G. Böcherer, "Constant composition distribution matching," *IEEE
42//! Trans. Inf. Theory*, 62(1), 430–434, 2016.
43//! 2. T. M. Cover, "Enumerative source coding," *IEEE Trans. Inf. Theory*, 19(1),
44//! 73–77, 1973.
45
46use crate::block::expect_len;
47use comnum_core::{Result, ensure};
48use comnum_math::bigint::BigUint;
49
50/// A constant-composition distribution matcher (see the module).
51#[derive(Clone, Debug, PartialEq, Eq)]
52pub struct Ccdm {
53 composition: Vec<usize>,
54 n: usize,
55 k: usize,
56 total: BigUint,
57}
58
59impl Ccdm {
60 /// The matcher of a composition: `composition[a]` occurrences of symbol `a`.
61 ///
62 /// # Errors
63 /// [`comnum_core::Error::Config`] for an empty composition or one that carries no bit.
64 pub fn new(composition: Vec<usize>) -> Result<Ccdm> {
65 let n: usize = composition.iter().sum();
66 ensure(n > 0, || "the composition is empty".into())?;
67 // the multinomial coefficient as a product of binomial coefficients
68 let mut total = BigUint::from(1);
69 let mut left = n as u64;
70 for &c in &composition {
71 for i in 1..=c as u64 {
72 total = total.mul_small(left - c as u64 + i).div_small(i).0;
73 }
74 left -= c as u64;
75 }
76 let k = total.bits().saturating_sub(1);
77 ensure(k >= 1, || "the composition has a single sequence: it carries no bit".into())?;
78 Ok(Ccdm { composition, n, k, total })
79 }
80
81 /// The matcher of blocks of `n` symbols whose composition is the nearest to the
82 /// distribution `p` (largest remainders: `⌊n p_a⌋`, then one more for the largest
83 /// remainders until the sum is `n`).
84 ///
85 /// # Errors
86 /// [`comnum_core::Error::Config`] for probabilities that are not non-negative with a
87 /// positive sum, and as [`Ccdm::new`].
88 pub fn from_distribution(p: &[f64], n: usize) -> Result<Ccdm> {
89 let s: f64 = p.iter().sum();
90 ensure(!p.is_empty() && s > 0.0 && p.iter().all(|v| *v >= 0.0), || {
91 "the distribution must be non-negative with a positive sum".into()
92 })?;
93 let target: Vec<f64> = p.iter().map(|v| v / s * n as f64).collect();
94 let mut c: Vec<usize> = target.iter().map(|t| t.floor() as usize).collect();
95 let mut order: Vec<usize> = (0..p.len()).collect();
96 order.sort_by(|&a, &b| {
97 (target[b] - target[b].floor()).total_cmp(&(target[a] - target[a].floor())).then(a.cmp(&b))
98 });
99 let missing = n - c.iter().sum::<usize>();
100 for &a in order.iter().take(missing) {
101 c[a] += 1;
102 }
103 Ccdm::new(c)
104 }
105
106 /// The composition.
107 pub fn composition(&self) -> &[usize] {
108 &self.composition
109 }
110
111 /// Symbols per block $n$.
112 pub fn n(&self) -> usize {
113 self.n
114 }
115
116 /// Bits per block $k$.
117 pub fn k(&self) -> usize {
118 self.k
119 }
120
121 /// The rate $k/n$ (bits per symbol).
122 pub fn rate(&self) -> f64 {
123 self.k as f64 / self.n as f64
124 }
125
126 /// The distribution of the symbols, $n_{a}/n$.
127 pub fn distribution(&self) -> Vec<f64> {
128 self.composition.iter().map(|&c| c as f64 / self.n as f64).collect()
129 }
130
131 /// The sequence of $n$ symbols of `bits` ($k$ bits, the rank, most significant
132 /// first).
133 ///
134 /// # Errors
135 /// [`comnum_core::Error::Shape`] if `bits` does not have $k$ bits.
136 pub fn match_bits(&self, bits: &[u8]) -> Result<Vec<usize>> {
137 expect_len(bits.len(), self.k, "bits")?;
138 let mut r = BigUint::from_bits(bits);
139 let mut count = self.composition.clone();
140 let mut total = self.total.clone();
141 let mut left = self.n as u64;
142 let mut out = Vec::with_capacity(self.n);
143 for _ in 0..self.n {
144 for (a, c) in count.iter_mut().enumerate() {
145 if *c == 0 {
146 continue;
147 }
148 // the sequences that start with a
149 let with_a = total.mul_small(*c as u64).div_small(left).0;
150 if r < with_a {
151 out.push(a);
152 total = with_a;
153 *c -= 1;
154 left -= 1;
155 break;
156 }
157 r = r.sub(&with_a);
158 }
159 }
160 Ok(out)
161 }
162
163 /// The $k$ bits of a sequence of $n$ symbols (its rank, most significant first); a
164 /// symbol the composition has used up is replaced by the nearest available one.
165 ///
166 /// # Errors
167 /// [`comnum_core::Error::Shape`] if `symbols` does not have $n$ symbols.
168 pub fn dematch(&self, symbols: &[usize]) -> Result<Vec<u8>> {
169 expect_len(symbols.len(), self.n, "symbols")?;
170 let mut r = BigUint::default();
171 let mut count = self.composition.clone();
172 let mut total = self.total.clone();
173 let mut left = self.n as u64;
174 for &s in symbols {
175 let s = (0..count.len()).filter(|&a| count[a] > 0).min_by_key(|&a| (a.abs_diff(s), a)).unwrap_or(0);
176 for &c in count.iter().take(s) {
177 if c > 0 {
178 r = r.add(&total.mul_small(c as u64).div_small(left).0);
179 }
180 }
181 total = total.mul_small(count[s] as u64).div_small(left).0;
182 count[s] -= 1;
183 left -= 1;
184 }
185 Ok(r.to_bits(self.k))
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192 use comnum_core::rng::Rng;
193
194 #[test]
195 fn matching_is_a_bijection_onto_the_composition() {
196 let dm = Ccdm::new(vec![3, 2, 1]).unwrap(); // 60 sequences, k = 5
197 assert_eq!(dm.k(), 5);
198 let mut seen = std::collections::HashSet::new();
199 for r in 0..32u8 {
200 let bits: Vec<u8> = (0..5).rev().map(|i| (r >> i) & 1).collect();
201 let a = dm.match_bits(&bits).unwrap();
202 assert_eq!(a.iter().filter(|&&s| s == 0).count(), 3);
203 assert_eq!(dm.dematch(&a).unwrap(), bits);
204 assert!(seen.insert(a));
205 }
206 }
207
208 #[test]
209 fn long_blocks_approach_the_entropy() {
210 let p = [0.35, 0.3, 0.2, 0.15];
211 let h: f64 = p.iter().map(|v: &f64| -v * v.log2()).sum();
212 let mut rng = Rng::new(1);
213 for n in [64, 512, 4096] {
214 let dm = Ccdm::from_distribution(&p, n).unwrap();
215 let bits: Vec<u8> = (0..dm.k()).map(|_| rng.bit()).collect();
216 assert_eq!(dm.dematch(&dm.match_bits(&bits).unwrap()).unwrap(), bits);
217 // rate loss below 4 log2(n)/n
218 assert!(h - dm.rate() < 4.0 * (n as f64).log2() / n as f64, "{n}: {}", h - dm.rate());
219 }
220 }
221
222 #[test]
223 fn a_wrong_sequence_still_gives_k_bits() {
224 let dm = Ccdm::from_distribution(&[0.5, 0.3, 0.2], 50).unwrap();
225 let wrong = vec![2; 50];
226 assert_eq!(dm.dematch(&wrong).unwrap().len(), dm.k());
227 }
228}