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::{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 `compute_combined_polynomials` for non-linear 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` for linear constraints (MLE-first).
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 (non-linear constraints).
40#[allow(clippy::arithmetic_side_effects)]
41pub fn project_trace_coeffs_row_major<F, PolyCoeff, Int, const DEGREE_PLUS_ONE: usize>(
42    trace: &UairTrace<PolyCoeff, Int, DEGREE_PLUS_ONE>,
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 (linear constraints only).
131#[allow(clippy::arithmetic_side_effects)]
132pub fn project_trace_coeffs_column_major<F, PolyCoeff, Int, const DEGREE_PLUS_ONE: usize>(
133    trace: &UairTrace<PolyCoeff, Int, DEGREE_PLUS_ONE>,
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/// Evaluate a projected trace along `F[X] -> F` and return column-indexed
224/// MLEs (`Vec<DenseMultilinearExtension<F::Inner>>`) for sumcheck
225/// compatibility. Dispatches on the trace layout internally.
226#[allow(clippy::arithmetic_side_effects)]
227pub fn evaluate_trace_to_column_mles<F: PrimeField + 'static>(
228    trace: &ProjectedTrace<F>,
229    projecting_element: &F,
230) -> Vec<DenseMultilinearExtension<F::Inner>> {
231    let zero = F::zero_with_cfg(projecting_element.cfg());
232    let one = F::one_with_cfg(projecting_element.cfg());
233
234    let max_coeffs_len = {
235        // Iterators have different types, so this is easier
236        macro_rules! common_code {
237            ($v:expr) => {
238                $v.iter()
239                    .flat_map(|row| row.iter())
240                    .map(|poly| poly.degree().map_or(0, |d| d + 1))
241                    .max()
242                    .unwrap_or(0)
243                    .max(1)
244            };
245        }
246        match trace {
247            ProjectedTrace::RowMajor(t) => common_code!(t),
248            ProjectedTrace::ColumnMajor(t) => common_code!(t),
249        }
250    };
251
252    let projection_powers: Vec<F> = powers(projecting_element.clone(), one, max_coeffs_len);
253
254    let evaluate_poly = |poly: &DynamicPolynomialF<F>| -> F::Inner {
255        let deg = poly.degree().map_or(0, |d| d + 1);
256        DynamicPolyFInnerProduct::inner_product::<UNCHECKED>(
257            &poly.coeffs[..deg],
258            &projection_powers[..deg],
259            zero.clone(),
260        )
261        .expect("inner product cannot fail here")
262        .into_inner()
263    };
264
265    match trace {
266        ProjectedTrace::RowMajor(t) => {
267            let num_rows = t.len();
268            let num_cols = t.first().map(|r| r.len()).unwrap_or(0);
269            let num_vars = num_rows.next_power_of_two().trailing_zeros() as usize;
270
271            cfg_into_iter!(0..num_cols)
272                .map(|col_idx| {
273                    let evaluations: Vec<F::Inner> = (0..num_rows)
274                        .map(|row_idx| evaluate_poly(&t[row_idx][col_idx]))
275                        .collect();
276                    DenseMultilinearExtension::from_evaluations_vec(
277                        num_vars,
278                        evaluations,
279                        zero.inner().clone(),
280                    )
281                })
282                .collect()
283        }
284        ProjectedTrace::ColumnMajor(t) => cfg_iter!(t)
285            .map(|col_mle| {
286                let evaluations: Vec<F::Inner> = cfg_iter!(col_mle).map(evaluate_poly).collect();
287                DenseMultilinearExtension::from_evaluations_vec(
288                    col_mle.num_vars,
289                    evaluations,
290                    zero.inner().clone(),
291                )
292            })
293            .collect(),
294    }
295}
296
297/// Project scalars of a UAIR onto F[X].
298pub fn project_scalars<F: PrimeField, U: Uair>(
299    project: impl Fn(&U::Scalar) -> DynamicPolynomialF<F>,
300) -> HashMap<U::Scalar, DynamicPolynomialF<F>> {
301    let uair_scalars = collect_scalars::<U>();
302
303    // TODO(Ilia): if there's a lot of scalars
304    //             we should do this in parallel probably.
305    uair_scalars
306        .into_iter()
307        .map(|scalar| {
308            (scalar.clone(), {
309                let mut dynamic_poly = project(&scalar);
310
311                dynamic_poly.trim();
312
313                dynamic_poly
314            })
315        })
316        .collect()
317}
318
319/// Project scalars of a UAIR along F[X] -> F.
320#[allow(clippy::arithmetic_side_effects)]
321pub fn project_scalars_to_field<R: Semiring + 'static, F: PrimeField>(
322    scalars: HashMap<R, DynamicPolynomialF<F>>,
323    projecting_element: &F,
324) -> Result<HashMap<R, F>, (R, F, EvaluationError)> {
325    // TODO(Ilia): Parallelising this might be good for big UAIRs.
326    //             We'd conditionally route between sequential and parallel
327    //             projection depending on how many scalars the UAIR has.
328    let one = F::one_with_cfg(projecting_element.cfg());
329    let zero = F::zero_with_cfg(projecting_element.cfg());
330
331    let max_coeffs_len = scalars
332        .values()
333        .map(|poly| poly.degree().map_or(0, |d| d + 1))
334        .max()
335        .unwrap_or(0)
336        .max(1);
337
338    let projection_powers: Vec<F> = powers(projecting_element.clone(), one, max_coeffs_len);
339
340    Ok(scalars
341        .into_iter()
342        .map(|(scalar, value)| {
343            let deg = value.degree().map_or(0, |d| d + 1);
344            (
345                scalar,
346                DynamicPolyFInnerProduct::inner_product::<UNCHECKED>(
347                    &value.coeffs[..deg],
348                    &projection_powers[..deg],
349                    zero.clone(),
350                )
351                .expect("inner product cannot fail here"),
352            )
353        })
354        .collect())
355}