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	Error, 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
29pub fn evaluate<F, P, Data>(evals: &FieldBuffer<P, Data>, point: &[F]) -> Result<F, Error>
30where
31	F: Field,
32	P: PackedField<Scalar = F>,
33	Data: Deref<Target = [P]>,
34{
35	if point.len() != evals.log_len() {
36		return Err(Error::IncorrectArgumentLength {
37			arg: "point".to_string(),
38			expected: evals.log_len(),
39		});
40	}
41
42	// Split coordinates: first half gets at least P::LOG_WIDTH coordinates
43	let first_half_len = (point.len() / 2).max(P::LOG_WIDTH).min(point.len());
44	let (first_coords, remaining_coords) = point.split_at(first_half_len);
45
46	// Generate eq tensor for first half of coordinates
47	let eq_tensor = eq_ind_partial_eval::<P>(first_coords);
48
49	// If there is no second half, just return the inner product with the whole evals.
50	if remaining_coords.is_empty() {
51		return Ok(inner_product_buffers(evals, &eq_tensor));
52	}
53
54	// Calculate chunk size based on first half length
55	let log_chunk_size = first_half_len;
56
57	// Collect inner products of chunks into scalar values
58	let scalars = evals
59		.chunks_par(log_chunk_size)?
60		.map(|chunk| inner_product_buffers(&chunk, &eq_tensor))
61		.collect::<Vec<_>>();
62
63	// Create temporary buffer from collected scalar values
64	let temp_buffer = FieldBuffer::<P>::from_values(&scalars)?;
65
66	// Evaluate remaining coordinates using evaluate_inplace
67	evaluate_inplace(temp_buffer, remaining_coords)
68}
69
70/// Evaluates a multilinear polynomial at a given point, modifying the buffer in-place.
71///
72/// This method computes the evaluation of a multilinear polynomial specified by it's evaluations
73/// on the boolean hypercube. For an $n$-variate multilinear, this implementation performs
74/// $2^n - 1$ field multiplications and allocates no additional memory. The `evals` buffer is
75/// modified in-place.
76///
77/// # Arguments
78/// * `evals` - A [`FieldBuffer`] containing the $2^n$ evaluations over the boolean hypercube
79/// * `coords` - The $n$ coordinates at which to evaluate the polynomial
80///
81/// # Returns
82/// The evaluation of the multilinear polynomial at the given point
83pub fn evaluate_inplace<F, P, Data>(
84	mut evals: FieldBuffer<P, Data>,
85	coords: &[F],
86) -> Result<F, Error>
87where
88	F: Field,
89	P: PackedField<Scalar = F>,
90	Data: DerefMut<Target = [P]>,
91{
92	if coords.len() != evals.log_len() {
93		return Err(Error::IncorrectArgumentLength {
94			arg: "coords".to_string(),
95			expected: evals.log_len(),
96		});
97	}
98
99	// Perform folding for each coordinate in reverse order
100	for &coord in coords.iter().rev() {
101		fold_highest_var_inplace(&mut evals, coord)?;
102	}
103
104	assert_eq!(evals.len(), 1);
105	Ok(evals.get(0).expect("evals.len() == 1"))
106}
107
108#[cfg(test)]
109mod tests {
110	use rand::{RngCore, SeedableRng, rngs::StdRng};
111
112	use super::*;
113	use crate::{
114		inner_product::inner_product_par,
115		test_utils::{
116			B128, Packed128b, index_to_hypercube_point, random_field_buffer, random_scalars,
117		},
118	};
119
120	type P = Packed128b;
121	type F = B128;
122
123	#[test]
124	fn test_evaluate_consistency() {
125		/// Simple reference function for multilinear polynomial evaluation.
126		fn evaluate_with_inner_product<F, P, Data>(
127			evals: &FieldBuffer<P, Data>,
128			point: &[F],
129		) -> Result<F, Error>
130		where
131			F: Field,
132			P: PackedField<Scalar = F>,
133			Data: Deref<Target = [P]>,
134		{
135			if point.len() != evals.log_len() {
136				return Err(Error::IncorrectArgumentLength {
137					arg: "coords".to_string(),
138					expected: evals.log_len(),
139				});
140			}
141
142			// Compute the equality indicator tensor expansion
143			let eq_tensor = eq_ind_partial_eval::<P>(point);
144			let result = inner_product_par(evals, &eq_tensor);
145			Ok(result)
146		}
147
148		let mut rng = StdRng::seed_from_u64(0);
149
150		for log_n in [0, P::LOG_WIDTH - 1, P::LOG_WIDTH, 10] {
151			// Generate random buffer and evaluation point
152			let buffer = random_field_buffer::<P>(&mut rng, log_n);
153			let point = random_scalars::<F>(&mut rng, log_n);
154
155			// Evaluate using all three methods
156			let result_inner_product = evaluate_with_inner_product(&buffer, &point).unwrap();
157			let result_inplace = evaluate_inplace(buffer.clone(), &point).unwrap();
158			let result_sqrt_memory = evaluate(&buffer, &point).unwrap();
159
160			// All results should be equal
161			assert_eq!(result_inner_product, result_inplace);
162			assert_eq!(result_inner_product, result_sqrt_memory);
163		}
164	}
165
166	#[test]
167	fn test_evaluate_at_hypercube_indices() {
168		let mut rng = StdRng::seed_from_u64(0);
169
170		// Generate random multilinear with 8 variables
171		let log_n = 8;
172		let buffer = random_field_buffer::<F>(&mut rng, log_n);
173
174		// Test 16 random hypercube indices
175		for _ in 0..16 {
176			let index = (rng.next_u32() as usize) % (1 << log_n);
177			let point = index_to_hypercube_point::<F>(log_n, index);
178
179			// Evaluate at the hypercube point
180			let eval_result = evaluate(&buffer, &point).unwrap();
181
182			// Get the value directly from the buffer
183			let direct_value = buffer.get(index).unwrap();
184
185			// They should be equal
186			assert_eq!(eval_result, direct_value);
187		}
188	}
189
190	#[test]
191	fn test_linearity() {
192		let mut rng = StdRng::seed_from_u64(0);
193
194		// Generate random 8-variable multilinear and evaluation point
195		let log_n = 8;
196		let buffer = random_field_buffer::<F>(&mut rng, log_n);
197		let mut point = random_scalars::<F>(&mut rng, log_n);
198
199		// Test linearity for each coordinate
200		for coord_idx in 0..log_n {
201			// Choose three coordinate values
202			let coord_vals = random_scalars::<F>(&mut rng, 3);
203
204			// Evaluate at three points differing only in coordinate coord_idx
205			let evals: Vec<_> = coord_vals
206				.iter()
207				.map(|&coord_val| {
208					point[coord_idx] = coord_val;
209					evaluate(&buffer, &point).unwrap()
210				})
211				.collect();
212
213			// Check that the three evaluations form a line
214			// For a line through points (x0, y0), (x1, y1), (x2, y2):
215			// y2 - y0 = (y1 - y0) * (x2 - x0) / (x1 - x0)
216			// Rearranging: (y2 - y0) * (x1 - x0) = (y1 - y0) * (x2 - x0)
217			let x0 = coord_vals[0];
218			let x1 = coord_vals[1];
219			let x2 = coord_vals[2];
220			let y0 = evals[0];
221			let y1 = evals[1];
222			let y2 = evals[2];
223
224			let lhs = (y2 - y0) * (x1 - x0);
225			let rhs = (y1 - y0) * (x2 - x0);
226
227			assert_eq!(lhs, rhs);
228		}
229	}
230}