Skip to main content

p3_binary_dft/
subfield.rs

1//! The additive NTT of a message whose entries lie in a byte-aligned tower subfield.
2
3use alloc::vec::Vec;
4
5use p3_binary_field::TowerLevel;
6use p3_matrix::Matrix;
7use p3_matrix::dense::RowMajorMatrix;
8use p3_maybe_rayon::prelude::*;
9use p3_util::log2_strict_usize;
10
11use crate::butterfly::ButterflyField;
12use crate::encoder::padded_message_len;
13use crate::lch::transform_stages;
14
15/// Number of widest butterfly layers a subfield of `2^log_bits` bits is closed under.
16///
17/// - Layer `j` of a height-`2^l` unshifted transform scales by a point of `span(v_1..v_{l-1-j})`.
18/// - A subfield of `b` bits holds the first `b` basis vectors and nothing above them.
19/// - So the layer stays inside it exactly while `l - 1 - j < b`, the top `b` layers.
20const fn closed_layers(log_bits: usize) -> usize {
21    1 << log_bits
22}
23
24/// Bytes a message must carry before running its closed layers narrow pays.
25///
26/// Those layers are a transform of the message alone, at a fraction of the alphabet's width.
27///
28/// A short one is a few microseconds of work behind one thread handoff per layer.
29/// The narrow element size does not save enough to cover those handoffs.
30///
31/// A hundred and twenty-eight kibibytes is the smallest message measured to come out ahead.
32const NARROW_MESSAGE_BYTES: usize = 128 * 1024;
33
34/// Bytes the codeword must carry before the widening pass is worth handing to the machine.
35///
36/// The pass is one read and one write per entry, with no arithmetic between them.
37/// It is therefore limited by memory rather than by cores.
38///
39/// Four mebibytes is the smallest codeword measured to gain from being spread.
40const PARALLEL_WIDEN_BYTES: usize = 4 * 1024 * 1024;
41
42/// Embed every entry into the wider level, in one pass over the data.
43fn widen<S: TowerLevel, F: TowerLevel + From<S>>(values: &[S]) -> Vec<F> {
44    if size_of::<F>() * values.len() < PARALLEL_WIDEN_BYTES {
45        return values.iter().map(|&value| F::from(value)).collect();
46    }
47    values.par_iter().map(|&value| F::from(value)).collect()
48}
49
50/// Evaluate a subfield message with the widest `head` layers run at the narrow element width.
51///
52/// # Algorithm
53///
54/// Write `l` for the domain dimension and `h` for the layers to run narrow.
55/// A row index splits into its top `h` bits and its bottom `l - h` bits:
56///
57/// ```text
58///     row = a * 2^(l-h) + b ,     a < 2^h ,  b < 2^(l-h)
59/// ```
60///
61/// The top `h` layers pair rows that differ in `a` alone, so they leave `b` untouched:
62///
63/// ```text
64///     phase 1   the buffer read as 2^h rows of 2^(l-h) * width columns, transformed whole
65///     phase 2   the remaining l - h layers of the transform the buffer really is
66/// ```
67///
68/// Both phases are unshifted and share block indices, so they agree twiddle for twiddle.
69///
70/// # Panics
71///
72/// Panics if `head` exceeds the layers the subfield is closed under.
73/// Phase 1 would then ask the subfield for a basis vector above its own.
74fn split_ntt_batch<S, F>(mut mat: RowMajorMatrix<S>, head: usize) -> RowMajorMatrix<F>
75where
76    S: ButterflyField,
77    F: ButterflyField + From<S>,
78{
79    let width = mat.width();
80    let log_n = log2_strict_usize(mat.height());
81    assert!(log_n <= 1 << F::LOG_BITS, "domain exceeds field dimension");
82    assert!(
83        head <= closed_layers(S::LOG_BITS).min(log_n),
84        "the subfield is not closed under that many layers"
85    );
86    let low = log_n - head;
87
88    // Phase 1: every layer of a shorter, wider reading of the same buffer.
89    transform_stages::<S, false>(&mut mat.values, width << low, head, S::ZERO);
90
91    let mut values = widen::<S, F>(&mat.values);
92
93    // Phase 2: the layers the subfield is not closed under, at the alphabet's own width.
94    transform_stages::<F, false>(&mut values, width, low, F::ZERO);
95
96    RowMajorMatrix::new(values, width)
97}
98
99/// Evaluate each column of a subfield message on the subspace of the wider level.
100///
101/// Widening the message first pays the wide element size at every layer.
102/// The widest layers do not need it: their twiddles map the subfield into itself.
103///
104/// The layers left over run the tower transform, not whichever backend is fastest wide.
105///
106/// # Panics
107///
108/// Panics if the height is not a power of two.
109/// Panics if the domain dimension exceeds the bit width of the wider level.
110#[must_use]
111pub fn subfield_ntt_batch<S, F>(mat: RowMajorMatrix<S>) -> RowMajorMatrix<F>
112where
113    S: ButterflyField,
114    F: ButterflyField + From<S>,
115{
116    let log_n = log2_strict_usize(mat.height());
117
118    // A message too small to pay for the narrow phase runs no layer narrow.
119    // What is left is the ordinary transform of the widened message.
120    let head = if size_of::<S>() * mat.values.len() >= NARROW_MESSAGE_BYTES {
121        closed_layers(S::LOG_BITS).min(log_n)
122    } else {
123        0
124    };
125    split_ntt_batch(mat, head)
126}
127
128/// Reed–Solomon encode a subfield message over the additive NTT domain.
129///
130/// The message holds the low-index novel-basis coefficients of each column.
131/// The codeword is their evaluation on the subspace `log_inv_rate` dimensions above.
132///
133/// Zero lies in the subfield, so the padding that extends the domain is paid narrow too.
134///
135/// # Panics
136///
137/// Panics under the same conditions as the plain transform.
138/// Panics if the codeword length overflows the address space.
139#[must_use]
140pub fn subfield_encode_batch<S, F>(
141    mut message: RowMajorMatrix<S>,
142    log_inv_rate: usize,
143) -> RowMajorMatrix<F>
144where
145    S: ButterflyField,
146    F: ButterflyField + From<S>,
147{
148    let padded_len = padded_message_len(message.values.len(), log_inv_rate);
149    let _ = log2_strict_usize(message.height());
150
151    message.values.resize(padded_len, S::ZERO);
152    subfield_ntt_batch(message)
153}
154
155#[cfg(test)]
156mod tests {
157    use alloc::format;
158
159    use p3_binary_field::{
160        BinaryField8, BinaryField16, BinaryField32, BinaryField64, BinaryField128, Gf2, TowerLevel,
161    };
162    use p3_field::PrimeCharacteristicRing;
163    use p3_matrix::dense::RowMajorMatrix;
164    use proptest::prelude::*;
165
166    use super::{closed_layers, split_ntt_batch, subfield_encode_batch, subfield_ntt_batch, widen};
167    use crate::butterfly::ButterflyField;
168    use crate::domain::{domain_point, subspace_polynomial};
169    use crate::lch::LchNtt;
170    use crate::naive::NaiveAdditiveNtt;
171    use crate::traits::AdditiveNtt;
172
173    /// Widths covering a single column, an odd row, and a row of several elements.
174    const WIDTHS: [usize; 4] = [1, 3, 4, 16];
175
176    /// A matrix of subfield entries that depend on both the position and the seed.
177    fn matrix<S: TowerLevel>(log_n: usize, width: usize, seed: u64) -> RowMajorMatrix<S> {
178        RowMajorMatrix::new(
179            (0..(width << log_n))
180                .map(|i| {
181                    let bits = seed
182                        .wrapping_mul(0x9e37_79b9_7f4a_7c15)
183                        .wrapping_add(i as u64 + 1)
184                        .wrapping_mul(0xbf58_476d_1ce4_e5b9);
185                    S::from_le_byte_iter(bits.to_le_bytes().into_iter().cycle())
186                })
187                .collect(),
188            width,
189        )
190    }
191
192    /// Every split depth against the unspecialised transform on the widened message.
193    fn check_against_the_wide_transform<S, F>(log_n: usize, width: usize, seed: u64)
194    where
195        S: ButterflyField,
196        F: ButterflyField + From<S>,
197    {
198        let message = matrix::<S>(log_n, width, seed);
199
200        // The reference widens first and runs the ordinary transform.
201        let wide = RowMajorMatrix::new(widen::<S, F>(&message.values), width);
202        let expected = LchNtt::<F>::default().ntt_batch(wide);
203
204        for head in 0..=closed_layers(S::LOG_BITS).min(log_n) {
205            let label = format!("log_n={log_n} width={width} seed={seed} head={head}");
206            assert_eq!(
207                split_ntt_batch::<S, F>(message.clone(), head),
208                expected,
209                "{label}"
210            );
211        }
212
213        // And the entry point itself, at whichever depth its own size rule picks.
214        assert_eq!(subfield_ntt_batch::<S, F>(message), expected);
215    }
216
217    #[test]
218    fn a_byte_message_transforms_like_the_widened_one() {
219        // Heights on both sides of the eight layers a byte subfield is closed under.
220        for width in WIDTHS {
221            for log_n in 0..=10 {
222                check_against_the_wide_transform::<BinaryField8, BinaryField128>(log_n, width, 7);
223                check_against_the_wide_transform::<BinaryField8, BinaryField64>(log_n, width, 11);
224                check_against_the_wide_transform::<BinaryField8, BinaryField32>(log_n, width, 13);
225            }
226        }
227    }
228
229    #[test]
230    fn a_bit_message_transforms_like_the_widened_one() {
231        // A single-bit subfield is closed under one layer, the narrowest split there is.
232        for width in WIDTHS {
233            for log_n in 0..=8 {
234                check_against_the_wide_transform::<Gf2, BinaryField128>(log_n, width, 17);
235            }
236        }
237    }
238
239    #[test]
240    fn a_two_and_four_byte_message_transforms_like_the_widened_one() {
241        // Sixteen and thirty-two closed layers, so the heights here are all inside the head.
242        for width in WIDTHS {
243            for log_n in 0..=10 {
244                check_against_the_wide_transform::<BinaryField16, BinaryField128>(log_n, width, 19);
245                check_against_the_wide_transform::<BinaryField32, BinaryField128>(log_n, width, 23);
246            }
247        }
248    }
249
250    #[test]
251    fn the_widest_subfield_is_the_alphabet_itself() {
252        // Invariant: with nothing left over the head, phase 2 has no layers at all.
253        for log_n in 0..=6 {
254            check_against_the_wide_transform::<BinaryField64, BinaryField64>(log_n, 3, 29);
255        }
256    }
257
258    #[test]
259    #[should_panic = "the subfield is not closed under that many layers"]
260    fn a_split_deeper_than_the_subfield_allows_is_refused() {
261        // A byte subfield is closed under eight layers.
262        // A ninth needs a basis vector it does not have, and would compute another map.
263        let message = matrix::<BinaryField8>(10, 1, 0);
264        let _ = split_ntt_batch::<BinaryField8, BinaryField128>(message, 9);
265    }
266
267    #[test]
268    fn a_message_below_the_narrow_threshold_runs_no_narrow_layer() {
269        // Invariant: the size rule is what decides, and it decides the same way twice.
270        //
271        // Fixture state: 2^6 rows of one byte column is 64 bytes, far below the threshold.
272        let message = matrix::<BinaryField8>(6, 1, 41);
273        assert_eq!(
274            subfield_ntt_batch::<BinaryField8, BinaryField128>(message.clone()),
275            split_ntt_batch::<BinaryField8, BinaryField128>(message, 0)
276        );
277    }
278
279    /// The deepest split of one shape, against the novel basis read from its product definition.
280    fn check_against_the_oracle(log_n: usize, width: usize) {
281        let message = matrix::<BinaryField8>(log_n, width, 31);
282        let wide = RowMajorMatrix::new(widen::<_, BinaryField128>(&message.values), width);
283        let expected = NaiveAdditiveNtt::default().ntt_batch(wide);
284
285        // The deepest split the height allows, which is where phase 1 does the most.
286        let head = closed_layers(BinaryField8::LOG_BITS).min(log_n);
287        assert_eq!(
288            split_ntt_batch::<_, BinaryField128>(message, head),
289            expected,
290            "log_n={log_n} width={width}"
291        );
292    }
293
294    #[test]
295    fn a_byte_message_matches_the_reference_oracle() {
296        // The oracle evaluates the novel basis straight from its product definition.
297        // So it pins the split to that basis, not to another split of the same network.
298        for width in [1usize, 3] {
299            for log_n in 0..=6 {
300                check_against_the_oracle(log_n, width);
301            }
302        }
303    }
304
305    #[test]
306    fn a_byte_message_past_the_closed_layers_matches_the_reference_oracle() {
307        // Invariant: both phases are pinned to the definition, not the narrow phase alone.
308        // A byte subfield is closed under eight layers, so phase 2 is empty below 2^9.
309        //
310        //     log_n = 9   head = 8, phase 2 runs one layer at the wide width
311        //     log_n = 10  head = 8, phase 2 runs two of them
312        for log_n in [9usize, 10] {
313            check_against_the_oracle(log_n, 1);
314        }
315    }
316
317    #[test]
318    fn the_closed_layer_count_is_the_subfield_bit_width() {
319        // Fixture state: the layer count a level's own basis vectors allow.
320        //
321        // - one bit gives one layer
322        // - eight bits give eight layers
323        // - a hundred and twenty-eight bits give that many
324        assert_eq!(closed_layers(Gf2::LOG_BITS), 1);
325        assert_eq!(closed_layers(BinaryField8::LOG_BITS), 8);
326        assert_eq!(closed_layers(BinaryField128::LOG_BITS), 128);
327    }
328
329    #[test]
330    fn the_closed_layers_really_keep_their_twiddles_in_the_subfield() {
331        // Invariant: every twiddle of the top `h` layers lies in the subfield.
332        // Checked against the subspace-polynomial definition, not against the split.
333        const LOG_N: usize = 12;
334        let head = closed_layers(BinaryField8::LOG_BITS);
335
336        for layer in LOG_N - head..LOG_N {
337            // Layer `layer` pairs rows `2^layer` apart, so it has this many blocks.
338            for block in 0..1usize << (LOG_N - 1 - layer) {
339                let t = subspace_polynomial::<BinaryField128>(layer, BinaryField128::ZERO)
340                    + domain_point::<BinaryField128>(block << 1);
341                assert!(
342                    t.to_repr() <= u128::from(u8::MAX),
343                    "layer={layer} block={block}"
344                );
345            }
346        }
347
348        // And the layer just below is where that stops, for this subfield at this height.
349        let t = domain_point::<BinaryField128>(1 << (head - 1) << 1);
350        assert!(
351            t.to_repr() > u128::from(u8::MAX),
352            "the head could be one layer deeper"
353        );
354    }
355
356    #[test]
357    fn encoding_pads_the_coefficients_at_the_narrow_width() {
358        // Fixture state: a 2^4-row message at rate 1/4, so the codeword has 2^6 rows.
359        for width in WIDTHS {
360            for log_inv_rate in 0..=3 {
361                let message = matrix::<BinaryField8>(4, width, 37);
362
363                // The reference pads at the wide width and transforms the whole thing.
364                let mut padded = widen::<_, BinaryField128>(&message.values);
365                padded.resize(padded.len() << log_inv_rate, BinaryField128::ZERO);
366                let expected = LchNtt::<BinaryField128>::default()
367                    .ntt_batch(RowMajorMatrix::new(padded, width));
368
369                let actual = subfield_encode_batch::<_, BinaryField128>(message, log_inv_rate);
370                assert_eq!(actual, expected, "width={width} rate={log_inv_rate}");
371            }
372        }
373    }
374
375    #[test]
376    #[should_panic = "codeword length overflows usize"]
377    fn encoding_rejects_a_codeword_length_past_the_address_space() {
378        let message = matrix::<BinaryField8>(1, 1, 0);
379        let _ = subfield_encode_batch::<_, BinaryField128>(message, usize::BITS as usize - 1);
380    }
381
382    #[test]
383    #[should_panic = "domain exceeds field dimension"]
384    fn a_domain_past_the_alphabet_dimension_is_refused() {
385        // A 2^9-row domain asks for nine Cantor basis vectors, and a byte level has eight.
386        let message = matrix::<BinaryField8>(9, 1, 0);
387        let _ = subfield_ntt_batch::<BinaryField8, BinaryField8>(message);
388    }
389
390    proptest! {
391        #![proptest_config(ProptestConfig::with_cases(32))]
392
393        /// Random heights and widths, across the three interesting subfield widths.
394        #[test]
395        fn random_subfield_messages_transform_like_the_widened_ones(
396            log_n in 0usize..=9,
397            width in 1usize..=5,
398            seed in any::<u64>(),
399        ) {
400            check_against_the_wide_transform::<Gf2, BinaryField128>(log_n, width, seed);
401            check_against_the_wide_transform::<BinaryField8, BinaryField128>(log_n, width, seed);
402            check_against_the_wide_transform::<BinaryField16, BinaryField64>(log_n, width, seed);
403        }
404    }
405}