Skip to main content

zinc_piop/
projections.rs

1#[cfg(feature = "parallel")]
2use rayon::prelude::*;
3
4use crypto_primitives::{FromWithConfig, PrimeField, Semiring};
5use std::{collections::HashMap, iter};
6use zinc_poly::{
7    EvaluationError,
8    mle::DenseMultilinearExtension,
9    univariate::dynamic::over_field::{DynamicPolyFInnerProduct, DynamicPolynomialF},
10};
11use zinc_uair::{BitOp, BitOpSpec, Uair, UairTrace, collect_scalars::collect_scalars};
12use zinc_utils::{
13    UNCHECKED, cfg_extend, cfg_into_iter, cfg_iter, cfg_iter_mut, inner_product::InnerProduct,
14    powers,
15};
16
17/// Row-indexed trace matrix: `trace[row][col]`.
18/// Each row contains all column values for that row.
19/// Used by `evaluate_for_constraints`.
20pub type RowMajorTrace<F> = Vec<Vec<DynamicPolynomialF<F>>>;
21
22/// Column-indexed trace matrix: `trace[col][row]`.
23/// Each column is a `DenseMultilinearExtension` over the hypercube.
24/// Used by `evaluate_combined_polynomials` (MLE-first approach).
25pub type ColumnMajorTrace<F> = Vec<DenseMultilinearExtension<DynamicPolynomialF<F>>>;
26
27/// Holds the projected trace in either row-major or column-major layout,
28/// depending on which ideal check approach (MLE-first or combined) is used.
29#[derive(Clone, Debug)]
30pub enum ProjectedTrace<F: PrimeField> {
31    RowMajor(RowMajorTrace<F>),
32    ColumnMajor(ColumnMajorTrace<F>),
33}
34
35/// Project a multi-typed trace onto F[X], returning a row-indexed (transposed)
36/// matrix. Result: `trace[row][col]` where columns are ordered as binary_poly,
37/// arbitrary_poly, int.
38///
39/// Use this for the combined polynomial approach.
40#[allow(clippy::arithmetic_side_effects)]
41pub fn project_trace_coeffs_row_major<F, PolyCoeff, Int, const DB: usize, const DA: usize>(
42    trace: &UairTrace<PolyCoeff, Int, DB, DA>,
43    field_cfg: &F::Config,
44) -> RowMajorTrace<F>
45where
46    F: for<'a> FromWithConfig<&'a PolyCoeff> + for<'a> FromWithConfig<&'a Int> + Send + Sync,
47    PolyCoeff: Clone + Send + Sync,
48    Int: Clone + Send + Sync,
49{
50    let zero = F::zero_with_cfg(field_cfg);
51    let one = F::one_with_cfg(field_cfg);
52
53    let binary_len = trace.binary_poly.len();
54    let arbitrary_len = trace.arbitrary_poly.len();
55    let num_cols = trace.binary_poly.len() + trace.arbitrary_poly.len() + trace.int.len();
56
57    // Determine number of rows from the first non-empty column
58    let num_rows = trace
59        .binary_poly
60        .first()
61        .map(|c| c.len())
62        .or_else(|| trace.arbitrary_poly.first().map(|c| c.len()))
63        .or_else(|| trace.int.first().map(|c| c.len()))
64        .unwrap_or(0);
65
66    // Preallocate the result matrix with the correct number of rows and columns.
67    // (We have to work around the fact that cloned Vec doesn't keep its capacity)
68    let mut result: RowMajorTrace<F> = iter::repeat_with(|| Vec::with_capacity(num_cols))
69        .take(num_rows)
70        .collect();
71
72    // Build row-by-row
73    cfg_iter_mut!(result)
74        .enumerate()
75        .for_each(|(row_idx, row)| {
76            let spare = row.spare_capacity_mut();
77
78            // Binary poly columns
79            cfg_iter_mut!(spare[..binary_len])
80                .zip(cfg_iter!(trace.binary_poly))
81                .for_each(|(slot, col)| {
82                    let binary_poly = &col.evaluations[row_idx];
83                    slot.write(
84                        binary_poly
85                            .iter()
86                            .map(|coeff| {
87                                if coeff.into_inner() {
88                                    one.clone()
89                                } else {
90                                    zero.clone()
91                                }
92                            })
93                            .collect(),
94                    );
95                });
96
97            // Arbitrary poly columns
98            cfg_iter_mut!(spare[binary_len..binary_len + arbitrary_len])
99                .zip(cfg_iter!(trace.arbitrary_poly))
100                .for_each(|(slot, col)| {
101                    let arbitrary_poly = &col.evaluations[row_idx];
102                    slot.write(
103                        arbitrary_poly
104                            .iter()
105                            .map(|coeff| F::from_with_cfg(coeff, field_cfg))
106                            .collect(),
107                    );
108                });
109
110            // Int columns
111            cfg_iter_mut!(spare[binary_len + arbitrary_len..])
112                .zip(cfg_iter!(trace.int))
113                .for_each(|(slot, col)| {
114                    let int_val = &col.evaluations[row_idx];
115                    slot.write(DynamicPolynomialF {
116                        coeffs: vec![F::from_with_cfg(int_val, field_cfg)],
117                    });
118                });
119
120            // SAFETY: All slots have been initialized above.
121            unsafe { row.set_len(num_cols) };
122        });
123    result
124}
125
126/// Project a multi-typed trace onto `F[X]`, returning a column-indexed matrix.
127/// Result: `trace[col]` is a
128/// `DenseMultilinearExtension<DynamicPolynomialF<F>>`.
129///
130/// Use this for the MLE-first approach.
131#[allow(clippy::arithmetic_side_effects)]
132pub fn project_trace_coeffs_column_major<F, PolyCoeff, Int, const DB: usize, const DA: usize>(
133    trace: &UairTrace<PolyCoeff, Int, DB, DA>,
134    field_cfg: &F::Config,
135) -> ColumnMajorTrace<F>
136where
137    F: for<'a> FromWithConfig<&'a PolyCoeff> + for<'a> FromWithConfig<&'a Int> + Send + Sync,
138    PolyCoeff: Clone + Send + Sync,
139    Int: Clone + Send + Sync,
140{
141    let zero = F::zero_with_cfg(field_cfg);
142    let one = F::one_with_cfg(field_cfg);
143
144    let num_vars = [
145        trace.binary_poly.first().map(|c| c.num_vars),
146        trace.arbitrary_poly.first().map(|c| c.num_vars),
147        trace.int.first().map(|c| c.num_vars),
148    ]
149    .into_iter()
150    .flatten()
151    .max()
152    .unwrap_or(0);
153
154    let mut result =
155        Vec::with_capacity(trace.binary_poly.len() + trace.arbitrary_poly.len() + trace.int.len());
156
157    // Binary poly columns
158    cfg_extend!(
159        result,
160        cfg_iter!(trace.binary_poly).map(|column| {
161            let evaluations: Vec<DynamicPolynomialF<F>> = column
162                .iter()
163                .map(|binary_poly| {
164                    binary_poly
165                        .iter()
166                        .map(|coeff| {
167                            if coeff.into_inner() {
168                                one.clone()
169                            } else {
170                                zero.clone()
171                            }
172                        })
173                        .collect()
174                })
175                .collect();
176            DenseMultilinearExtension {
177                evaluations,
178                num_vars,
179            }
180        })
181    );
182
183    // Arbitrary poly columns
184    cfg_extend!(
185        result,
186        cfg_iter!(trace.arbitrary_poly).map(|column| {
187            let evaluations: Vec<DynamicPolynomialF<F>> = column
188                .iter()
189                .map(|arbitrary_poly| {
190                    arbitrary_poly
191                        .iter()
192                        .map(|coeff| F::from_with_cfg(coeff, field_cfg))
193                        .collect()
194                })
195                .collect();
196            DenseMultilinearExtension {
197                evaluations,
198                num_vars,
199            }
200        })
201    );
202
203    // Int columns
204    cfg_extend!(
205        result,
206        cfg_iter!(trace.int).map(|column| {
207            let evaluations: Vec<DynamicPolynomialF<F>> = column
208                .iter()
209                .map(|int| DynamicPolynomialF {
210                    coeffs: vec![F::from_with_cfg(int, field_cfg)],
211                })
212                .collect();
213            DenseMultilinearExtension {
214                evaluations,
215                num_vars,
216            }
217        })
218    );
219
220    result
221}
222
223/// Transpose a column-indexed trace into a row-indexed trace.
224///
225/// `result[row][col] = trace[col].evaluations[row].clone()`. Used by the
226/// MLE-first prover when it needs to fall back to the row-major
227/// `evaluate_for_constraints` path for non-linear constraints.
228pub fn column_major_to_row_major<F: PrimeField>(trace: &ColumnMajorTrace<F>) -> RowMajorTrace<F> {
229    let num_rows = trace.first().map(|c| c.evaluations.len()).unwrap_or(0);
230
231    cfg_into_iter!(0..num_rows)
232        .map(|row_idx| {
233            trace
234                .iter()
235                .map(|col| col.evaluations[row_idx].clone())
236                .collect()
237        })
238        .collect()
239}
240
241/// Evaluate a projected trace along `F[X] -> F` and return column-indexed
242/// MLEs (`Vec<DenseMultilinearExtension<F::Inner>>`) for sumcheck
243/// compatibility. Dispatches on the trace layout internally.
244#[allow(clippy::arithmetic_side_effects)]
245pub fn evaluate_trace_to_column_mles<F: PrimeField + 'static>(
246    trace: &ProjectedTrace<F>,
247    projecting_element: &F,
248) -> Vec<DenseMultilinearExtension<F::Inner>> {
249    let zero = F::zero_with_cfg(projecting_element.cfg());
250    let one = F::one_with_cfg(projecting_element.cfg());
251
252    let max_coeffs_len = {
253        // Iterators have different types, so this is easier
254        macro_rules! common_code {
255            ($v:expr) => {
256                $v.iter()
257                    .flat_map(|row| row.iter())
258                    .map(|poly| poly.degree().map_or(0, |d| d + 1))
259                    .max()
260                    .unwrap_or(0)
261                    .max(1)
262            };
263        }
264        match trace {
265            ProjectedTrace::RowMajor(t) => common_code!(t),
266            ProjectedTrace::ColumnMajor(t) => common_code!(t),
267        }
268    };
269
270    let projection_powers: Vec<F> = powers(projecting_element.clone(), one, max_coeffs_len);
271
272    let evaluate_poly = |poly: &DynamicPolynomialF<F>| -> F::Inner {
273        let deg = poly.degree().map_or(0, |d| d + 1);
274        DynamicPolyFInnerProduct::inner_product::<UNCHECKED>(
275            &poly.coeffs[..deg],
276            &projection_powers[..deg],
277            zero.clone(),
278        )
279        .expect("inner product cannot fail here")
280        .into_inner()
281    };
282
283    match trace {
284        ProjectedTrace::RowMajor(t) => {
285            let num_rows = t.len();
286            let num_cols = t.first().map(|r| r.len()).unwrap_or(0);
287            let num_vars = num_rows.next_power_of_two().trailing_zeros() as usize;
288
289            cfg_into_iter!(0..num_cols)
290                .map(|col_idx| {
291                    let evaluations: Vec<F::Inner> = (0..num_rows)
292                        .map(|row_idx| evaluate_poly(&t[row_idx][col_idx]))
293                        .collect();
294                    DenseMultilinearExtension::from_evaluations_vec(
295                        num_vars,
296                        evaluations,
297                        zero.inner().clone(),
298                    )
299                })
300                .collect()
301        }
302        ProjectedTrace::ColumnMajor(t) => cfg_iter!(t)
303            .map(|col_mle| {
304                let evaluations: Vec<F::Inner> = cfg_iter!(col_mle).map(evaluate_poly).collect();
305                DenseMultilinearExtension::from_evaluations_vec(
306                    col_mle.num_vars,
307                    evaluations,
308                    zero.inner().clone(),
309                )
310            })
311            .collect(),
312    }
313}
314
315/// Build the projected MLE of a bit-op virtual column.
316///
317/// The bit operation is applied to each source cell's coefficients before
318/// evaluating the cell at `projecting_element`.
319pub fn build_bit_op_virtual_mle<F: PrimeField + 'static, const D: usize>(
320    trace: &ProjectedTrace<F>,
321    spec: &BitOpSpec,
322    projecting_element: &F,
323    field_cfg: &F::Config,
324) -> DenseMultilinearExtension<F::Inner> {
325    let zero = F::zero_with_cfg(field_cfg);
326    let one = F::one_with_cfg(field_cfg);
327    let projection_powers: Vec<F> = powers(projecting_element.clone(), one, D);
328
329    let c = spec.op().count();
330    assert!(
331        c > 0 && c < D,
332        "BitOp count {c} out of range for cell width D = {D}",
333    );
334
335    let evaluate_with_bit_op = |cell: &DynamicPolynomialF<F>| -> F::Inner {
336        let transformed = match spec.op() {
337            BitOp::Rot(c) => cell.rotate_right::<D>(c, field_cfg),
338            BitOp::ShR(c) => cell.shr::<D>(c, field_cfg),
339        };
340        DynamicPolyFInnerProduct::inner_product::<UNCHECKED>(
341            &transformed.coeffs,
342            &projection_powers,
343            zero.clone(),
344        )
345        .expect("inner product cannot fail here")
346        .into_inner()
347    };
348
349    match trace {
350        ProjectedTrace::RowMajor(t) => {
351            let num_rows = t.len();
352            let num_vars = num_rows.next_power_of_two().trailing_zeros() as usize;
353            let evaluations: Vec<F::Inner> = (0..num_rows)
354                .map(|row_idx| evaluate_with_bit_op(&t[row_idx][spec.source_col()]))
355                .collect();
356            DenseMultilinearExtension::from_evaluations_vec(
357                num_vars,
358                evaluations,
359                zero.inner().clone(),
360            )
361        }
362        ProjectedTrace::ColumnMajor(t) => {
363            let col_mle = &t[spec.source_col()];
364            let evaluations: Vec<F::Inner> = col_mle.iter().map(evaluate_with_bit_op).collect();
365            DenseMultilinearExtension::from_evaluations_vec(
366                col_mle.num_vars,
367                evaluations,
368                zero.inner().clone(),
369            )
370        }
371    }
372}
373
374/// Project scalars of a UAIR onto F[X].
375pub fn project_scalars<F: PrimeField, U: Uair>(
376    project: impl Fn(&U::Scalar) -> DynamicPolynomialF<F>,
377) -> HashMap<U::Scalar, DynamicPolynomialF<F>> {
378    let uair_scalars = collect_scalars::<U>();
379
380    // TODO(Ilia): if there's a lot of scalars
381    //             we should do this in parallel probably.
382    uair_scalars
383        .into_iter()
384        .map(|scalar| {
385            let mut dynamic_poly = project(&scalar);
386            dynamic_poly.trim();
387            (scalar, dynamic_poly)
388        })
389        .collect()
390}
391
392/// Project scalars of a UAIR along F[X] -> F.
393#[allow(clippy::arithmetic_side_effects)]
394pub fn project_scalars_to_field<R: Semiring + 'static, F: PrimeField>(
395    scalars: HashMap<R, DynamicPolynomialF<F>>,
396    projecting_element: &F,
397) -> Result<HashMap<R, F>, (R, F, EvaluationError)> {
398    // TODO(Ilia): Parallelising this might be good for big UAIRs.
399    //             We'd conditionally route between sequential and parallel
400    //             projection depending on how many scalars the UAIR has.
401    let one = F::one_with_cfg(projecting_element.cfg());
402    let zero = F::zero_with_cfg(projecting_element.cfg());
403
404    let max_coeffs_len = scalars
405        .values()
406        .map(|poly| poly.degree().map_or(0, |d| d + 1))
407        .max()
408        .unwrap_or(0)
409        .max(1);
410
411    let projection_powers: Vec<F> = powers(projecting_element.clone(), one, max_coeffs_len);
412
413    Ok(scalars
414        .into_iter()
415        .map(|(scalar, value)| {
416            let deg = value.degree().map_or(0, |d| d + 1);
417            (
418                scalar,
419                DynamicPolyFInnerProduct::inner_product::<UNCHECKED>(
420                    &value.coeffs[..deg],
421                    &projection_powers[..deg],
422                    zero.clone(),
423                )
424                .expect("inner product cannot fail here"),
425            )
426        })
427        .collect())
428}
429
430#[cfg(test)]
431mod tests {
432    use super::*;
433    use crate::test_utils::{LIMBS, test_config};
434    use crypto_primitives::{Field, FromWithConfig, crypto_bigint_monty::MontyField};
435
436    type F = MontyField<LIMBS>;
437
438    fn f(value: u32, cfg: &<F as PrimeField>::Config) -> F {
439        F::from_with_cfg(value, cfg)
440    }
441
442    fn poly(coeffs: Vec<F>) -> DynamicPolynomialF<F> {
443        DynamicPolynomialF { coeffs }
444    }
445
446    fn expected_inner(
447        coeffs: &[F],
448        projecting_element: &F,
449        cfg: &<F as PrimeField>::Config,
450    ) -> <F as Field>::Inner {
451        let zero = F::zero_with_cfg(cfg);
452        let one = F::one_with_cfg(cfg);
453        let powers = powers(projecting_element.clone(), one, coeffs.len());
454        DynamicPolyFInnerProduct::inner_product::<UNCHECKED>(coeffs, &powers, zero)
455            .expect("inner product cannot fail here")
456            .into_inner()
457    }
458
459    #[test]
460    fn builds_bit_op_virtual_mle_from_row_major_trace() {
461        let cfg = test_config();
462        let alpha = f(2, &cfg);
463        let zero = F::zero_with_cfg(&cfg);
464        let row0 = [f(1, &cfg), f(2, &cfg), f(3, &cfg), f(4, &cfg)];
465        let row1 = [f(5, &cfg), f(6, &cfg), f(7, &cfg), f(8, &cfg)];
466        let trace =
467            ProjectedTrace::RowMajor(vec![vec![poly(row0.to_vec())], vec![poly(row1.to_vec())]]);
468
469        let mle = build_bit_op_virtual_mle::<F, 4>(
470            &trace,
471            &BitOpSpec::new(0, BitOp::ShR(2)),
472            &alpha,
473            &cfg,
474        );
475
476        assert_eq!(mle.num_vars, 1);
477        assert_eq!(
478            mle.evaluations,
479            vec![
480                expected_inner(
481                    &[row0[2].clone(), row0[3].clone(), zero.clone(), zero.clone()],
482                    &alpha,
483                    &cfg
484                ),
485                expected_inner(
486                    &[row1[2].clone(), row1[3].clone(), zero.clone(), zero],
487                    &alpha,
488                    &cfg
489                ),
490            ],
491        );
492    }
493
494    #[test]
495    fn builds_bit_op_virtual_mle_from_column_major_trace() {
496        let cfg = test_config();
497        let alpha = f(3, &cfg);
498        let row0 = [f(1, &cfg), f(2, &cfg), f(3, &cfg), f(4, &cfg)];
499        let row1 = [f(5, &cfg), f(6, &cfg), f(7, &cfg), f(8, &cfg)];
500        let trace = ProjectedTrace::ColumnMajor(vec![DenseMultilinearExtension {
501            evaluations: vec![poly(row0.to_vec()), poly(row1.to_vec())],
502            num_vars: 1,
503        }]);
504
505        let mle = build_bit_op_virtual_mle::<F, 4>(
506            &trace,
507            &BitOpSpec::new(0, BitOp::Rot(1)),
508            &alpha,
509            &cfg,
510        );
511
512        assert_eq!(mle.num_vars, 1);
513        assert_eq!(
514            mle.evaluations,
515            vec![
516                expected_inner(
517                    &[
518                        row0[1].clone(),
519                        row0[2].clone(),
520                        row0[3].clone(),
521                        row0[0].clone()
522                    ],
523                    &alpha,
524                    &cfg,
525                ),
526                expected_inner(
527                    &[
528                        row1[1].clone(),
529                        row1[2].clone(),
530                        row1[3].clone(),
531                        row1[0].clone()
532                    ],
533                    &alpha,
534                    &cfg,
535                ),
536            ],
537        );
538    }
539
540    #[test]
541    #[should_panic(expected = "out of range")]
542    fn rejects_bit_op_count_at_cell_width() {
543        let cfg = test_config();
544        let alpha = f(2, &cfg);
545        let trace = ProjectedTrace::RowMajor(vec![vec![poly(vec![f(1, &cfg), f(2, &cfg)])]]);
546
547        let _ = build_bit_op_virtual_mle::<F, 2>(
548            &trace,
549            &BitOpSpec::new(0, BitOp::Rot(2)),
550            &alpha,
551            &cfg,
552        );
553    }
554}