Skip to main content

dsi_bitstream/codes/
pi.rs

1/*
2 * SPDX-FileCopyrightText: 2024 Tommaso Fontana
3 * SPDX-FileCopyrightText: 2025 Sebastiano Vigna
4 *
5 * SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later
6 */
7
8//! Streamlined Apostolico–Drovandi π codes.
9//!
10//! The streamlined π code with parameter *k* ≥ 0 of a natural number *n* is the
11//! concatenation of the [Rice code] with parameter 2*ᵏ* of ⌊log₂(*n* + 1)⌋ and
12//! of the binary representation of *n* + 1 with the most significant bit
13//! removed.
14//!
15//! The implied distribution of a π code with parameter *k* is ≈
16//! 1/*x*<sup>1 + 1/2*ᵏ*</sup>.
17//!
18//! Note that π₀ = [ζ₁] = [γ] and π₁ = [ζ₂]. However, due to [subtle problems
19//! with endianness], in the little-endian case π₁ and ζ₂ have the same codeword
20//! lengths but slightly permuted bits.
21//!
22//! This module provides a generic implementation of π codes, and a specialized
23//! implementation for π₂ that may use tables.
24//!
25//! The supported range is [0 . . 2⁶⁴ – 1) for *k* in [0 . . 64).
26//!
27//! In the original paper the definition of the code is very convoluted, as the
28//! authors appear to have missed the connection with [Rice codes]. The
29//! codewords implemented by this module are equivalent to the ones in the
30//! paper, in the sense that corresponding codewords have the same length, but
31//! the codewords for *k* ≥ 2 are different, and encoding/decoding is
32//! faster—hence the name "streamlined π codes".
33//!
34//! # Table-Based Optimization
35//!
36//! Like [δ] codes, π codes use a special optimization for partial decoding. Due
37//! to the structure of π codes (a Rice code followed by fixed bits), when a
38//! complete codeword cannot be read from the table, the table may still provide
39//! partial information about the Rice prefix (λ) that was successfully decoded.
40//! This partial state is used to directly read the remaining λ fixed bits,
41//! avoiding re-reading the Rice prefix.
42//!
43//! # References
44//!
45//! Alberto Apostolico and Guido Drovandi. "[Graph Compression by BFS]",
46//! Algorithms, 2:1031-1044, 2009.
47//!
48//! [Rice code]: super::rice
49//! [ζ₁]: super::zeta
50//! [γ]: super::gamma
51//! [ζ₂]: super::zeta
52//! [subtle problems with endianness]: crate::codes
53//! [Rice codes]: super::rice
54//! [δ]: super::delta
55//! [Graph Compression by BFS]: https://doi.org/10.3390/a2031031
56
57use crate::traits::*;
58
59use super::{RiceRead, RiceWrite, len_rice, pi_tables};
60
61/// Returns the length of the π code with parameter `k` for `n`.
62#[must_use]
63#[inline(always)]
64#[allow(clippy::collapsible_if)]
65pub const fn len_pi_param<const USE_TABLE: bool>(mut n: u64, k: usize) -> usize {
66    debug_assert!(k < 64);
67    if USE_TABLE {
68        if k == pi_tables::K {
69            // We cannot use .get() here because n is a u64
70            if n < pi_tables::LEN.len() as u64 {
71                return pi_tables::LEN[n as usize] as usize;
72            }
73        }
74    }
75    debug_assert!(n < u64::MAX);
76    n += 1;
77    let λ = n.ilog2() as usize;
78    len_rice(λ as u64, k) + λ
79}
80
81/// Returns the length of the π code for `n` with parameter `k` using
82/// a default value for `USE_TABLE`.
83#[must_use]
84#[inline(always)]
85pub const fn len_pi(n: u64, k: usize) -> usize {
86    len_pi_param::<true>(n, k)
87}
88
89/// Trait for reading π codes.
90///
91/// This is the trait you should usually pull into scope to read π codes.
92pub trait PiRead<E: Endianness>: BitRead<E> {
93    fn read_pi(&mut self, k: usize) -> Result<u64, Self::Error>;
94    fn read_pi2(&mut self) -> Result<u64, Self::Error>;
95}
96
97/// Parametric trait for reading π codes.
98///
99/// This trait is more general than [`PiRead`], as it makes it possible
100/// to specify how to use tables using const parameters.
101///
102/// We provide an implementation of this trait for [`BitRead`]. An implementation
103/// of [`PiRead`] using default values is usually provided exploiting the
104/// [`crate::codes::params::ReadParams`] mechanism.
105pub trait PiReadParam<E: Endianness>: BitRead<E> {
106    fn read_pi_param(&mut self, k: usize) -> Result<u64, Self::Error>;
107    fn read_pi2_param<const USE_TABLE: bool>(&mut self) -> Result<u64, Self::Error>;
108}
109
110impl<B: BitRead<BE>> PiReadParam<BE> for B {
111    #[inline(always)]
112    fn read_pi_param(&mut self, k: usize) -> Result<u64, B::Error> {
113        default_read_pi(self, k)
114    }
115
116    #[inline(always)]
117    fn read_pi2_param<const USE_TABLE: bool>(&mut self) -> Result<u64, B::Error> {
118        const {
119            if USE_TABLE {
120                pi_tables::check_read_table(B::PEEK_BITS)
121            }
122        }
123        if USE_TABLE {
124            let (len_with_flag, value_or_lambda) = pi_tables::read_table_be(self);
125            if len_with_flag > 0 {
126                // Complete code - bits already skipped in read_table
127                return Ok(value_or_lambda);
128            } else if len_with_flag < 0 {
129                // Partial code: rice decoded, need to read fixed part
130                // Bits already skipped in read_table
131                let λ = value_or_lambda;
132                debug_assert!(λ < 64);
133                return Ok((1 << λ) + self.read_bits(λ as usize)? - 1);
134            }
135            // len_with_flag == 0: no valid decoding, fall through
136        }
137        default_read_pi(self, 2)
138    }
139}
140
141impl<B: BitRead<LE>> PiReadParam<LE> for B {
142    #[inline(always)]
143    fn read_pi_param(&mut self, k: usize) -> Result<u64, B::Error> {
144        default_read_pi(self, k)
145    }
146
147    #[inline(always)]
148    fn read_pi2_param<const USE_TABLE: bool>(&mut self) -> Result<u64, B::Error> {
149        const {
150            if USE_TABLE {
151                pi_tables::check_read_table(B::PEEK_BITS)
152            }
153        }
154        if USE_TABLE {
155            let (len_with_flag, value_or_lambda) = pi_tables::read_table_le(self);
156            if len_with_flag > 0 {
157                // Complete code - bits already skipped in read_table
158                return Ok(value_or_lambda);
159            } else if len_with_flag < 0 {
160                // Partial code: rice decoded, need to read fixed part
161                // Bits already skipped in read_table
162                let λ = value_or_lambda;
163                debug_assert!(λ < 64);
164                return Ok((1 << λ) + self.read_bits(λ as usize)? - 1);
165            }
166            // len_with_flag == 0: no valid decoding, fall through
167        }
168        default_read_pi(self, 2)
169    }
170}
171
172/// Default, internal non-table based implementation that works
173/// for any endianness.
174#[inline(always)]
175fn default_read_pi<E: Endianness, B: BitRead<E>>(
176    backend: &mut B,
177    k: usize,
178) -> Result<u64, B::Error> {
179    debug_assert!(k < 64);
180    let λ = backend.read_rice(k)?;
181    debug_assert!(λ < 64);
182    Ok((1 << λ) + backend.read_bits(λ as usize)? - 1)
183}
184
185/// Trait for writing π codes.
186///
187/// This is the trait you should usually pull into scope to write π codes.
188pub trait PiWrite<E: Endianness>: BitWrite<E> {
189    fn write_pi(&mut self, n: u64, k: usize) -> Result<usize, Self::Error>;
190    fn write_pi2(&mut self, n: u64) -> Result<usize, Self::Error>;
191}
192
193/// Parametric trait for writing π codes.
194///
195/// This trait is more general than [`PiWrite`], as it makes it possible
196/// to specify how to use tables using const parameters.
197///
198/// We provide an implementation of this trait for [`BitWrite`]. An implementation
199/// of [`PiWrite`] using default values is usually provided exploiting the
200/// [`crate::codes::params::WriteParams`] mechanism.
201pub trait PiWriteParam<E: Endianness>: BitWrite<E> {
202    fn write_pi_param(&mut self, n: u64, k: usize) -> Result<usize, Self::Error>;
203    fn write_pi2_param<const USE_TABLE: bool>(&mut self, n: u64) -> Result<usize, Self::Error>;
204}
205
206impl<B: BitWrite<BE>> PiWriteParam<BE> for B {
207    #[inline(always)]
208    fn write_pi_param(&mut self, n: u64, k: usize) -> Result<usize, Self::Error> {
209        default_write_pi(self, n, k)
210    }
211
212    #[inline(always)]
213    #[allow(clippy::collapsible_if)]
214    fn write_pi2_param<const USE_TABLE: bool>(&mut self, n: u64) -> Result<usize, Self::Error> {
215        if USE_TABLE {
216            if let Some(len) = pi_tables::write_table_be(self, n)? {
217                return Ok(len);
218            }
219        }
220        default_write_pi(self, n, 2)
221    }
222}
223
224impl<B: BitWrite<LE>> PiWriteParam<LE> for B {
225    #[inline(always)]
226    fn write_pi_param(&mut self, n: u64, k: usize) -> Result<usize, Self::Error> {
227        default_write_pi(self, n, k)
228    }
229
230    #[inline(always)]
231    #[allow(clippy::collapsible_if)]
232    fn write_pi2_param<const USE_TABLE: bool>(&mut self, n: u64) -> Result<usize, Self::Error> {
233        if USE_TABLE {
234            if let Some(len) = pi_tables::write_table_le(self, n)? {
235                return Ok(len);
236            }
237        }
238        default_write_pi(self, n, 2)
239    }
240}
241
242/// Default, internal non-table based implementation that works
243/// for any endianness.
244#[inline(always)]
245fn default_write_pi<E: Endianness, B: BitWrite<E>>(
246    backend: &mut B,
247    mut n: u64,
248    k: usize,
249) -> Result<usize, B::Error> {
250    debug_assert!(k < 64);
251    debug_assert!(n < u64::MAX);
252
253    n += 1;
254    let λ = n.ilog2() as usize;
255
256    #[cfg(feature = "checks")]
257    {
258        // Clean up n in case checks are enabled
259        n ^= 1 << λ;
260    }
261
262    Ok(backend.write_rice(λ as u64, k)? + backend.write_bits(n, λ)?)
263}
264
265#[cfg(test)]
266mod tests {
267    use crate::{
268        codes::pi::{PiReadParam, PiWriteParam},
269        prelude::*,
270    };
271
272    #[test]
273    fn test_roundtrip() {
274        let k = 3;
275        for value in (0..64).map(|i| 1 << i).chain(0..1024).chain([u64::MAX - 1]) {
276            let mut data = [0_u64; 10];
277            let mut writer = <BufBitWriter<BE, _>>::new(MemWordWriterSlice::new(&mut data));
278            let code_len = writer.write_pi(value, k).unwrap();
279            assert_eq!(code_len, len_pi(value, k));
280            drop(writer);
281            let mut reader = <BufBitReader<BE, _>>::new(MemWordReader::new_inf(&data));
282            assert_eq!(
283                reader.read_pi(k).unwrap(),
284                value,
285                "for value: {} with k {}",
286                value,
287                k
288            );
289        }
290    }
291
292    #[test]
293    fn test_roundtrip_pi2() {
294        // Test the specialized pi2 methods
295        for value in (0..64).map(|i| 1 << i).chain(0..1024).chain([u64::MAX - 1]) {
296            // Test BE
297            let mut data = [0_u64; 10];
298            let mut writer = <BufBitWriter<BE, _>>::new(MemWordWriterSlice::new(&mut data));
299            let code_len = writer.write_pi2(value).unwrap();
300            assert_eq!(code_len, len_pi(value, 2));
301            drop(writer);
302            let mut reader = <BufBitReader<BE, _>>::new(MemWordReader::new_inf(&data));
303            assert_eq!(reader.read_pi2().unwrap(), value, "BE for value: {}", value,);
304
305            // Test LE
306            let mut data = [0_u64; 10];
307            let mut writer = <BufBitWriter<LE, _>>::new(MemWordWriterSlice::new(&mut data));
308            let code_len = writer.write_pi2(value).unwrap();
309            assert_eq!(code_len, len_pi(value, 2));
310            drop(writer);
311            let mut reader = <BufBitReader<LE, _>>::new(MemWordReader::new_inf(&data));
312            assert_eq!(reader.read_pi2().unwrap(), value, "LE for value: {}", value,);
313        }
314    }
315
316    #[test]
317    fn test_roundtrip_pi2_param() {
318        // Test the parametric pi2 methods with tables
319        for value in (0..64).map(|i| 1 << i).chain(0..1024).chain([u64::MAX - 1]) {
320            // Test BE with tables
321            let mut data = [0_u64; 10];
322            let mut writer = <BufBitWriter<BE, _>>::new(MemWordWriterSlice::new(&mut data));
323            let code_len = writer.write_pi2_param::<true>(value).unwrap();
324            assert_eq!(code_len, len_pi(value, 2));
325            drop(writer);
326            let mut reader = <BufBitReader<BE, _>>::new(MemWordReader::new_inf(&data));
327            assert_eq!(
328                reader.read_pi2_param::<true>().unwrap(),
329                value,
330                "BE table for value: {}",
331                value,
332            );
333
334            // Test BE without tables
335            let mut data = [0_u64; 10];
336            let mut writer = <BufBitWriter<BE, _>>::new(MemWordWriterSlice::new(&mut data));
337            let code_len = writer.write_pi2_param::<false>(value).unwrap();
338            assert_eq!(code_len, len_pi(value, 2));
339            drop(writer);
340            let mut reader = <BufBitReader<BE, _>>::new(MemWordReader::new_inf(&data));
341            assert_eq!(
342                reader.read_pi2_param::<false>().unwrap(),
343                value,
344                "BE no table for value: {}",
345                value,
346            );
347
348            // Test LE with tables
349            let mut data = [0_u64; 10];
350            let mut writer = <BufBitWriter<LE, _>>::new(MemWordWriterSlice::new(&mut data));
351            let code_len = writer.write_pi2_param::<true>(value).unwrap();
352            assert_eq!(code_len, len_pi(value, 2));
353            drop(writer);
354            let mut reader = <BufBitReader<LE, _>>::new(MemWordReader::new_inf(&data));
355            assert_eq!(
356                reader.read_pi2_param::<true>().unwrap(),
357                value,
358                "LE table for value: {}",
359                value,
360            );
361
362            // Test LE without tables
363            let mut data = [0_u64; 10];
364            let mut writer = <BufBitWriter<LE, _>>::new(MemWordWriterSlice::new(&mut data));
365            let code_len = writer.write_pi2_param::<false>(value).unwrap();
366            assert_eq!(code_len, len_pi(value, 2));
367            drop(writer);
368            let mut reader = <BufBitReader<LE, _>>::new(MemWordReader::new_inf(&data));
369            assert_eq!(
370                reader.read_pi2_param::<false>().unwrap(),
371                value,
372                "LE no table for value: {}",
373                value,
374            );
375        }
376    }
377
378    #[test]
379    #[allow(clippy::unusual_byte_groupings)]
380    fn test_bits() {
381        for (k, value, expected) in [
382            (2, 20, 0b01_00_0101 << (64 - 8)),
383            (2, 0, 0b100 << (64 - 3)),
384            (2, 1, 0b1010 << (64 - 4)),
385            (2, 2, 0b1011 << (64 - 4)),
386            (2, 3, 0b1_1000 << (64 - 5)),
387            (2, 4, 0b1_1001 << (64 - 5)),
388            (2, 5, 0b1_1010 << (64 - 5)),
389            (2, 6, 0b1_1011 << (64 - 5)),
390            (2, 7, 0b11_1000 << (64 - 6)),
391            (3, 0, 0b1000 << (64 - 4)),
392            (3, 1, 0b1_0010 << (64 - 5)),
393            (3, 2, 0b1_0011 << (64 - 5)),
394            (3, 3, 0b1_01000 << (64 - 6)),
395            (3, 4, 0b1_01001 << (64 - 6)),
396            (3, 5, 0b1_01010 << (64 - 6)),
397            (3, 6, 0b1_01011 << (64 - 6)),
398            (3, 7, 0b101_1000 << (64 - 7)),
399        ] {
400            let mut data = [0_u64; 10];
401            let mut writer = <BufBitWriter<BE, _>>::new(MemWordWriterSlice::new(&mut data));
402            let code_len = writer.write_pi(value, k).unwrap();
403            assert_eq!(code_len, len_pi(value, k));
404            drop(writer);
405            assert_eq!(
406                data[0].to_be(),
407                expected,
408                "\nfor value: {} with k {}\ngot: {:064b}\nexp: {:064b}\ngot_len: {} exp_len: {}\n",
409                value,
410                k,
411                data[0].to_be(),
412                expected,
413                code_len,
414                len_pi(value, k),
415            );
416        }
417    }
418
419    #[test]
420    fn test_against_zeta() {
421        // BE: π₀ = ζ₁ and π₁ = ζ₂
422        for k in 0..2 {
423            for value in 0..100 {
424                let mut data_pi = [0_u64; 10];
425                let mut data_zeta = [0_u64; 10];
426
427                let mut writer = <BufBitWriter<BE, _>>::new(MemWordWriterSlice::new(&mut data_pi));
428                let code_len = writer.write_pi(value, k).unwrap();
429                assert_eq!(code_len, len_pi(value, k));
430                drop(writer);
431
432                let mut writer =
433                    <BufBitWriter<BE, _>>::new(MemWordWriterSlice::new(&mut data_zeta));
434                let code_len = writer.write_zeta(value, 1 << k).unwrap();
435                assert_eq!(code_len, len_zeta(value, 1 << k));
436                drop(writer);
437
438                assert_eq!(data_pi[0], data_zeta[0]);
439            }
440        }
441
442        // LE: π₀ = ζ₁; π₁ and ζ₂ have the same lengths but permuted bits
443        for value in 0..100 {
444            let mut data_pi = [0_u64; 10];
445            let mut data_zeta = [0_u64; 10];
446
447            let mut writer = <BufBitWriter<LE, _>>::new(MemWordWriterSlice::new(&mut data_pi));
448            let code_len = writer.write_pi(value, 0).unwrap();
449            assert_eq!(code_len, len_pi(value, 0));
450            drop(writer);
451
452            let mut writer = <BufBitWriter<LE, _>>::new(MemWordWriterSlice::new(&mut data_zeta));
453            let code_len = writer.write_zeta(value, 1).unwrap();
454            assert_eq!(code_len, len_zeta(value, 1));
455            drop(writer);
456
457            assert_eq!(data_pi[0], data_zeta[0]);
458        }
459
460        for value in 0..100 {
461            assert_eq!(len_pi(value, 1), len_zeta(value, 2));
462        }
463    }
464}