Skip to main content

p3_circle/
point.rs

1use alloc::vec::Vec;
2use core::ops::{Add, AddAssign, Mul, Neg, Sub};
3
4use p3_field::extension::ComplexExtendable;
5use p3_field::{
6    ExtensionField, Field, PackedValue, PrimeCharacteristicRing, batch_multiplicative_inverse,
7};
8use p3_maybe_rayon::prelude::*;
9
10/// Affine representation of a point on the circle.
11/// x^2 + y^2 == 1
12// _private is to prevent construction so we can debug assert the invariant
13#[allow(clippy::manual_non_exhaustive)]
14#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)]
15pub struct Point<F> {
16    pub x: F,
17    pub y: F,
18    _private: (),
19}
20
21impl<F: Field> Point<F> {
22    #[inline]
23    pub fn new(x: F, y: F) -> Self {
24        debug_assert_eq!(x.square() + y.square(), F::ONE);
25        Self { x, y, _private: () }
26    }
27
28    const ZERO: Self = Self {
29        x: F::ONE,
30        y: F::ZERO,
31        _private: (),
32    };
33
34    /// Circle STARKs, Section 3, Lemma 1: (page 4 of the first revision PDF)
35    /// ```ignore
36    /// (x, y) = ((1-t^2)/(1+t^2), 2t/(1+t^2))
37    /// ```
38    /// Panics if t^2 = -1, corresponding to either of the points at infinity
39    /// (on the projective *circle*) (1 : ±i : 0)
40    pub fn from_projective_line(t: F) -> Self {
41        let t2 = t.square();
42        let inv_denom = (F::ONE + t2).try_inverse().expect("t^2 = -1");
43        Self::new((F::ONE - t2) * inv_denom, t.double() * inv_denom)
44    }
45
46    /// Circle STARKs, Section 3, Lemma 1: (page 4 of the first revision PDF)
47    /// ```ignore
48    /// t = y / (x + 1)
49    /// ```
50    /// Returns None if self.x = -1, corresponding to Inf on the projective line
51    ///
52    /// This is also used as a selector polynomial, with a simple zero at (1,0)
53    /// and a simple pole at (-1,0), which in the paper is called v_0
54    /// Circle STARKs, Section 5.1, Lemma 11 (page 21 of the first revision PDF)
55    pub fn to_projective_line(self) -> Option<F> {
56        (self.x + F::ONE).try_inverse().map(|x| x * self.y)
57    }
58
59    /// The "squaring map", or doubling in additive notation, denoted π(x,y)
60    /// Circle STARKs, Section 3.1, Equation 1: (page 5 of the first revision PDF)
61    pub fn double(self) -> Self {
62        Self::new(self.x.square().double() - F::ONE, self.x.double() * self.y)
63    }
64
65    /// Apply the doubling map `n` times: π^n(x,y)
66    pub fn repeated_double(mut self, n: usize) -> Self {
67        for _ in 0..n {
68            self = self.double();
69        }
70        self
71    }
72
73    /// Evaluate the vanishing polynomial for the standard position coset of size 2^log_n
74    /// at this point
75    /// Circle STARKs, Section 3.3, Equation 8 (page 10 of the first revision PDF)
76    pub fn v_n(mut self, log_n: usize) -> F {
77        debug_assert!(log_n >= 1, "v_n requires log_n >= 1");
78        for _ in 0..log_n.saturating_sub(1) {
79            self.x = self.x.square().double() - F::ONE; // TODO: replace this by a custom field impl.
80        }
81        self.x
82    }
83
84    /// Compute a product of successive `v_n`'s.
85    ///
86    /// More explicitly this computes `(1..log_n).map(|i| self.v_n(i)).product()`
87    /// but uses far fewer `self.x.square().double() - F::ONE` steps compared to the naive implementation.
88    pub fn v_n_prod(mut self, log_n: usize) -> F {
89        if log_n <= 1 {
90            return F::ONE;
91        }
92        let mut output = self.x;
93        for _ in 0..(log_n - 2) {
94            self.x = self.x.square().double() - F::ONE; // TODO: replace this by a custom field impl.
95            output *= self.x;
96        }
97        output
98    }
99
100    /// Evaluate the selector function which is zero at `self` and nonzero elsewhere, at `at`.
101    /// Called v_0 . T_p⁻¹ or ṽ_p(x,y) in the paper, used for constraint selectors.
102    /// Panics if p = -self, the pole.
103    /// Section 5.1, Lemma 11 of Circle Starks (page 21 of first edition PDF)
104    pub fn v_tilde_p<EF: ExtensionField<F>>(self, at: Point<EF>) -> EF {
105        (at - self).to_projective_line().unwrap()
106    }
107
108    /// The concrete value of the selector s_P = v_n / (v_0 . T_p⁻¹) at P=self, used for normalization.
109    /// Circle STARKs, Section 5.1, Remark 16 (page 22 of the first revision PDF)
110    pub fn s_p_at_p(self, log_n: usize) -> F {
111        debug_assert!(log_n >= 1, "s_p_at_p requires log_n >= 1");
112        -self.v_n_prod(log_n).mul_2exp_u64((2 * log_n - 1) as u64) * self.y
113    }
114
115    /// Evaluate the alternate single-point vanishing function v_p(x), used for DEEP quotient.
116    /// Returns (a, b), representing the complex number a + bi.
117    /// Simple zero at p, simple pole at +-infinity.
118    /// Circle STARKs, Section 3.3, Equation 11 (page 11 of the first edition PDF).
119    pub fn v_p<EF: ExtensionField<F>>(self, at: Point<EF>) -> (EF, EF) {
120        let diff = -at + self;
121        (EF::ONE - diff.x, -diff.y)
122    }
123}
124
125/// Compute (ṽ_P(x,y) * s_p)^{-1} for each element in the list.
126///
127/// All denominators share a single batch inversion instead of one inversion per point.
128pub(crate) fn compute_lagrange_den_batched<F: Field, EF: ExtensionField<F>>(
129    points: &[Point<F>],
130    at: Point<EF>,
131    log_n: usize,
132) -> Vec<EF> {
133    // Selector normalization `s_p` for every point, computed packed.
134    let s_p = {
135        let mut s_p = F::zero_vec(points.len());
136
137        if log_n < 2 {
138            // The squaring chain is empty, so the packed path buys nothing.
139            for (slot, p) in s_p.iter_mut().zip(points) {
140                *slot = p.s_p_at_p(log_n);
141            }
142        } else {
143            // Power-of-two scaling and chain length, shared by every lane.
144            let exp = (2 * log_n - 1) as u64;
145            let iters = log_n - 2;
146            let width = F::Packing::WIDTH;
147            let packed_len = (points.len() / width) * width;
148
149            s_p[..packed_len]
150                .par_chunks_exact_mut(width)
151                .zip(points.par_chunks_exact(width))
152                .for_each(|(slots, chunk)| {
153                    // Seed the running product with the x-coordinates of the lane.
154                    let mut cur = F::Packing::from_fn(|l| chunk[l].x);
155                    let mut output = cur;
156
157                    // Fold in each squaring-chain step `x -> 2 x^2 - 1`.
158                    for _ in 0..iters {
159                        cur = cur.square().double() - F::Packing::ONE;
160                        output *= cur;
161                    }
162
163                    // Close the formula: scale by the power of two and the y-coordinate.
164                    let ys = F::Packing::from_fn(|l| chunk[l].y);
165                    let packed_s_p = -(output.mul_2exp_u64(exp) * ys);
166
167                    slots.copy_from_slice(packed_s_p.as_slice());
168                });
169
170            // Trailing points below one full lane fall back to the scalar formula.
171            for (slot, &pt) in s_p[packed_len..].iter_mut().zip(&points[packed_len..]) {
172                *slot = pt.s_p_at_p(log_n);
173            }
174        }
175        s_p
176    };
177
178    // Pair each numerator with its denominator before inverting.
179    let (numer, denom): (Vec<_>, Vec<_>) = points
180        .par_iter()
181        .zip(&s_p)
182        .map(|(&pt, &s_p)| {
183            let diff = at - pt;
184            let numer = diff.x + F::ONE;
185            let denom = diff.y * s_p;
186            (numer, denom)
187        })
188        .unzip();
189
190    // One inversion covers the whole batch via Montgomery's trick.
191    let inv_d = batch_multiplicative_inverse(&denom);
192
193    // Recombine each numerator with its inverted denominator.
194    numer
195        .par_iter()
196        .zip(inv_d.par_iter())
197        .map(|(&num, &inv_d)| num * inv_d)
198        .collect()
199}
200
201impl<F: ComplexExtendable> Point<F> {
202    pub fn generator(log_n: usize) -> Self {
203        let g = F::circle_two_adic_generator(log_n);
204        Self::new(g.real(), g.imag())
205    }
206}
207
208/// Circle STARKs, Section 3.1, Equation 2: (page 5 of the first revision PDF)
209/// The inverse map J(x,y) = (x,-y)
210impl<F: Field> Neg for Point<F> {
211    type Output = Self;
212    fn neg(mut self) -> Self::Output {
213        self.y = -self.y;
214        self
215    }
216}
217
218impl<F: Field, EF: ExtensionField<F>> Add<Point<F>> for Point<EF> {
219    type Output = Self;
220    fn add(self, rhs: Point<F>) -> Self::Output {
221        Self::new(
222            self.x * rhs.x - self.y * rhs.y,
223            self.x * rhs.y + self.y * rhs.x,
224        )
225    }
226}
227
228impl<F: Field> AddAssign for Point<F> {
229    fn add_assign(&mut self, rhs: Self) {
230        *self = *self + rhs;
231    }
232}
233
234impl<F: Field, EF: ExtensionField<F>> Sub<Point<F>> for Point<EF> {
235    type Output = Self;
236    fn sub(self, rhs: Point<F>) -> Self::Output {
237        Self::new(
238            self.x * rhs.x + self.y * rhs.y,
239            self.y * rhs.x - self.x * rhs.y,
240        )
241    }
242}
243
244impl<F: Field> Mul<usize> for Point<F> {
245    type Output = Self;
246    fn mul(mut self, mut rhs: usize) -> Self::Output {
247        let mut res = Self::ZERO;
248        while rhs != 0 {
249            if rhs & 1 == 1 {
250                res += self;
251            }
252            rhs >>= 1;
253            self = self.double();
254        }
255        res
256    }
257}
258
259#[cfg(test)]
260mod tests {
261    use p3_field::extension::BinomialExtensionField;
262    use p3_mersenne_31::Mersenne31;
263    use proptest::prelude::*;
264    use rand::rngs::SmallRng;
265    use rand::{RngExt, SeedableRng};
266
267    use super::*;
268
269    type F = Mersenne31;
270    type EF = BinomialExtensionField<F, 3>;
271    type Pt = Point<F>;
272
273    #[test]
274    fn test_arithmetic() {
275        let one = Pt::generator(3);
276        assert_eq!(one - one, Pt::ZERO);
277        assert_eq!(one + one, one * 2);
278        assert_eq!(one + one + one, one * 3);
279        assert_eq!(one * 7, -one);
280        assert_eq!(one * 8, Pt::ZERO);
281
282        let generator = Pt::generator(10);
283        let log_n = 10;
284        let vn_prod_gen = (1..log_n).map(|i| generator.v_n(i)).product();
285        assert_eq!(generator.v_n_prod(log_n), vn_prod_gen);
286    }
287
288    #[cfg(debug_assertions)]
289    #[test]
290    #[should_panic(expected = "v_n requires log_n >= 1")]
291    fn test_v_n_underflow_log_n_0() {
292        let p = Pt::generator(3);
293        let _ = p.v_n(0);
294    }
295
296    #[cfg(debug_assertions)]
297    #[test]
298    #[should_panic(expected = "s_p_at_p requires log_n >= 1")]
299    fn test_s_p_at_p_underflow_log_n_0() {
300        let p = Pt::generator(3);
301        let _ = p.s_p_at_p(0);
302    }
303
304    /// Independent reference: the pre-batched formulation, one inversion per point.
305    fn lagrange_den_scalar(points: &[Pt], at: Point<EF>, log_n: usize) -> Vec<EF> {
306        points
307            .iter()
308            .map(|&pt| {
309                let diff = at - pt;
310                let numer = diff.x + F::ONE;
311                let denom = diff.y * pt.s_p_at_p(log_n);
312                numer * denom.inverse()
313            })
314            .collect()
315    }
316
317    proptest! {
318        #[test]
319        fn compute_lagrange_den_batched_matches_scalar(
320            log_n in 1usize..19,
321            len in 0usize..40,
322            at_seed in any::<u64>(),
323        ) {
324            // A small prefix of real domain points keeps every `s_p` nonzero.
325            let prefix: Vec<Pt> = crate::CircleDomain::standard(log_n).points().take(40).collect();
326            let points = &prefix[..len.min(prefix.len())];
327
328            // A pseudo-random extension point stands in for the out-of-domain query.
329            let mut rng = SmallRng::seed_from_u64(at_seed);
330            let at = Point::<EF>::from_projective_line(rng.random());
331
332            // Discard the measure-zero draws that would invert a zero denominator.
333            let all_invertible = points
334                .iter()
335                .all(|&pt| (at - pt).y * pt.s_p_at_p(log_n) != EF::ZERO);
336            prop_assume!(all_invertible);
337
338            prop_assert_eq!(
339                compute_lagrange_den_batched(points, at, log_n),
340                lagrange_den_scalar(points, at, log_n)
341            );
342        }
343    }
344}