Accelerating Special Function Evaluations in Python via Interpolation
Bessel functions of the first kind, accessible via scipy.special.jv, rely on recursive and iterative algorithms that demand significant CPU resources compared to basic algebraic operations.
To quantify this bottleneck, consider a banchmark comparing jv against a straightforward polynomial function.
import numpy as np
import time
from scipy.special import jv as compute_bessel
basic_math = lambda arr: 2.5 * arr**2 + 1.2
POINTS = 1500
LOOPS = 8000
BESSEL_ORDER = 4
data_arr = np.linspace(-25.0, 25.0, POINTS)
output_arr = np.zeros_like(data_arr)
t_start = time.perf_counter()
for _ in range(LOOPS):
output_arr = basic_math(data_arr)
t_end = time.perf_counter()
print(f"Basic Math Execution Time: {t_end - t_start:.6f} seconds")
t_start = time.perf_counter()
for _ in range(LOOPS):
output_arr = compute_bessel(BESSEL_ORDER, data_arr)
t_end = time.perf_counter()
print(f"Bessel Function Execution Time: {t_end - t_start:.6f} seconds")
Executing the test reveals a massive disparity. The Bessel function evaluation takes substantially longer—often hundreds of times more than the simple polynomial. Repeating the loops confirms this overhead scales linearly, highlighting the heavy computational burden of the underlying iterative calculations. To avoid redundant processing, precmoputing values and reusing them is essential.
Interpolation offers a practical solution. By computing the function on a dense grid upfront and querying the cached results, we can bypass expensive recursive evaluations. The interp1d utility from SciPy facilitates this approach.
from scipy.interpolate import interp1d
BOUND = 40.0
grid_pts = np.linspace(-BOUND, BOUND, 10000)
math_approx = interp1d(grid_pts, basic_math(grid_pts),
kind="linear",
bounds_error=False,
fill_value=np.nan)
bessel_approx = interp1d(grid_pts, compute_bessel(BESSEL_ORDER, grid_pts),
kind="linear",
bounds_error=False,
fill_value=np.nan)
Both functions now have lookup-based counterparts. The interpolation domain is restricted to [-BOUND, BOUND], returning NaN for out-of-bounds queries. Linear interpolation is applied initially.
Timing the interpolated versions yields a dramatic speedup for the Bessel function, reducing execution time from tens of seconds to fractions of a second—roughly a 40x performance gain. Conversely, the simple polynomial becomes slower due to the overhead of the lookup mechanism, indicating that interpolation is only beneficial when the original function's evaluation cost exceeds the lookup cost.
Maintaining numerical accuracy is critical. By selecting appropriate sample densities and interpolation degrees, float64 precision is achievable.
Calculating the base-10 logarithm of the absolute difference between exact and interpolated results allows for precision validation. Testing various polynomial degrees supported by interp1d reveals the impact on accuracy.
dense_grid = np.linspace(-BOUND, BOUND, 20000)
import matplotlib.pyplot as plt
degrees = [1, 3, 5, 7, 9]
log_diff = lambda v1, v2: np.log10(np.abs(v1 - v2) + 1e-300)
approximators = []
for deg in degrees:
approximators.append(
interp1d(dense_grid, compute_bessel(BESSEL_ORDER, dense_grid),
kind=deg,
bounds_error=False,
fill_value=np.nan)
)
ground_truth = compute_bessel(BESSEL_ORDER, data_arr)
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 4))
ax1.set_xlabel("Input")
ax1.set_ylabel("$J_n(x)$")
ax1.plot(data_arr, ground_truth)
ax2.set_xlabel("Input")
ax2.set_ylabel("Log10 Absolute Error")
for idx, approximator in enumerate(approximators):
estimated = approximator(data_arr)
error_mag = log_diff(ground_truth, estimated)
ax2.plot(data_arr, error_mag, label=f"Degree {degrees[idx]}")
ax2.legend()
plt.tight_layout()
plt.show()
The resulting plots show that for polynomial degrees of 5 or higher, the absolute error drops below $10^{-15}$, satisfying strict numerical requirements. The relative error similarly falls below $10^{-14}$.
While higher-order interpolation and denser grids introduce some computational overhead compared to simple linear interpolation, the performance benefits remain substantial. The Bessel function evaluation drops from over 15 seconds to roughly 1.3 seconds—an 11x acceleration without compromising data integrity.