2026, Jan 07 03:02
Квинтический Эрмит на NumPy vs SciPy BPoly.from_derivatives: разбор и выбор API
Сравниваем квинтическую эрмитову интерполяцию на NumPy и SciPy BPoly.from_derivatives: бенчмарк, причины разрыва и рекомендация — CubicHermiteSpline.
Когда чистая реализация квинтической эрмитовой интерполяции на NumPy обгоняет SciPy BPoly.from_derivatives с большим отрывом, естественно задаться вопросом, откуда берётся разница и как выбрать подходящее API. Ниже — компактное руководство, которое воспроизводит сравнение, объясняет первопричину со слов мейнтейнеров и указывает прагматичный путь вперёд.
Воспроизводим сравнение
Следующий скрипт строит синусоиду из 10 000 точек с аналитическими первой и второй производными, создаёт интерполянт с помощью SciPy BPoly.from_derivatives, а затем делает то же самое вручную на NumPy — квинтический Эрмит через поинтервальные решения 6×6. Скрипт проверяет совпадение значений интерполянтов в узлах и замеряет время построения коэффициентов и вычисления. Имена переменных и функций выбраны для ясности, а вычислительный поток соответствует исходному бенчмарку.
"""
Синусоидальная интерполяция на 10 000 точках: SciPy BPoly.from_derivatives против чистого NumPy (квинтический Эрмит).
Измеряет время построения коэффициентов и время вычисления, а также проверяет численное совпадение.
"""
import sys
import time
import numpy as np
import scipy
from scipy.interpolate import BPoly
# Отчёт об окружении
print(f"Python version: {sys.version.split()[0]}")
print(f"NumPy version: {np.__version__}")
print(f"SciPy version: {scipy.__version__}")
# Сетка и точные значения/производные на [0, 2π]
n_pts = 10_000
grid_x = np.linspace(0.0, 2 * np.pi, n_pts)
f_val = np.sin(grid_x) # f(x)
d1_val = np.cos(grid_x) # f'(x)
d2_val = -np.sin(grid_x) # f''(x)
# === SciPy: BPoly.from_derivatives ===
stacked_y = np.column_stack((f_val, d1_val, d2_val))
t_start = time.perf_counter()
poly_scipy = BPoly.from_derivatives(grid_x, stacked_y)
t_end = time.perf_counter()
scipy_build = t_end - t_start
# Вычисление в исходных узлах
t_start = time.perf_counter()
y_bp = poly_scipy(grid_x)
t_end = time.perf_counter()
scipy_eval = t_end - t_start
# === Pure NumPy: quintic Hermite (per-interval linear solves) ===
def assemble_quintic(xk, f, fp, fpp):
"""
Вычислить коэффициенты квинтического Эрмита на интервалах [xk[i], xk[i+1]].
Возвращает массив формы (6, len(xk)-1) с коэффициентами полинома по интервалам.
"""
m = len(xk) - 1
params = np.zeros((6, m))
for i in range(m):
h = xk[i + 1] - xk[i]
M = np.array([
[1, 0, 0, 0, 0, 0],
[0, 1, 0, 0, 0, 0],
[0, 0, 2, 0, 0, 0],
[1, h, h**2, h**3, h**4, h**5],
[0, 1, 2*h, 3*h**2, 4*h**3, 5*h**4],
[0, 0, 2, 6*h, 12*h**2, 20*h**3],
])
rhs = np.array([f[i], fp[i], fpp[i], f[i + 1], fp[i + 1], fpp[i + 1]])
params[:, i] = np.linalg.solve(M, rhs)
return params
def eval_quintic(xk, coeff, xq):
"""
Вычислить кусочно-квинтический полином в точках xq, используя заранее посчитанные coeff.
xk: точки разбиения (n,), coeff: (6, n-1), xq: набор точек.
"""
idx = np.searchsorted(xk, xq) - 1
idx = np.clip(idx, 0, len(xk) - 2)
dx = xq - xk[idx]
out = np.zeros_like(xq)
for j in range(6):
out += coeff[j, idx] * dx**j
return out
# Построение пользовательских коэффициентов
t_start = time.perf_counter()
quintic_coef = assemble_quintic(grid_x, f_val, d1_val, d2_val)
t_end = time.perf_counter()
np_build = t_end - t_start
# Вычисление в узлах
t_start = time.perf_counter()
y_np = eval_quintic(grid_x, quintic_coef, grid_x)
t_end = time.perf_counter()
np_eval = t_end - t_start
# Проверка совпадения на сетке
assert np.allclose(y_bp, y_np, atol=1e-12), "NumPy and SciPy interpolants differ"
print("Custom NumPy interpolation matches SciPy BPoly for 10 000 points.")
# Отчёт по времени
print(f"SciPy coeff time: {scipy_build:.6f} s")
print(f"SciPy eval time : {scipy_eval:.6f} s")
print(f"Custom coeff time: {np_build:.6f} s")
print(f"Custom eval time : {np_eval:.6f} s")
# Необязательно: визуализация (не требуется для замеров)
import matplotlib.pyplot as plt
plt.plot(grid_x, y_bp, label='SciPy BPoly')
plt.plot(grid_x, y_np, '--', label='Custom NumPy')
plt.legend()
plt.title('Sinusoidal interpolation: SciPy vs NumPy Quintic')
plt.xlabel('x')
plt.ylabel('sin(x)')
plt.show()
В одном из зафиксированных запусков вывод показал, что самописный вариант на NumPy даёт те же значения в узлах и быстрее строит коэффициенты. Тест выполнялся на Python 3.11.7, NumPy 2.1.3 и SciPy 1.15.2.
Что на самом деле даёт разрыв в производительности
BPoly.from_derivatives не получал заметных доработок после начальной реализации (которая, в своё время, заменила интерполятор, более медленный на порядок). На то были разные причины, одна из них — его во многом заменяет CubicHermiteSpline. В реальных проектах мы почти не видели его использования, поэтому и мотивации уделять ему внимание было мало. Если вы им пользуетесь или стали бы пользоваться, будь он быстрее, не стесняйтесь прислать pull request. Мы с радостью его рассмотрим.
Ключевой момент в том, что BPoly.from_derivatives — это общий путь, который не оптимизировали целенаправленно, в том числе потому, что его в основном заменил CubicHermiteSpline и он редко используется. Наблюдаемый разрыв — следствие инженерных приоритетов, а не ограничение всей системы интерполяции SciPy.
Дополнительное наблюдение автора бенчмарка: дальнейшая векторизация построителя поинтервальных коэффициентов позволяет убрать явный цикл, однако прирост на протестированных системах был ничтожным. Это не меняет ключевого вывода о сравнительной производительности для данной задачи.
Практический курс: используйте оптимизированный путь
Когда постановка задачи позволяет, более специализированный и поддерживаемый CubicHermiteSpline — современная альтернатива BPoly.from_derivatives. Его прямо называют преемником, и он получает больше внимания. Ниже показано, как построить и вычислить его на той же синусоиде, используя значения функции и первые производные.
from scipy.interpolate import CubicHermiteSpline
# Построить кубический эрмитов сплайн по значениям и первым производным
start = time.perf_counter()
chs = CubicHermiteSpline(grid_x, f_val, d1_val)
stop = time.perf_counter()
chs_build = stop - start
# Вычисление в исходных узлах
start = time.perf_counter()
chs_y = chs(grid_x)
stop = time.perf_counter()
chs_eval = stop - start
print(f"CubicHermiteSpline coeff time: {chs_build:.6f} s")
print(f"CubicHermiteSpline eval time : {chs_eval:.6f} s")
Опыт, приведённый вместе с сравнением, согласуется с этой рекомендацией.
Это правильный ответ. Я ещё больше оптимизировал реализацию numpy-numba, сверх того, что сделал @JeromeRichard в своём ответе. При значительных усилиях я вышел на сопоставимый уровень с CubicHermiteSpline, но на моей машине CubicHermiteSpline всё же оказался быстрее. Я бы не рекомендовал обновлять более общий from_derivatives. Было бы полезно поправить документацию SciPy, в частности добавить указатель на более оптимизированные варианты при рассмотрении общих опций интерполяции. Команда SciPy, отличная работа!
Почему это важно инженерам
Выводы о производительности зависят от того, какое именно API вы используете. Универсальная рутина, рассчитанная на широкие возможности, может быть настроена хуже, чем узкоспециализированный конструктор сплайна, а компактная реализация на NumPy легко обгонит неотрефакторенный код, когда математика проста и предсказуема. Понимание того, какие функции в библиотеке активно поддерживаются, а какие — наследие или заменены, напрямую влияет на результат ваших бенчмарков и, в конечном итоге, на производственные решения.
Выводы и рекомендации
Если вы видите большой разрыв между самописным интерполятором на NumPy и SciPy BPoly.from_derivatives, скорее всего, это отражает отсутствие недавних оптимизаций в этой конкретной функции. Предпочитайте CubicHermiteSpline, когда он подходит под ваши ограничения. Если вашему процессу действительно нужен BPoly.from_derivatives и требуется больше скорости, подумайте о вкладах в апстрим — мейнтейнеры готовы рассматривать pull request’ы. Для локальной доводки простой NumPy может быть весьма эффективен, а более глубокие микрооптимизации поверх ясной реализации могут мало что изменить — в зависимости от системы.