Skip to main content

p3_binary_dft/
encoder.rs

1//! Reed–Solomon encoder over the additive NTT domain.
2
3use core::marker::PhantomData;
4
5use p3_binary_field::{
6    BinaryField8, BinaryField16, BinaryField32, BinaryField64, BinaryField128, Poly64, TowerLevel,
7};
8use p3_commit::Encoder;
9use p3_matrix::Matrix;
10use p3_matrix::dense::RowMajorMatrix;
11use p3_util::log2_strict_usize;
12
13use crate::lch::LchNtt;
14use crate::poly::PolyBasisNtt;
15use crate::traits::AdditiveNtt;
16
17/// The length a message grows to once its coefficients are zero-padded to the target rate.
18///
19/// # Panics
20///
21/// Panics if that length does not fit the address space.
22pub(crate) fn padded_message_len(len: usize, log_inv_rate: usize) -> usize {
23    // A shift amount below the word size still leaves the value itself free to overflow.
24    // Recovering the original length from the shifted one is what proves no bits were lost.
25    u32::try_from(log_inv_rate)
26        .ok()
27        .and_then(|rate| len.checked_shl(rate))
28        .filter(|&padded| padded >> log_inv_rate == len)
29        .expect("codeword length overflows usize")
30}
31
32/// Reed–Solomon over the additive NTT domain.
33///
34/// The message holds the low-index novel-basis coefficients of each column, so the codeword is
35/// the evaluation of `f̂(Ŵ_0(x), …, Ŵ_{k−1}(x))` on `S_{k + log_inv_rate}`.
36///
37/// The transform is a type parameter, because the fastest one differs by alphabet.
38/// The default is the one the widest tower level uses.
39#[derive(Clone, Debug, Default)]
40pub struct AdditiveRsEncoder<F, Ntt = PolyBasisNtt> {
41    ntt: Ntt,
42    _marker: PhantomData<F>,
43}
44
45impl<F, Ntt> AdditiveRsEncoder<F, Ntt> {
46    /// Builds an encoder around the given additive NTT.
47    pub const fn new(ntt: Ntt) -> Self {
48        Self {
49            ntt,
50            _marker: PhantomData,
51        }
52    }
53}
54
55/// A tower level this crate encodes over, together with the encoder it picks for that level.
56///
57/// A caller naming a level rather than a transform reaches the right pair through this.
58/// Nothing but the implementations below decides which transform an alphabet gets.
59pub trait EncodableLevel: TowerLevel {
60    /// The Reed-Solomon encoder this crate uses for this level.
61    type Encoder: Encoder<Self> + Default + Sync;
62}
63
64/// The widest level has a polynomial-basis transform.
65/// It falls back to the portable tower transform where the target has no carryless multiply.
66impl EncodableLevel for BinaryField128 {
67    type Encoder = AdditiveRsEncoder<Self, PolyBasisNtt>;
68}
69
70impl EncodableLevel for BinaryField64 {
71    type Encoder = AdditiveRsEncoder<Self, LchNtt<Self>>;
72}
73
74impl EncodableLevel for Poly64 {
75    type Encoder = AdditiveRsEncoder<Self, LchNtt<Self>>;
76}
77
78impl EncodableLevel for BinaryField32 {
79    type Encoder = AdditiveRsEncoder<Self, LchNtt<Self>>;
80}
81
82impl EncodableLevel for BinaryField16 {
83    type Encoder = AdditiveRsEncoder<Self, LchNtt<Self>>;
84}
85
86impl EncodableLevel for BinaryField8 {
87    type Encoder = AdditiveRsEncoder<Self, LchNtt<Self>>;
88}
89
90/// Zero-extend each column's coefficient vector to the codeword length, then transform.
91///
92/// The rate is passed through, so a transform may skip the work the zero tail would do.
93///
94/// # Panics
95///
96/// Panics if the message height is not a power of two, or if the codeword length overflows.
97fn encode_by_padding<F, Ntt>(
98    ntt: &Ntt,
99    mut message: RowMajorMatrix<F>,
100    log_inv_rate: usize,
101) -> RowMajorMatrix<F>
102where
103    F: TowerLevel,
104    Ntt: AdditiveNtt<F> + Sync,
105{
106    if log_inv_rate == 0 {
107        return ntt.ntt_batch(message);
108    }
109
110    // Zero-padding the novel-basis coefficients extends the evaluation domain.
111    let padded_len = padded_message_len(message.values.len(), log_inv_rate);
112    let _ = log2_strict_usize(message.height());
113    message.values.resize(padded_len, F::ZERO);
114    ntt.ntt_batch_padded(message, log_inv_rate)
115}
116
117// One implementation per alphabet, each forwarding to the shared body above.
118//
119// A blanket implementation over every level would overlap the two-adic one in `p3-commit`.
120// No downstream crate may resolve that overlap, so the alphabets are named instead.
121macro_rules! impl_additive_rs_encoder {
122    ($($field:ty),* $(,)?) => {$(
123        impl<Ntt: AdditiveNtt<$field> + Sync> Encoder<$field> for AdditiveRsEncoder<$field, Ntt> {
124            fn encode_batch(
125                &self,
126                message: RowMajorMatrix<$field>,
127                log_inv_rate: usize,
128            ) -> RowMajorMatrix<$field> {
129                encode_by_padding(&self.ntt, message, log_inv_rate)
130            }
131
132            fn encode_batch_padded(
133                &self,
134                message: RowMajorMatrix<$field>,
135                log_inv_rate: usize,
136            ) -> RowMajorMatrix<$field> {
137                self.ntt.ntt_batch_padded(message, log_inv_rate)
138            }
139        }
140    )*};
141}
142
143// The six `impl Encoder<F> for AdditiveRsEncoder<F, _>`, one per alphabet below.
144impl_additive_rs_encoder!(
145    BinaryField128,
146    BinaryField64,
147    Poly64,
148    BinaryField32,
149    BinaryField16,
150    BinaryField8,
151);
152
153#[cfg(test)]
154mod tests {
155    use alloc::vec;
156
157    use p3_binary_field::{BinaryField128, TowerLevel};
158    use p3_commit::Encoder;
159    use p3_field::PrimeCharacteristicRing;
160    use p3_matrix::Matrix;
161    use p3_matrix::dense::RowMajorMatrix;
162    use proptest::prelude::*;
163    use rand::SeedableRng;
164    use rand::rngs::SmallRng;
165
166    use super::{AdditiveRsEncoder, padded_message_len};
167    use crate::naive::NaiveAdditiveNtt;
168    use crate::traits::AdditiveNtt;
169
170    type F = BinaryField128;
171
172    /// Builds a matrix whose entries are distinct functions of the seed and the position.
173    fn matrix(log_n: usize, width: usize, seed: u64) -> RowMajorMatrix<F> {
174        RowMajorMatrix::new(
175            (0..(width << log_n))
176                .map(|i| {
177                    let bits = seed
178                        .wrapping_mul(0x9e37_79b9_7f4a_7c15)
179                        .wrapping_add(i as u64);
180                    F::from_le_byte_iter(bits.to_le_bytes().into_iter().cycle())
181                })
182                .collect(),
183            width,
184        )
185    }
186
187    /// Encoding is zero-padding the novel-basis coefficients and evaluating on the whole domain.
188    #[test]
189    fn encodes_by_padding_and_transforming() {
190        let mut rng = SmallRng::seed_from_u64(1);
191        let message = RowMajorMatrix::<F>::rand(&mut rng, 1 << 5, 3);
192
193        let mut padded = message.clone();
194        padded.values.resize(message.values.len() * 4, F::ZERO);
195        let expected = NaiveAdditiveNtt::<F>::default().ntt_batch(padded);
196
197        let encoded = AdditiveRsEncoder::<F>::default().encode_batch(message, 2);
198        assert_eq!(encoded.height(), 1 << 7);
199        assert_eq!(encoded, expected);
200    }
201
202    /// The codeword restricted to the message-sized prefix is the message's own transform: the
203    /// correspondence Phase 3 folds along.
204    #[test]
205    fn codeword_prefix_is_the_message_transform() {
206        let mut rng = SmallRng::seed_from_u64(2);
207        let message = RowMajorMatrix::<F>::rand(&mut rng, 1 << 5, 2);
208
209        let encoded = AdditiveRsEncoder::<F>::default().encode_batch(message.clone(), 1);
210        let direct = AdditiveRsEncoder::<F>::default().encode_batch(message, 0);
211        assert_eq!(&encoded.values[..direct.values.len()], &direct.values[..]);
212    }
213
214    #[test]
215    #[should_panic = "codeword length overflows usize"]
216    fn encode_batch_panics_when_the_codeword_length_overflows() {
217        let message = RowMajorMatrix::new(vec![F::ZERO; 2], 1);
218        let _ = AdditiveRsEncoder::<F>::default().encode_batch(message, usize::BITS as usize - 1);
219    }
220
221    #[test]
222    fn padded_encoding_matches_naive() {
223        for width in [1, 4, 16, 64] {
224            for rate in [0, 1, 2, 3] {
225                let mut mat = matrix(4, width, 13);
226                mat.values.resize(mat.values.len() << rate, F::ZERO);
227                let expected = NaiveAdditiveNtt::default().ntt_batch(mat.clone());
228                assert_eq!(
229                    AdditiveRsEncoder::<F>::default().encode_batch_padded(mat, rate),
230                    expected
231                );
232            }
233        }
234    }
235
236    #[test]
237    #[should_panic = "padding exceeds matrix height"]
238    fn padded_encoding_rejects_excessive_padding() {
239        let _ = AdditiveRsEncoder::<F>::default().encode_batch_padded(matrix(2, 4, 0), 3);
240    }
241
242    #[test]
243    #[should_panic]
244    fn padded_encoding_rejects_non_power_of_two_height() {
245        let mat = RowMajorMatrix::new(vec![F::ZERO; 12], 4);
246        let _ = AdditiveRsEncoder::<F>::default().encode_batch_padded(mat, 1);
247    }
248
249    #[test]
250    fn the_padded_length_is_the_message_length_shifted() {
251        // Fixture state: a rate of 1/8 multiplies the coefficient count by eight.
252        assert_eq!(padded_message_len(48, 3), 384);
253
254        // No added dimension leaves the message length alone.
255        assert_eq!(padded_message_len(48, 0), 48);
256
257        // An empty message stays empty at every rate.
258        assert_eq!(padded_message_len(0, 60), 0);
259    }
260
261    #[test]
262    #[should_panic = "codeword length overflows usize"]
263    fn the_padded_length_refuses_a_shift_past_the_word_size() {
264        // A shift of the whole word width has no result `usize` can hold.
265        let _ = padded_message_len(2, usize::BITS as usize);
266    }
267
268    #[test]
269    #[should_panic = "codeword length overflows usize"]
270    fn the_padded_length_refuses_a_value_that_overflows() {
271        // The shift amount fits the word, and the shifted value does not.
272        //
273        //     1 << (BITS - 1)  shifted once more drops its only set bit
274        let _ = padded_message_len(1 << (usize::BITS - 1), 1);
275    }
276
277    proptest! {
278        #![proptest_config(ProptestConfig::with_cases(32))]
279
280        /// The coset-wise encoder agrees with zero-padding and transforming, across the sizes
281        /// D8's coset decomposition actually branches on: `log_inv_rate = 0` (no cosets),
282        /// `log_n = 0` (single-row cosets), and ordinary cases in between.
283        #[test]
284        fn encode_batch_matches_padding_and_transforming(
285            log_n in 0usize..=6,
286            log_inv_rate in 0usize..=3,
287            width in 1usize..=4,
288            seed in any::<u64>(),
289        ) {
290            let message = matrix(log_n, width, seed);
291
292            let mut padded = message.clone();
293            padded
294                .values
295                .resize(message.values.len() << log_inv_rate, F::ZERO);
296            let expected = NaiveAdditiveNtt::<F>::default().ntt_batch(padded);
297
298            let encoded = AdditiveRsEncoder::<F>::default().encode_batch(message, log_inv_rate);
299            prop_assert_eq!(encoded, expected);
300        }
301    }
302}