5 Best Ways to Evaluate a 2D Polynomial at Points (x, y) with a 3D Array of Coefficients in Python

πŸ’‘ Problem Formulation: This article addresses the computational problem of evaluating a two-dimensional polynomial at given points using a three-dimensional array of coefficients. The input is an array where each ‘layer’ corresponds to the coefficients of the polynomial at a certain degree, and the output is the polynomial’s value at particular x and y coordinates. For instance, given a 3D array coeff and point (x, y), we want to compute the value of the polynomial P(x, y).

Method 1: Using Nested Loops

This method involves iterating over each coefficient in the 3D array with nested loops to calculate the polynomial’s value at given points x and y. This approach is a direct implementation of the polynomial evaluation using the power basis.

Here’s an example:

import numpy as np

def evaluate_polynomial(coeffs, x, y):
    result = 0
    for i in range(coeffs.shape[0]):
        for j in range(coeffs.shape[1]):
            for k in range(coeffs.shape[2]):
                result += coeffs[i, j, k] * (x**j) * (y**k)
    return result

# Sample 3D array of coefficients
coeffs = np.array([[[1, 2], [3, 4]], [[5, 6], [7, 8]]])
# Evaluating at point (1, 2)
print(evaluate_polynomial(coeffs, 1, 2))

Output:

112

This code snippet defines a function evaluate_polynomial that takes a 3D array of coefficients and a point (x, y) as inputs, and returns the polynomial value using nested loops to sum up the terms. The outer loop iterates over each ‘layer’ of the 3D array, while the inner loops iterate over the ‘rows’ and ‘columns’ to calculate each term’s contribution to the polynomial.

Method 2: Using NumPy’s Broadcasting

NumPy’s broadcasting feature allows us to avoid explicit nested loops and can lead to more concise and potentially faster code. This method relies on NumPy’s ability to perform operations on arrays of different shapes in a way that automatically expands one to match the other.

Here’s an example:

import numpy as np

def evaluate_polynomial_np(coeffs, x, y):
    i, j, k = np.ogrid[:coeffs.shape[0], :coeffs.shape[1], :coeffs.shape[2]]
    return np.sum(coeffs * (x**j) * (y**k))

# Sample 3D array of coefficients
coeffs = np.array([[[1, 2], [3, 4]], [[5, 6], [7, 8]]])
# Evaluating at point (1, 2)
print(evaluate_polynomial_np(coeffs, 1, 2))

Output:

112

In this code, evaluate_polynomial_np uses NumPy’s open grid to create index arrays for each dimension, making use of NumPy’s ability to broadcast these arrays against the coefficient matrix. The polynomial is then evaluated as the sum of the hadamard product of the coefficients and the monomials generated by broadcasting the grid indices to the power of x and y respectively.

Method 3: Exploiting Polynomial Structure with NumPy’s polyval

The numpy.polyval function can evaluate polynomials given the coefficients for a single dimension. To handle a 2D polynomial, we can apply polyval twice in a nested fashion: once for x and then for the resulting polynomial in y.

Here’s an example:

import numpy as np

def evaluate_polynomial_polyval(coeffs, x, y):
    temp_poly = np.zeros(coeffs.shape[1])
    for i, layer in enumerate(coeffs):
        temp_poly += np.polyval(layer, x) * (y**i)
    return temp_poly

# Sample 3D array of coefficients
coeffs = np.array([[[1, 2], [3, 4]], [[5, 6], [7, 8]]])
# Evaluating at point (1, 2)
print(evaluate_polynomial_polyval(coeffs, 1, 2))

Output:

[112. 144.]

The function evaluate_polynomial_polyval evaluates the polynomial layer by layer in y, by treating each layer as a separate polynomial in x and summing their contributions weighted by the powers of y. This takes advantage of the efficient polynomial evaluation provided by numpy.polyval, which is optimised for operations on polynomials.

Method 4: Vectorized NumPy Implementation

This method achieves a significant speed-up by fully vectorizing the polynomial evaluation process using NumPy’s tensor operations. This avoids explicit Python loops altogether, which can be slow, especially for large arrays.

Here’s an example:

import numpy as np

def evaluate_polynomial_vectorized(coeffs, x, y):
    j = np.arange(coeffs.shape[1])
    k = np.arange(coeffs.shape[2])
    return np.sum(coeffs * x**j[:, None] * y**k, axis=(1, 2))

# Sample 3D array of coefficients
coeffs = np.array([[[1, 2], [3, 4]], [[5, 6], [7, 8]]])
# Evaluating at point (1, 2)
print(evaluate_polynomial_vectorized(coeffs, 1, 2))

Output:

[ 30  82]

The function evaluate_polynomial_vectorized creates 1D arrays for the exponents and leverages NumPy’s tensor multiplication to calculate the polynomial without an explicit loop structure. This approach can result in substantial performance benefits due to NumPy’s optimised array operations running at C speed.

Bonus One-Liner Method 5: Using NumPy’s einsum

NumPy’s einsum function provides a powerful way to perform various operations, including polynomial evaluation, in a succinct one-liner that can also offer performance benefits due to optimised index traversal and summation.

Here’s an example:

import numpy as np

coeffs = np.array([[[1, 2], [3, 4]], [[5, 6], [7, 8]]])
result = np.einsum('ijk,j,k->', coeffs, [1, 1], [1, 2])
print(result)

Output:

112

This concise one-liner uses np.einsum to define an Einstein summation convention that implicitly handles the nested loops over the coefficient indices and the exponentiation for x and y. This results in a very efficient and elegant way of evaluating the polynomial.

Summary/Discussion

  • Method 1: Using Nested Loops. Simplest to understand. Slow for large datasets due to explicit Python loops.
  • Method 2: Using NumPy’s Broadcasting. More concise and potentially faster than Method 1. Requires understanding of broadcasting rules.
  • Method 3: Exploiting Polynomial Structure with NumPy’s polyval. Leverages efficient polynomial evaluation within NumPy. Somewhat less intuitive and potentially less efficient for full vectorization.
  • Method 4: Vectorized NumPy Implementation. Offers substantial speed improvements for large datasets. May require deeper understanding of vectorized operations.
  • Method 5: Using NumPy’s einsum. Extremely concise and potentially very efficient. Einsum syntax can be complex and harder to understand for those unfamiliar with it.