Skip to main content

binius_math/multilinear/
evaluate.rs

1// Copyright 2025 Irreducible Inc.
2
3use std::ops::{Deref, DerefMut};
4
5use binius_field::{Field, PackedField, field::FieldOps};
6use binius_utils::rayon::prelude::*;
7
8use crate::{
9	FieldBuffer,
10	inner_product::inner_product_buffers,
11	multilinear::{eq::eq_ind_partial_eval, fold::fold_highest_var_inplace},
12};
13
14/// Evaluates a multilinear polynomial at a given point using sqrt(n) memory.
15///
16/// This method computes the evaluation by splitting the computation into two phases:
17/// 1. Expand an eq tensor for the first half of coordinates (or at least P::LOG_WIDTH)
18/// 2. Take inner products of evaluation chunks with the eq tensor to reduce the problem size
19/// 3. Evaluate the remaining coordinates using evaluate_inplace
20///
21/// This approach uses O(sqrt(2^n)) memory instead of O(2^n).
22///
23/// # Arguments
24/// * `evals` - A FieldBuffer containing the 2^n evaluations over the boolean hypercube
25/// * `point` - The n coordinates at which to evaluate the polynomial
26///
27/// # Returns
28/// The evaluation of the multilinear polynomial at the given point
29///
30/// ## Preconditions
31///
32/// * `point.len()` must equal `evals.log_len()`
33pub fn evaluate<F, P, Data>(evals: &FieldBuffer<P, Data>, point: &[F]) -> F
34where
35	F: Field,
36	P: PackedField<Scalar = F>,
37	Data: Deref<Target = [P]>,
38{
39	assert_eq!(
40		point.len(),
41		evals.log_len(),
42		"precondition: point length must equal evals log length"
43	);
44
45	// Split coordinates: first half gets at least P::LOG_WIDTH coordinates
46	let first_half_len = (point.len() / 2).max(P::LOG_WIDTH).min(point.len());
47	let (first_coords, remaining_coords) = point.split_at(first_half_len);
48
49	// Generate eq tensor for first half of coordinates
50	let eq_tensor = eq_ind_partial_eval::<P>(first_coords);
51
52	// If there is no second half, just return the inner product with the whole evals.
53	if remaining_coords.is_empty() {
54		return inner_product_buffers(evals, &eq_tensor);
55	}
56
57	// Calculate chunk size based on first half length
58	let log_chunk_size = first_half_len;
59
60	// Collect inner products of chunks into scalar values
61	let scalars = evals
62		.chunks_par(log_chunk_size)
63		.map(|chunk| inner_product_buffers(&chunk, &eq_tensor))
64		.collect::<Vec<_>>();
65
66	// Create temporary buffer from collected scalar values
67	let temp_buffer = FieldBuffer::<P>::from_values(&scalars);
68
69	// Evaluate remaining coordinates using evaluate_inplace
70	evaluate_inplace(temp_buffer, remaining_coords)
71}
72
73/// Evaluates a multilinear polynomial at a given point, modifying the buffer in-place.
74///
75/// This method computes the evaluation of a multilinear polynomial specified by it's evaluations
76/// on the boolean hypercube. For an $n$-variate multilinear, this implementation performs
77/// $2^n - 1$ field multiplications and allocates no additional memory. The `evals` buffer is
78/// modified in-place.
79///
80/// # Arguments
81/// * `evals` - A [`FieldBuffer`] containing the $2^n$ evaluations over the boolean hypercube
82/// * `coords` - The $n$ coordinates at which to evaluate the polynomial
83///
84/// # Returns
85/// The evaluation of the multilinear polynomial at the given point
86///
87/// ## Preconditions
88///
89/// * `coords.len()` must equal `evals.log_len()`
90pub fn evaluate_inplace<F, P, Data>(mut evals: FieldBuffer<P, Data>, coords: &[F]) -> F
91where
92	F: Field,
93	P: PackedField<Scalar = F>,
94	Data: DerefMut<Target = [P]>,
95{
96	assert_eq!(
97		coords.len(),
98		evals.log_len(),
99		"precondition: coords length must equal evals log length"
100	);
101
102	// Perform folding for each coordinate in reverse order
103	for &coord in coords.iter().rev() {
104		fold_highest_var_inplace(&mut evals, coord);
105	}
106
107	assert_eq!(evals.len(), 1);
108	evals.get(0)
109}
110
111/// Evaluates a multilinear polynomial at a given point in-place using scalar operations.
112///
113/// This is a simple variant of multilinear evaluation that works directly on slices of scalars
114/// with only a `FieldOps` bound. For each coordinate (highest to lowest), it folds the upper
115/// half into the lower half: `evals[j] += r * (evals[j + half] - evals[j])`.
116///
117/// The final result is stored in `evals[0]` after all folds.
118///
119/// # Arguments
120/// * `evals` - The 2^n evaluations over the boolean hypercube, modified in-place
121/// * `point` - The n coordinates at which to evaluate the polynomial
122///
123/// # Panics
124///
125/// Panics if `evals.len() != 1 << point.len()`.
126pub fn evaluate_inplace_scalars<F: FieldOps>(
127	mut evals: impl DerefMut<Target = [F]>,
128	point: &[F],
129) -> F {
130	assert_eq!(evals.len(), 1 << point.len(), "precondition: evals length must be 2^point.len()");
131
132	for (log_half_len, point_i) in point.iter().enumerate().rev() {
133		let half_len = 1 << log_half_len;
134		for j in 0..half_len {
135			let delta = evals[j + half_len].clone() - evals[j].clone();
136			evals[j] += point_i.clone() * delta;
137		}
138	}
139	evals[0].clone()
140}
141
142#[cfg(test)]
143mod tests {
144	use rand::prelude::*;
145
146	use super::*;
147	use crate::{
148		inner_product::inner_product_par,
149		test_utils::{
150			B128, Packed128b, index_to_hypercube_point, random_field_buffer, random_scalars,
151		},
152	};
153
154	type P = Packed128b;
155	type F = B128;
156
157	#[test]
158	fn test_evaluate_consistency() {
159		/// Simple reference function for multilinear polynomial evaluation.
160		fn evaluate_with_inner_product<F, P, Data>(evals: &FieldBuffer<P, Data>, point: &[F]) -> F
161		where
162			F: Field,
163			P: PackedField<Scalar = F>,
164			Data: Deref<Target = [P]>,
165		{
166			assert_eq!(point.len(), evals.log_len());
167
168			// Compute the equality indicator tensor expansion
169			let eq_tensor = eq_ind_partial_eval::<P>(point);
170			inner_product_par(evals, &eq_tensor)
171		}
172
173		let mut rng = StdRng::seed_from_u64(0);
174
175		for log_n in [0, P::LOG_WIDTH - 1, P::LOG_WIDTH, 10] {
176			// Generate random buffer and evaluation point
177			let buffer = random_field_buffer::<P>(&mut rng, log_n);
178			let point = random_scalars::<F>(&mut rng, log_n);
179
180			// Evaluate using all three methods
181			let result_inner_product = evaluate_with_inner_product(&buffer, &point);
182			let result_inplace = evaluate_inplace(buffer.clone(), &point);
183			let result_sqrt_memory = evaluate(&buffer, &point);
184
185			// All results should be equal
186			assert_eq!(result_inner_product, result_inplace);
187			assert_eq!(result_inner_product, result_sqrt_memory);
188		}
189	}
190
191	#[test]
192	fn test_evaluate_at_hypercube_indices() {
193		let mut rng = StdRng::seed_from_u64(0);
194
195		// Generate random multilinear with 8 variables
196		let log_n = 8;
197		let buffer = random_field_buffer::<F>(&mut rng, log_n);
198
199		// Test 16 random hypercube indices
200		for _ in 0..16 {
201			let index = (rng.next_u32() as usize) % (1 << log_n);
202			let point = index_to_hypercube_point::<F>(log_n, index);
203
204			// Evaluate at the hypercube point
205			let eval_result = evaluate(&buffer, &point);
206
207			// Get the value directly from the buffer
208			let direct_value = buffer.get(index);
209
210			// They should be equal
211			assert_eq!(eval_result, direct_value);
212		}
213	}
214
215	#[test]
216	fn test_evaluate_inplace_scalars_consistency() {
217		let mut rng = StdRng::seed_from_u64(0);
218
219		for log_n in [0, P::LOG_WIDTH - 1, P::LOG_WIDTH, 10] {
220			let buffer = random_field_buffer::<P>(&mut rng, log_n);
221			let point = random_scalars::<F>(&mut rng, log_n);
222
223			let result_inplace = evaluate_inplace(buffer.clone(), &point);
224
225			let scalar_evals = buffer.iter_scalars().collect::<Vec<_>>();
226			let result_scalar = evaluate_inplace_scalars(scalar_evals, &point);
227
228			assert_eq!(result_inplace, result_scalar, "mismatch at log_n={log_n}");
229		}
230	}
231
232	#[test]
233	fn test_linearity() {
234		let mut rng = StdRng::seed_from_u64(0);
235
236		// Generate random 8-variable multilinear and evaluation point
237		let log_n = 8;
238		let buffer = random_field_buffer::<F>(&mut rng, log_n);
239		let mut point = random_scalars::<F>(&mut rng, log_n);
240
241		// Test linearity for each coordinate
242		for coord_idx in 0..log_n {
243			// Choose three coordinate values
244			let coord_vals = random_scalars::<F>(&mut rng, 3);
245
246			// Evaluate at three points differing only in coordinate coord_idx
247			let evals: Vec<_> = coord_vals
248				.iter()
249				.map(|&coord_val| {
250					point[coord_idx] = coord_val;
251					evaluate(&buffer, &point)
252				})
253				.collect();
254
255			// Check that the three evaluations form a line
256			// For a line through points (x0, y0), (x1, y1), (x2, y2):
257			// y2 - y0 = (y1 - y0) * (x2 - x0) / (x1 - x0)
258			// Rearranging: (y2 - y0) * (x1 - x0) = (y1 - y0) * (x2 - x0)
259			let x0 = coord_vals[0];
260			let x1 = coord_vals[1];
261			let x2 = coord_vals[2];
262			let y0 = evals[0];
263			let y1 = evals[1];
264			let y2 = evals[2];
265
266			let lhs = (y2 - y0) * (x1 - x0);
267			let rhs = (y1 - y0) * (x2 - x0);
268
269			assert_eq!(lhs, rhs);
270		}
271	}
272}