p3_commit/periodic.rs
1//! Periodic column evaluation support.
2//!
3//! Periodic columns are columns whose values repeat with a period that divides the trace length.
4//! This module provides the `PeriodicEvaluator` trait for evaluating periodic polynomials
5//! in a domain-agnostic way (supporting both two-adic and circle STARKs).
6//!
7//! ## Power-of-Two Requirement
8//!
9//! **All period lengths must be powers of two.** This is because:
10//! - The trace domain is a multiplicative/additive group of order `n` (a power of 2)
11//! - The periodic subdomain must be a subgroup of order `p`
12//! - For `p` to divide `n` as group orders, `p` must also be a power of 2
13//!
14//! ## Mathematical Background
15//!
16//! A periodic column with period `p` and trace length `n` repeats every `p` rows:
17//! `col[i] = col[i + p]` for all `i`.
18//!
19//! **The problem**: We have a polynomial `P` of degree `n-1` over the trace domain `H`,
20//! but it only takes `p` distinct values. Can we work with a smaller polynomial instead?
21//!
22//! **Key observation**: We want `P(ω^i) = P(ω^{i+p})` for all `i`. So we need a map
23//! `π: H → ?` that identifies points `p` apart: `π(ω^i) = π(ω^{i+p})`, i.e., `π` must
24//! be constant on cosets of the subgroup `⟨ω^p⟩` of order `n/p`.
25//!
26//! **Finding π**: For cyclic groups, raising to the power `k` gives a homomorphism with
27//! kernel of size `k`. Since we need `ker(π) = ⟨ω^p⟩` of order `n/p`, we set `π(x) = x^(n/p)`.
28//! Indeed, `π(ω^{i+p}) = ω^{(i+p)·n/p} = ω^{i·n/p} · ω^n = π(ω^i)` since `ω^n = 1`.
29//!
30//! **Where π lands**: The image of `π` is `H_p = {1, ω^(n/p), ω^(2n/p), ...}`, a subgroup
31//! of order `p`. Now we can factor `P = Q ∘ π` where `Q: H_p → F` is a degree `p-1`
32//! polynomial interpolating the `p` periodic values.
33//!
34//! **Group-theoretic view**: `π: H → H_p` is a surjective homomorphism with kernel of
35//! order `n/p`. By the first isomorphism theorem, `H/ker(π) ≅ H_p`. The periodic column
36//! is constant on cosets of `ker(π)`, so it factors through `π`.
37//!
38//! **For Circle STARKs**: The same idea applies with `π(P) = (n/p)·P` (repeated doubling)
39//! instead of exponentiation.
40//!
41//! **Evaluating at an out-of-domain point `ζ`**:
42//! 1. Compute `π(ζ)` to get a point in `H_p`
43//! 2. Evaluate `Q(π(ζ))` using Lagrange interpolation over `H_p`
44//!
45//! ## Memory-Efficient Storage
46//!
47//! Instead of materializing the full LDE-sized table (which would be wasteful for small periods),
48//! we store only `max_period × blowup` rows in a [`PeriodicLdeTable`]. All periodic columns are
49//! padded to the maximum period, creating a rectangular matrix that can be efficiently accessed
50//! with modular indexing in the constraint evaluation hot loop.
51
52use alloc::vec::Vec;
53
54use p3_field::{ExtensionField, Field};
55use p3_matrix::dense::RowMajorMatrix;
56
57use crate::PolynomialSpace;
58
59/// Compact storage for periodic column values on the LDE domain.
60///
61/// Instead of materializing the full LDE-sized table, stores only `extended_height` rows
62/// (where `extended_height = max_period × blowup`) and uses modular indexing to access values.
63///
64/// All periodic columns are padded to the maximum period before extrapolation, creating a
65/// rectangular matrix for cache-friendly row-wise access.
66///
67/// # Invariants
68///
69/// - All periods must be powers of 2 (see module-level documentation)
70/// - Height is always `max_period × blowup` (both powers of 2, so height is power of 2)
71#[derive(Clone, Debug)]
72pub struct PeriodicLdeTable<F> {
73 /// Values in row-major form: height = extended_height, width = num_columns.
74 /// Empty if there are no periodic columns.
75 values: RowMajorMatrix<F>,
76 /// Cached `values.values.len() / values.width` (`0` if `values.width == 0`).
77 /// Guaranteed to be a power of two, so `get` can index with `& (height - 1)`
78 /// instead of `%`.
79 height: usize,
80}
81
82impl<F: Clone + Send + Sync> PeriodicLdeTable<F> {
83 /// Create a new periodic LDE table from extrapolated values.
84 ///
85 /// The matrix should have height = `max_period × blowup` and width = `num_periodic_columns`.
86 pub const fn new(values: RowMajorMatrix<F>) -> Self {
87 let height = match values.values.len().checked_div(values.width) {
88 Some(h) => h,
89 None => 0,
90 };
91 debug_assert!(
92 height == 0 || height.is_power_of_two(),
93 "PeriodicLdeTable height must be a power of two for bitmask indexing"
94 );
95 Self { values, height }
96 }
97
98 /// Create an empty table (for AIRs without periodic columns).
99 pub fn empty() -> Self {
100 Self {
101 values: RowMajorMatrix::new(Vec::new(), 0),
102 height: 0,
103 }
104 }
105
106 /// Returns true if there are no periodic columns.
107 pub const fn is_empty(&self) -> bool {
108 self.values.values.is_empty()
109 }
110
111 /// Number of periodic columns.
112 pub const fn width(&self) -> usize {
113 self.values.width
114 }
115
116 /// Height of the compact table (max_period × blowup).
117 pub const fn height(&self) -> usize {
118 self.height
119 }
120
121 /// Get a specific periodic column value for a given LDE index.
122 #[inline]
123 pub fn get(&self, lde_idx: usize, col_idx: usize) -> &F {
124 let height = self.height;
125 debug_assert!(height > 0, "cannot index into empty periodic table");
126 let row_idx = lde_idx & (height - 1);
127 &self.values.values[row_idx * self.values.width + col_idx]
128 }
129}
130
131/// Evaluates periodic polynomials for a given domain system.
132///
133/// Periodic columns are defined by their values over one period. This trait
134/// handles interpolation and evaluation, abstracting over the domain-specific
135/// math (two-adic multiplicative groups vs circle groups).
136///
137/// # Power-of-Two Requirement
138///
139/// **All period lengths must be powers of two.** This ensures the periodic subdomain
140/// is a valid subgroup of the trace domain. See module-level documentation for details.
141///
142/// # Type Parameters
143/// - `F`: The base field type
144/// - `D`: The polynomial space / domain type
145pub trait PeriodicEvaluator<F: Field, D: PolynomialSpace<Val = F>> {
146 /// Evaluate all periodic columns on the LDE domain, returning a compact table.
147 ///
148 /// This is used by the prover to compute periodic column values on the
149 /// low-degree extension domain for constraint evaluation.
150 ///
151 /// The returned table stores only `max_period × blowup` rows. All columns are
152 /// padded to the maximum period before extrapolation, creating a rectangular
153 /// matrix for efficient row-wise access with modular indexing.
154 ///
155 /// # Arguments
156 /// * `periodic_table` - Slice of periodic columns, each containing one period of values.
157 /// The length of each inner `Vec` is the period of that column (must be a power of 2).
158 /// * `trace_domain` - The original trace domain
159 /// * `lde_domain` - The low-degree extension domain
160 ///
161 /// # Returns
162 /// A [`PeriodicLdeTable`] with height = `max_period × blowup` and width = number of columns.
163 fn eval_on_lde(
164 periodic_table: &[Vec<F>],
165 trace_domain: &D,
166 lde_domain: &D,
167 ) -> PeriodicLdeTable<F>;
168
169 /// Evaluate all periodic columns at a single point (for verification).
170 ///
171 /// This is used by the verifier to compute periodic column values at
172 /// query points during constraint verification.
173 ///
174 /// # Arguments
175 /// * `periodic_table` - Slice of periodic columns. Each column's length (period)
176 /// must be a power of 2.
177 /// * `trace_domain` - The original trace domain
178 /// * `point` - The query point (in extension field)
179 ///
180 /// # Returns
181 /// `Vec<EF>` containing the evaluation of each periodic column at `point`
182 fn eval_at_point<EF: ExtensionField<F>>(
183 periodic_table: &[Vec<F>],
184 trace_domain: &D,
185 point: EF,
186 ) -> Vec<EF>;
187}
188
189/// Unit type implements `PeriodicEvaluator` as a no-op.
190///
191/// This is used internally by `prove` and `verify` for AIRs without periodic columns.
192/// Panics if any periodic columns are present.
193impl<F: Field, D: PolynomialSpace<Val = F>> PeriodicEvaluator<F, D> for () {
194 fn eval_on_lde(
195 periodic_table: &[Vec<F>],
196 _trace_domain: &D,
197 _lde_domain: &D,
198 ) -> PeriodicLdeTable<F> {
199 assert!(
200 periodic_table.is_empty(),
201 "AIR has periodic columns but no PeriodicEvaluator was specified. \
202 Use prove_with_periodic or verify_with_periodic with TwoAdicPeriodicEvaluator \
203 or CirclePeriodicEvaluator."
204 );
205 PeriodicLdeTable::empty()
206 }
207
208 fn eval_at_point<EF: ExtensionField<F>>(
209 periodic_table: &[Vec<F>],
210 _trace_domain: &D,
211 _point: EF,
212 ) -> Vec<EF> {
213 assert!(
214 periodic_table.is_empty(),
215 "AIR has periodic columns but no PeriodicEvaluator was specified. \
216 Use prove_with_periodic or verify_with_periodic with TwoAdicPeriodicEvaluator \
217 or CirclePeriodicEvaluator."
218 );
219 Vec::new()
220 }
221}
222
223#[cfg(test)]
224mod tests {
225 #[cfg(debug_assertions)]
226 #[test]
227 #[should_panic(expected = "PeriodicLdeTable height must be a power of two")]
228 fn new_panics_on_non_power_of_two_height() {
229 use alloc::vec;
230
231 use p3_baby_bear::BabyBear;
232 use p3_field::PrimeCharacteristicRing;
233
234 use super::*;
235
236 type F = BabyBear;
237
238 let (a, b, c) = (F::ONE, F::TWO, F::from_u8(3));
239 let _ = PeriodicLdeTable::new(RowMajorMatrix::new(vec![a, b, c], 1));
240 }
241}