1use alloc::vec::Vec;
31
32use p3_commit::{PeriodicEvaluator, PeriodicLdeTable, PolynomialSpace};
33use p3_field::ExtensionField;
34use p3_field::extension::ComplexExtendable;
35use p3_matrix::Matrix;
36use p3_matrix::dense::RowMajorMatrix;
37use p3_util::log2_strict_usize;
38
39use crate::CircleEvaluations;
40use crate::domain::CircleDomain;
41
42pub fn build_periodic_lde_table_circle<F: ComplexExtendable>(
47 periodic_table: &[Vec<F>],
48 trace_domain: &CircleDomain<F>,
49 lde_domain: &CircleDomain<F>,
50) -> p3_commit::PeriodicLdeTable<F> {
51 CirclePeriodicEvaluator::eval_on_lde(periodic_table, trace_domain, lde_domain)
52}
53
54#[derive(Clone, Copy, Debug, Default)]
62pub struct CirclePeriodicEvaluator;
63
64impl<F: ComplexExtendable> PeriodicEvaluator<F, CircleDomain<F>> for CirclePeriodicEvaluator {
65 fn eval_on_lde(
66 periodic_table: &[Vec<F>],
67 trace_domain: &CircleDomain<F>,
68 lde_domain: &CircleDomain<F>,
69 ) -> PeriodicLdeTable<F> {
70 if periodic_table.is_empty() {
71 return PeriodicLdeTable::empty();
72 }
73
74 let trace_len = trace_domain.size();
75 let log_blowup = lde_domain
76 .log_n
77 .checked_sub(trace_domain.log_n)
78 .expect("LDE domain log_n must be >= trace domain log_n");
79 let blowup = 1usize
80 .checked_shl(log_blowup as u32)
81 .expect("blowup overflow when computing 1 << log_blowup");
82
83 for col in periodic_table {
84 let period = col.len();
85 assert!(
86 period > 0 && period.is_power_of_two(),
87 "periodic column length must be a non-zero power of 2, got {period}",
88 );
89 assert!(
90 trace_len.is_multiple_of(period),
91 "trace domain size ({trace_len}) must be divisible by periodic column length ({period})",
92 );
93 }
94 let max_period = periodic_table.iter().map(|c| c.len()).max().unwrap();
95
96 let log_max_period = log2_strict_usize(max_period);
97 let log_repetitions = log2_strict_usize(trace_len / max_period);
98 let extended_height = max_period
99 .checked_mul(blowup)
100 .expect("extended height overflow when computing max_period * blowup");
101 let num_cols = periodic_table.len();
102 let row_major_capacity = extended_height
103 .checked_mul(num_cols)
104 .expect("row-major periodic table capacity overflow");
105
106 let extended_shift = lde_domain.shift.repeated_double(log_repetitions);
109 let extended_log_n = log_max_period
110 .checked_add(log_blowup)
111 .expect("extended periodic domain log size overflow");
112 let extended_periodic_domain = CircleDomain::new(extended_log_n, extended_shift);
113
114 let mut columns: Vec<Vec<F>> = Vec::with_capacity(num_cols);
117
118 for col in periodic_table {
119 let period = col.len();
120
121 let padded: Vec<F> = if period == max_period {
123 col.clone()
124 } else {
125 (0..max_period).map(|i| col[i % period]).collect()
126 };
127
128 let periodic_domain = CircleDomain::standard(log_max_period);
130 let evals = CircleEvaluations::from_natural_order(
131 periodic_domain,
132 RowMajorMatrix::new_col(padded),
133 );
134
135 let extended_evals = evals.extrapolate(extended_periodic_domain);
137 let extended_values = extended_evals.to_natural_order().to_row_major_matrix();
138 columns.push(extended_values.values);
139 }
140
141 let mut row_major_values = Vec::with_capacity(row_major_capacity);
143 for row_idx in 0..extended_height {
144 for col in &columns {
145 row_major_values.push(col[row_idx]);
146 }
147 }
148
149 PeriodicLdeTable::new(RowMajorMatrix::new(row_major_values, num_cols))
150 }
151
152 fn eval_at_point<EF: ExtensionField<F>>(
153 periodic_table: &[Vec<F>],
154 trace_domain: &CircleDomain<F>,
155 point: EF,
156 ) -> Vec<EF> {
157 let trace_len = trace_domain.size();
158 periodic_table
159 .iter()
160 .map(|col| {
161 let period = col.len();
162 assert!(
163 period > 0 && period.is_power_of_two(),
164 "periodic column length must be a non-zero power of 2, got {period}",
165 );
166 assert!(
167 trace_len.is_multiple_of(period),
168 "trace domain size ({trace_len}) must be divisible by periodic column length ({period})",
169 );
170 PolynomialSpace::evaluate_periodic_column_at(trace_domain, col, point)
171 })
172 .collect()
173 }
174}
175
176#[cfg(test)]
177mod tests {
178 use alloc::vec;
179
180 use hashbrown::HashMap;
181 use p3_field::PrimeCharacteristicRing;
182 use p3_field::extension::BinomialExtensionField;
183 use p3_mersenne_31::Mersenne31;
184 use rand::rngs::SmallRng;
185 use rand::{RngExt, SeedableRng};
186
187 use super::*;
188 use crate::point::Point;
189
190 type F = Mersenne31;
191 type EF = BinomialExtensionField<F, 3>;
192
193 #[test]
194 fn test_periodic_eval_consistency_random_points() {
195 let log_n = 4;
198 let log_blowup = 1;
199 let trace_domain = CircleDomain::<F>::standard(log_n);
200 let lde_domain = CircleDomain::<F>::standard(log_n + log_blowup);
201 let lde_len = lde_domain.size();
202
203 let periodic_col = vec![
205 F::from_u32(10),
206 F::from_u32(20),
207 F::from_u32(30),
208 F::from_u32(40),
209 ];
210 let periodic_table = vec![periodic_col];
211
212 let lde_table =
214 CirclePeriodicEvaluator::eval_on_lde(&periodic_table, &trace_domain, &lde_domain);
215
216 assert_eq!(lde_table.width(), 1);
217 assert_eq!(lde_table.height(), 8);
219
220 let full_lde: Vec<F> = (0..lde_len).map(|i| *lde_table.get(i, 0)).collect();
222
223 let lde_evals =
225 CircleEvaluations::from_natural_order(lde_domain, RowMajorMatrix::new_col(full_lde));
226
227 let mut rng = SmallRng::seed_from_u64(42);
229 for _ in 0..10 {
230 let random_point: EF = rng.random();
231
232 let lde_at_point =
234 lde_evals.evaluate_at_point(Point::from_projective_line(random_point))[0];
235
236 let eval_at_point_result = CirclePeriodicEvaluator::eval_at_point(
238 &periodic_table,
239 &trace_domain,
240 random_point,
241 );
242
243 assert_eq!(
244 lde_at_point, eval_at_point_result[0],
245 "Mismatch at random point: LDE interpolation={:?}, eval_at_point={:?}",
246 lde_at_point, eval_at_point_result[0]
247 );
248 }
249 }
250
251 #[test]
252 fn test_periodic_eval_at_trace_domain_points() {
253 let log_n = 4; let trace_domain = CircleDomain::<F>::standard(log_n);
257 let trace_len = trace_domain.size();
258 let period = 4;
259
260 let periodic_col = vec![
262 F::from_u32(1),
263 F::from_u32(2),
264 F::from_u32(3),
265 F::from_u32(4),
266 ];
267 let periodic_table = vec![periodic_col];
268
269 let lde_table =
271 CirclePeriodicEvaluator::eval_on_lde(&periodic_table, &trace_domain, &trace_domain);
272
273 assert_eq!(lde_table.width(), 1);
274 assert_eq!(lde_table.height(), 4);
276
277 let full_trace: Vec<F> = (0..trace_len).map(|i| *lde_table.get(i, 0)).collect();
279
280 let mut value_counts = HashMap::new();
284 for &val in &full_trace {
285 *value_counts.entry(val).or_insert(0) += 1;
286 }
287 assert_eq!(
288 value_counts.len(),
289 period,
290 "Expected {} distinct values, got {}",
291 period,
292 value_counts.len()
293 );
294 for (val, count) in &value_counts {
295 assert_eq!(
296 *count, 4,
297 "Value {:?} appears {} times, expected 4",
298 val, count
299 );
300 }
301 }
302
303 #[test]
304 fn test_cfft_extrapolation_matches_naive() {
305 for (log_n, log_blowup, log_period) in [(4, 1, 2), (5, 2, 2), (6, 1, 3), (8, 2, 4)] {
308 let trace_domain = CircleDomain::<F>::standard(log_n);
309 let lde_domain = CircleDomain::<F>::standard(log_n + log_blowup);
310 let lde_len = lde_domain.size();
311 let period = 1 << log_period;
312 let log_repetitions = log_n - log_period;
313
314 let periodic_col: Vec<F> = (0..period).map(|i| F::from_u32(i as u32 + 1)).collect();
316 let periodic_table = vec![periodic_col.clone()];
317
318 let cfft_table =
320 CirclePeriodicEvaluator::eval_on_lde(&periodic_table, &trace_domain, &lde_domain);
321
322 let cfft_result: Vec<F> = (0..lde_len).map(|i| *cfft_table.get(i, 0)).collect();
324
325 let periodic_domain = CircleDomain::standard(log_period);
327 let evals = CircleEvaluations::from_natural_order(
328 periodic_domain,
329 RowMajorMatrix::new_col(periodic_col.clone()),
330 );
331
332 let naive_result: Vec<F> = (0..lde_len)
333 .map(|lde_idx| {
334 let lde_point = lde_domain.nth_point(lde_idx);
335 let periodic_point = lde_point.repeated_double(log_repetitions);
336 evals.evaluate_at_point(periodic_point)[0]
337 })
338 .collect();
339
340 assert_eq!(
341 cfft_result, naive_result,
342 "CFFT-based and naive methods disagree for log_n={}, log_blowup={}, log_period={}",
343 log_n, log_blowup, log_period
344 );
345 }
346 }
347}