binius_math/multilinear/
evaluate.rs

1// Copyright 2025 Irreducible Inc.
2
3use std::ops::{Deref, DerefMut};
4
5use binius_field::{Field, PackedField};
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#[cfg(test)]
112mod tests {
113	use rand::{RngCore, SeedableRng, rngs::StdRng};
114
115	use super::*;
116	use crate::{
117		inner_product::inner_product_par,
118		test_utils::{
119			B128, Packed128b, index_to_hypercube_point, random_field_buffer, random_scalars,
120		},
121	};
122
123	type P = Packed128b;
124	type F = B128;
125
126	#[test]
127	fn test_evaluate_consistency() {
128		/// Simple reference function for multilinear polynomial evaluation.
129		fn evaluate_with_inner_product<F, P, Data>(evals: &FieldBuffer<P, Data>, point: &[F]) -> F
130		where
131			F: Field,
132			P: PackedField<Scalar = F>,
133			Data: Deref<Target = [P]>,
134		{
135			assert_eq!(point.len(), evals.log_len());
136
137			// Compute the equality indicator tensor expansion
138			let eq_tensor = eq_ind_partial_eval::<P>(point);
139			inner_product_par(evals, &eq_tensor)
140		}
141
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			// Generate random buffer and evaluation point
146			let buffer = random_field_buffer::<P>(&mut rng, log_n);
147			let point = random_scalars::<F>(&mut rng, log_n);
148
149			// Evaluate using all three methods
150			let result_inner_product = evaluate_with_inner_product(&buffer, &point);
151			let result_inplace = evaluate_inplace(buffer.clone(), &point);
152			let result_sqrt_memory = evaluate(&buffer, &point);
153
154			// All results should be equal
155			assert_eq!(result_inner_product, result_inplace);
156			assert_eq!(result_inner_product, result_sqrt_memory);
157		}
158	}
159
160	#[test]
161	fn test_evaluate_at_hypercube_indices() {
162		let mut rng = StdRng::seed_from_u64(0);
163
164		// Generate random multilinear with 8 variables
165		let log_n = 8;
166		let buffer = random_field_buffer::<F>(&mut rng, log_n);
167
168		// Test 16 random hypercube indices
169		for _ in 0..16 {
170			let index = (rng.next_u32() as usize) % (1 << log_n);
171			let point = index_to_hypercube_point::<F>(log_n, index);
172
173			// Evaluate at the hypercube point
174			let eval_result = evaluate(&buffer, &point);
175
176			// Get the value directly from the buffer
177			let direct_value = buffer.get(index);
178
179			// They should be equal
180			assert_eq!(eval_result, direct_value);
181		}
182	}
183
184	#[test]
185	fn test_linearity() {
186		let mut rng = StdRng::seed_from_u64(0);
187
188		// Generate random 8-variable multilinear and evaluation point
189		let log_n = 8;
190		let buffer = random_field_buffer::<F>(&mut rng, log_n);
191		let mut point = random_scalars::<F>(&mut rng, log_n);
192
193		// Test linearity for each coordinate
194		for coord_idx in 0..log_n {
195			// Choose three coordinate values
196			let coord_vals = random_scalars::<F>(&mut rng, 3);
197
198			// Evaluate at three points differing only in coordinate coord_idx
199			let evals: Vec<_> = coord_vals
200				.iter()
201				.map(|&coord_val| {
202					point[coord_idx] = coord_val;
203					evaluate(&buffer, &point)
204				})
205				.collect();
206
207			// Check that the three evaluations form a line
208			// For a line through points (x0, y0), (x1, y1), (x2, y2):
209			// y2 - y0 = (y1 - y0) * (x2 - x0) / (x1 - x0)
210			// Rearranging: (y2 - y0) * (x1 - x0) = (y1 - y0) * (x2 - x0)
211			let x0 = coord_vals[0];
212			let x1 = coord_vals[1];
213			let x2 = coord_vals[2];
214			let y0 = evals[0];
215			let y1 = evals[1];
216			let y2 = evals[2];
217
218			let lhs = (y2 - y0) * (x1 - x0);
219			let rhs = (y1 - y0) * (x2 - x0);
220
221			assert_eq!(lhs, rhs);
222		}
223	}
224}