Skip to main content

p3_binary_dft/
domain.rs

1//! The additive NTT domain: `F_2`-linear subspaces spanned by the Cantor basis.
2
3use alloc::vec::Vec;
4
5use p3_binary_field::TowerLevel;
6
7/// The `index`-th point of the additive NTT domain, `Σ_r bit_r(index) · v_r`.
8///
9/// The domain `S_ℓ` is the span of the first `ℓ` Cantor basis vectors, so this is well
10/// defined for any `ℓ > log2(index)` and does not depend on which level evaluates it.
11///
12/// # Panics
13/// Panics if `index` has a set bit at or above the bit width of `F`.
14#[must_use]
15pub fn domain_point<F: TowerLevel>(index: usize) -> F {
16    let mut point = F::ZERO;
17    let mut remaining = index;
18    let mut r = 0;
19    while remaining != 0 {
20        if remaining & 1 == 1 {
21            point += F::cantor_basis(r);
22        }
23        remaining >>= 1;
24        r += 1;
25    }
26    point
27}
28
29/// The increments of `domain_point(index << 1)` as `index` runs upwards from zero.
30///
31/// `domain_point` is `F_2`-linear in its index and `(index − 1) ^ index` is the mask of bits
32/// `0..=k` for `k = index.trailing_zeros()`, so consecutive points differ by
33/// `steps[k] = Σ_{r ≤ k} v_{r+1}`. The `count` entries returned cover every `index` below
34/// `2^count`.
35///
36/// # Panics
37/// Panics if `count` is at least the bit width of `F`.
38#[must_use]
39pub fn domain_point_steps<F: TowerLevel>(count: usize) -> Vec<F> {
40    let mut point = F::ZERO;
41    (0..count)
42        .map(|r| {
43            point += F::cantor_basis(r + 1);
44            point
45        })
46        .collect()
47}
48
49/// The subspace polynomial `W_j` of `S_j`, defined by `W_0(x) = x` and
50/// `W_j(x) = W_{j−1}(x)² + W_{j−1}(x)`.
51///
52/// `W_j` is `F_2`-linear, vanishes exactly on `S_j`, and satisfies `W_j(v_j) = v_0 = 1`, so it
53/// is already the normalised subspace polynomial `Ŵ_j` for the Cantor basis (D8).
54#[must_use]
55pub fn subspace_polynomial<F: TowerLevel>(j: usize, x: F) -> F {
56    let mut value = x;
57    for _ in 0..j {
58        value = value.square() + value;
59    }
60    value
61}
62
63#[cfg(test)]
64mod tests {
65    use p3_binary_field::{BinaryField8, BinaryField16, BinaryField128, TowerLevel};
66    use p3_field::PrimeCharacteristicRing;
67
68    use super::{domain_point, domain_point_steps, subspace_polynomial};
69
70    /// Chaining the increments of a stage reproduces the points a full walk reaches.
71    fn check_steps_chain_to_the_walk<F: TowerLevel>(count: usize) {
72        let steps = domain_point_steps::<F>(count);
73        let mut point = F::ZERO;
74        assert_eq!(point, domain_point::<F>(0));
75        for index in 1..1usize << count {
76            point += steps[index.trailing_zeros() as usize];
77            assert_eq!(point, domain_point::<F>(index << 1), "index={index}");
78        }
79    }
80
81    /// The image of every single-bit index is the matching Cantor basis vector, which pins the
82    /// basis itself and not merely some graded `F_2`-linear reparametrisation of it.
83    #[test]
84    fn domain_point_images_are_the_cantor_basis() {
85        for r in 0..8 {
86            assert_eq!(
87                domain_point::<BinaryField8>(1 << r),
88                BinaryField8::cantor_basis(r),
89                "r={r}"
90            );
91        }
92        for r in 0..usize::BITS as usize {
93            assert_eq!(
94                domain_point::<BinaryField128>(1 << r),
95                BinaryField128::cantor_basis(r),
96                "r={r}"
97            );
98        }
99    }
100
101    /// An index reaching past the basis of its level is rejected rather than silently masked.
102    #[test]
103    #[should_panic = "Cantor basis index out of range"]
104    fn domain_point_rejects_an_index_past_the_basis() {
105        let _ = domain_point::<BinaryField8>(1 << 8);
106    }
107
108    /// `domain_point` is `F_2`-linear in its index.
109    #[test]
110    fn domain_point_is_linear_in_the_index() {
111        for a in 0..1usize << 8 {
112            for b in 0..1usize << 8 {
113                assert_eq!(
114                    domain_point::<BinaryField8>(a ^ b),
115                    domain_point::<BinaryField8>(a) + domain_point::<BinaryField8>(b)
116                );
117            }
118        }
119    }
120
121    /// The increments are what a transform adds to a twiddle in place of walking the block
122    /// index, so they have to agree with that walk at every index, not merely at the ends.
123    #[test]
124    fn domain_point_steps_chain_to_the_even_domain_points() {
125        check_steps_chain_to_the_walk::<BinaryField8>(7);
126        check_steps_chain_to_the_walk::<BinaryField16>(10);
127        check_steps_chain_to_the_walk::<BinaryField128>(12);
128    }
129
130    /// `W_j` vanishes exactly on `S_j`, the span of the first `j` basis vectors.
131    #[test]
132    fn subspace_polynomial_vanishes_on_its_subspace() {
133        for j in 0..=8 {
134            for m in 0..1usize << 8 {
135                let value = subspace_polynomial::<BinaryField8>(j, domain_point(m));
136                assert_eq!(value == BinaryField8::ZERO, m < 1 << j, "j={j} m={m}");
137            }
138        }
139    }
140
141    /// D8's index-shift identity: `W_j(v_i) = v_{i−j}` for `i ≥ j`.
142    #[test]
143    fn subspace_polynomial_shifts_the_basis() {
144        for j in 0..16 {
145            for i in j..16 {
146                assert_eq!(
147                    subspace_polynomial::<BinaryField16>(j, BinaryField16::cantor_basis(i)),
148                    BinaryField16::cantor_basis(i - j),
149                    "j={j} i={i}"
150                );
151            }
152        }
153    }
154
155    /// A point of the domain does not depend on the level it is computed at.
156    #[test]
157    fn domain_points_agree_across_levels() {
158        for m in 0..1usize << 8 {
159            let small: u128 = domain_point::<BinaryField8>(m).to_repr().into();
160            let large: u128 = domain_point::<BinaryField128>(m).to_repr();
161            assert_eq!(small, large, "index {m}");
162        }
163    }
164}