Skip to main content

zinc_piop/
projections.rs

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