Skip to main content

binius_math/multilinear/
evaluate.rs

1// Copyright 2025 Irreducible Inc.
2// Copyright 2026 The Binius Developers
3
4//! Evaluating a multilinear polynomial at a point off the hypercube.
5
6use std::ops::{Deref, DerefMut};
7
8use binius_compute::BufferData;
9use binius_field::{Field, PackedField, field::FieldOps};
10use binius_utils::rayon::prelude::*;
11
12use crate::{
13	FieldBuffer,
14	inner_product::inner_product_buffers,
15	multilinear::{eq::eq_ind_partial_eval, fold::fold_highest_var_inplace},
16};
17
18/// Evaluates a multilinear polynomial at a point, leaving the coefficients in place.
19///
20/// The point holds one coordinate per variable.
21/// The result is a single field element.
22/// Memory used is on the order of the square root of the coefficient count.
23///
24/// ## Preconditions
25///
26/// * the point must hold one coordinate per variable of the polynomial
27pub fn evaluate<F, P, Data>(evals: &FieldBuffer<P, Data>, point: &[F]) -> F
28where
29	F: Field,
30	P: PackedField<Scalar = F>,
31	Data: Deref<Target = [P]>,
32{
33	assert_eq!(
34		point.len(),
35		evals.log_len(),
36		"precondition: point length must equal evals log length"
37	);
38
39	// The point splits in half, and the first half gets at least one packed word's worth.
40	// Expanding only that half costs memory on the order of the square root of the whole.
41	let first_half_len = (point.len() / 2).max(P::LOG_WIDTH).min(point.len());
42	let (first_coords, remaining_coords) = point.split_at(first_half_len);
43	let eq_tensor = eq_ind_partial_eval::<P>(first_coords);
44
45	// With nothing left over the expansion covers every variable, so one pairing finishes.
46	if remaining_coords.is_empty() {
47		return inner_product_buffers(evals, &eq_tensor);
48	}
49
50	// Otherwise each chunk pairs with the expansion, and the resulting scalars are the
51	// residual multilinear over the coordinates not yet used.
52	let scalars = evals
53		.par_chunks(first_half_len)
54		.map(|chunk| inner_product_buffers(&chunk, &eq_tensor))
55		.collect::<Vec<_>>();
56
57	evaluate_inplace(FieldBuffer::<P>::from_values(&scalars), remaining_coords)
58}
59
60/// Evaluates a multilinear polynomial at a point, consuming the coefficients.
61///
62/// One variable is fixed at a time, in place, so nothing beyond the buffer is allocated.
63/// Each fold halves the buffer, and the last one leaves the single result.
64///
65/// ## Preconditions
66///
67/// * the point must hold one coordinate per variable of the polynomial
68pub fn evaluate_inplace<F, P, Data>(mut evals: FieldBuffer<P, Data>, coords: &[F]) -> F
69where
70	F: Field,
71	P: PackedField<Scalar = F>,
72	Data: BufferData<P>,
73{
74	assert_eq!(
75		coords.len(),
76		evals.log_len(),
77		"precondition: coords length must equal evals log length"
78	);
79
80	// Fixing the highest variable first keeps the survivors in a prefix, so an `n`-variate
81	// polynomial costs `2^n - 1` multiplications and no memory beyond the buffer.
82	for &coord in coords.iter().rev() {
83		fold_highest_var_inplace(&mut evals, coord);
84	}
85
86	assert_eq!(evals.len(), 1);
87	evals.get(0)
88}
89
90/// Evaluates a multilinear polynomial at a given point in-place using scalar operations.
91///
92/// This is a simple variant of multilinear evaluation that works directly on slices of scalars
93/// with only a `FieldOps` bound. For each coordinate (highest to lowest), it folds the upper
94/// half into the lower half: `evals[j] += r * (evals[j + half] - evals[j])`.
95///
96/// The final result is stored in `evals[0]` after all folds.
97///
98/// # Arguments
99/// * `evals` - The 2^n evaluations over the boolean hypercube, modified in-place
100/// * `point` - The n coordinates at which to evaluate the polynomial
101///
102/// # Panics
103///
104/// Panics if `evals.len() != 1 << point.len()`.
105pub fn evaluate_inplace_scalars<F: FieldOps>(
106	mut evals: impl DerefMut<Target = [F]>,
107	point: &[F],
108) -> F {
109	assert_eq!(evals.len(), 1 << point.len(), "precondition: evals length must be 2^point.len()");
110
111	for (log_half_len, point_i) in point.iter().enumerate().rev() {
112		let half_len = 1 << log_half_len;
113		for j in 0..half_len {
114			let delta = evals[j + half_len].clone() - evals[j].clone();
115			evals[j] += point_i.clone() * delta;
116		}
117	}
118	evals[0].clone()
119}
120
121#[cfg(test)]
122mod tests {
123	use proptest::prelude::*;
124	use rand::prelude::*;
125
126	use super::*;
127	use crate::{
128		inner_product::inner_product_par,
129		test_utils::{
130			B128, Packed128b, index_to_hypercube_point, random_field_buffer, random_scalars,
131		},
132	};
133
134	type P = Packed128b;
135	type F = B128;
136
137	// The packing width is four scalars, so this range straddles it in both directions.
138	const MAX_VARS: usize = 8;
139
140	#[test]
141	fn test_evaluate_inplace_scalars_consistency() {
142		let mut rng = StdRng::seed_from_u64(0);
143
144		for log_n in [0, P::LOG_WIDTH - 1, P::LOG_WIDTH, 10] {
145			let buffer = random_field_buffer::<P>(&mut rng, log_n);
146			let point = random_scalars::<F>(&mut rng, log_n);
147
148			let result_inplace = evaluate_inplace(buffer.clone(), &point);
149
150			let scalar_evals = buffer.iter_scalars().collect::<Vec<_>>();
151			let result_scalar = evaluate_inplace_scalars(scalar_evals, &point);
152
153			assert_eq!(result_inplace, result_scalar, "mismatch at log_n={log_n}");
154		}
155	}
156
157	#[test]
158	fn evaluate_at_a_hypercube_vertex_reads_that_coefficient() {
159		let mut rng = StdRng::seed_from_u64(0);
160
161		// Every vertex of a small cube is cheap enough to check exhaustively.
162		let n_vars = 8;
163		let buffer = random_field_buffer::<F>(&mut rng, n_vars);
164
165		for index in 0..1 << n_vars {
166			let point = index_to_hypercube_point::<F>(n_vars, index);
167
168			assert_eq!(evaluate(&buffer, &point), buffer.get(index), "mismatch at vertex {index}");
169		}
170	}
171
172	#[test]
173	fn evaluating_a_borrowed_view_matches_evaluating_its_owner() {
174		let mut rng = StdRng::seed_from_u64(0);
175
176		// Invariant: a shared view carries no store of its own, only a borrow of one.
177		// Reading it must reach the same coefficients as reading the buffer it came from.
178		//
179		// Fixture state: one 5-variable buffer and the view onto it, evaluated at one point.
180		let buffer = random_field_buffer::<P>(&mut rng, 5);
181		let point = random_scalars::<F>(&mut rng, 5);
182		let view = buffer.as_view();
183
184		assert_eq!(view.log_len(), 5);
185		assert_eq!(evaluate(&view, &point), evaluate(&buffer, &point));
186	}
187
188	#[test]
189	fn evaluate_is_linear_in_every_coordinate() {
190		let mut rng = StdRng::seed_from_u64(0);
191
192		let n_vars = 8;
193		let buffer = random_field_buffer::<F>(&mut rng, n_vars);
194		let mut point = random_scalars::<F>(&mut rng, n_vars);
195
196		for coord_idx in 0..n_vars {
197			// Three points differing only in this coordinate must have collinear evaluations.
198			let coord_vals = random_scalars::<F>(&mut rng, 3);
199			let evals = coord_vals
200				.iter()
201				.map(|&coord_val| {
202					point[coord_idx] = coord_val;
203					evaluate(&buffer, &point)
204				})
205				.collect::<Vec<_>>();
206
207			// Collinearity of the three points, cross-multiplied so nothing is divided.
208			let [x0, x1, x2] = [coord_vals[0], coord_vals[1], coord_vals[2]];
209			let [y0, y1, y2] = [evals[0], evals[1], evals[2]];
210			assert_eq!((y2 - y0) * (x1 - x0), (y1 - y0) * (x2 - x0));
211		}
212	}
213
214	proptest! {
215		#[test]
216		fn the_two_evaluations_agree_with_the_definition(
217			n_vars in 0..=MAX_VARS,
218			seed: u64,
219		) {
220			let mut rng = StdRng::seed_from_u64(seed);
221			let buffer = random_field_buffer::<P>(&mut rng, n_vars);
222			let point = random_scalars::<F>(&mut rng, n_vars);
223
224			// Pairing with the full expansion is the definition, and the cheapest reference.
225			let reference = inner_product_par(&buffer, &eq_ind_partial_eval::<P>(&point));
226
227			prop_assert_eq!(evaluate(&buffer, &point), reference);
228
229			// The in-place form consumes the coefficients, so it goes last.
230			prop_assert_eq!(evaluate_inplace(buffer, &point), reference);
231		}
232	}
233}