Добавление двумерных массивов numpy с различными осями: как правильно заменить устаревшую interp2d на RectBivariateSpline?

Вопрос или проблема

Мне нужно сложить два двумерных массива numpy, которые могут иметь разные формы и разные соответствующие массивы осей.

Что я имею в виду: давайте определим два разных набора осей x и y, и рассчитаем значения z в соответствии с какой-либо функцией (здесь: двумерное гауссово распределение):

import numpy as np
import matplotlib.pyplot as plt

def gaussian_2D(X, Y, amplitude, mu_x, mu_y, sigma_x, sigma_y, theta, offset=0):
    """
    X,Y: должны быть сетками numpy
    """
    a = (np.cos(theta)**2)/(2*sigma_x**2) + (np.sin(theta)**2)/(2*sigma_y**2)
    b = -(np.sin(2*theta))/(4*sigma_x**2) + (np.sin(2*theta))/(4*sigma_y**2)
    c = (np.sin(theta)**2)/(2*sigma_x**2) + (np.cos(theta)**2)/(2*sigma_y**2)
    return offset + amplitude*np.exp( - (a*((X-mu_x)**2) + 2*b*(X-mu_x)*(Y-mu_y) + c*((Y-mu_y)**2))

x1 = np.linspace(10, 100, num=100)
y1 = np.linspace(0, 200, num=100)
X1,Y1 = np.meshgrid(x1,y1)
Z1 = gaussian_2D(X1,Y1, 10, 35, 100, 10, 20, 0)

x2 = np.linspace(0, 150, num=120)
y2 = np.linspace(30, 220, num=120)
X2,Y2 = np.meshgrid(x2,y2)
Z2 = gaussian_2D(X2,Y2, 10, 75, 150, 5, 4, 12)

Вышеуказанный код генерирует 2 различных массива x длиной (100,) и (120,), 2 различных массива y длиной (100,) и (120,) и 2 массива z формы (100,100) и (120,120).

Массивы x и y определяют некоторую физическую размерность, здесь пространство в миллиметрах, так что имеет смысл сложить их вместе, даже если исходные массивы разные. Я выбрал эти оси перекрывающимися, но на самом деле это не обязательно, массивы могут быть полностью раздельными. Графики показывают, что происходит:

fig1, ax1 = plt.subplots()
ax1.pcolormesh(X1,Y1,Z1)
ax1.set_xlim([0,150])
ax1.set_ylim([0,220])
ax1.set_xlabel("x [мм]")
ax1.set_ylabel("y [мм]")

fig2, ax2 = plt.subplots()
ax2.pcolormesh(X2,Y2,Z2)
ax2.set_xlim([0,150])
ax2.set_ylim([0,220])
ax2.set_xlabel("x [мм]")
ax2.set_ylabel("y [мм]")

результатом будет:

график массива1

график массива2

И мы можем получить представление о том, как должен выглядеть результирующий график при сложении двух массивов, просто наложив их друг на друга с некоторым значением alpha:

fig3, ax3 = plt.subplots()
ax3.pcolormesh(X1, Y1, Z1, alpha=0.5)
ax3.pcolormesh(X2, Y2, Z2, alpha=0.3)
ax3.set_xlabel("x [мм]")
ax3.set_ylabel("y [мм]")

график массива1 и массива2 наложенные друг на друга


Теперь, очевидно, поскольку структура исходных массивов различна, мы должны использовать некоторую интерполяцию, чтобы “стандартизировать” два массива на общей сетке. Для удобства сделаем так, чтобы результирующий массив всегда имел форму (100, 100) (т.е. приближенно такую же форму, как и два входных массива). Это предположение обычно соответствует моим данным… давайте проигнорируем пограничные случаи…)

def add_arrays_with_axes(array_1, array_1_x, array_1_y, array_2, array_2_x, array_2_y, method="interp2d"):
    import scipy.interpolate as interp

    # Определим новые диапазоны осей x и y, которые охватывают обе оси array_1 и array_2
    new_x = np.linspace(min(array_1_x[0], array_2_x[0]), max(array_1_x[-1], array_2_x[-1]), num=100)
    new_y = np.linspace(min(array_1_y[0], array_2_y[0]), max(array_1_y[-1], array_2_y[-1]), num=100)

    # Интерполируем array_1 и array_2 на новой сетке
    match method:
        case "interp2d":
            interp_array_1 = interp.interp2d(array_1_x, array_1_y, array_1, kind='cubic', bounds_error=False, fill_value=np.nan)
            interp_array_2 = interp.interp2d(array_2_x, array_2_y, array_2, kind='cubic', bounds_error=False, fill_value=np.nan)

        case "RectBivariateSpline":
            interp_array_1 = interp.RectBivariateSpline(array_1_x, array_1_y, array_1)
            interp_array_2 = interp.RectBivariateSpline(array_2_x, array_2_y, array_2)

        case _:
            raise Exception("Неподдерживаемый метод интерполяции.")

    # Оцениваем интерполяции на новой сетке x и y
    array_1_resampled = interp_array_1(new_x, new_y)
    array_2_resampled = interp_array_2(new_x, new_y)

    # Заменяем NaN на 0 в каждом массиве, где данные изначально не определены
    array_1_resampled = np.nan_to_num(array_1_resampled, nan=0.0)
    array_2_resampled = np.nan_to_num(array_2_resampled, nan=0.0)

    # Складываем переинтерполированные массивы
    summed_array = array_1_resampled + array_2_resampled

    return summed_array, new_x, new_y

В приведенном выше коде я допускаю использование двух различных методов интерполяции: interp2d и RectBivariateSpline.

Давайте посмотрим, каков будет результат:

interp2d

new_array, new_x, new_y = add_arrays_with_axes(Z1, x1, y1, Z2, x2, y2, method="interp2d")
new_mesh = np.meshgrid(new_x, new_y)

fig4, ax4 = plt.subplots()
ax4.pcolormesh(X1, Y1, Z1, alpha=0.5)
ax4.pcolormesh(X2, Y2, Z2, alpha=0.3)
ax4.set_xlabel("x [мм]")
ax4.set_ylabel("y [мм]")

ax4.pcolormesh(*new_mesh, new_array)

результат с использованием interp2d


RectBivariateSpline

new_array, new_x, new_y = add_arrays_with_axes(Z1, x1, y1, Z2, x2, y2, method="RectBivariateSpline")
new_mesh = np.meshgrid(new_x, new_y)

fig5, ax5 = plt.subplots()
ax5.pcolormesh(X1, Y1, Z1, alpha=0.5)
ax5.pcolormesh(X2, Y2, Z2, alpha=0.3)
ax5.set_xlabel("x [мм]")
ax5.set_ylabel("y [мм]")

ax5.pcolormesh(*new_mesh, new_array)

результат с использованием RectBivariateSpline

Очевидно, что два метода показывают очень разные результаты

interp2d показывает именно то, что я ожидал: значения получаются только там, где исходные массивы были определены, и нули там, где они не были определены, что приводит к видимому краю при x=10.
RectBivariateSpline, с другой стороны, похоже, не только растягивает данные, но и изменяет их… Сравните, например, центральную точку более широкой гауссовой функции. Она была определена как (x=35, y=100), и interp2d воспроизводит это правильно, но в случае RectBivariateSpline центр сдвинулся куда-то ближе к (x=35, y=80). Почему это происходит? Кажется, что interp2d — это правильный путь, но эта функция на самом деле устарела. RectBivariateSpline имеет еще один, возможно, релевантный аргумент: bbox, но я не уверен, что он делает, и что мне следует установить, кроме оригинального ограничивающего прямоугольника исходного массива, который у него по умолчанию… Плюс к тому же, кажется, есть некоторая проблема с этим аргументом.

Есть ли у вас идеи относительно того, что происходит с RectBivariateSpline?

Посмотрев на __call__ метод RectBivariateSpline, мы видим, что код ожидает точки в формате indexing="ij", в то время как np.meshgrid по умолчанию использует indexing="xy" (не спрашивайте меня, почему). Чтобы исправить это, вам нужно будет внести два изменения:

  1. Транспонировать массивы данных при создании объектов RectBivariateSpline.
case "RectBivariateSpline":
    interp_array_1 = interp.RectBivariateSpline(array_1_x, array_1_y, array_1.T)
    interp_array_2 = interp.RectBivariateSpline(array_2_x, array_2_y, array_2.T)
  1. Создать new_mesh с использованием порядка "ij".
new_mesh = np.meshgrid(new_x, new_y, indexing="ij")

Это даст результаты, сопоставимые с interp2d.

Также следует учитывать, что RectBivariateSpline экстраполирует, в то время как interp2d заполняет неизвестные значения (в вашем случае вы заполняете np.nan). Это станет проблемой только в том случае, если у вас есть какая-либо структура около/на границе одного набора данных, и эта граница является общей для двух наборов данных.

Ответ или решение

Чтобы добавить две двумерные numpy-массива с различными осями, необходимо выполнить интерполяцию данных в унифицированной системе координат. Данный процесс можно осуществить при помощи функции RectBivariateSpline, так как ранее используемая interp2d устарела и теперь не рекомендуемая для использования.

1. Определение задачи

Предположим, у нас имеются два двумерных массива Z1 и Z2, которые мы хотим сложить. Они имеют разные координатные оси. Примеры массивов показаны ниже:

x1 = np.linspace(10, 100, num=100)
y1 = np.linspace(0, 200, num=100)
X1, Y1 = np.meshgrid(x1, y1)
Z1 = gaussian_2D(X1, Y1, 10, 35, 100, 10, 20, 0)

x2 = np.linspace(0, 150, num=120)
y2 = np.linspace(30, 220, num=120)
X2, Y2 = np.meshgrid(x2, y2)
Z2 = gaussian_2D(X2, Y2, 10, 75, 150, 5, 4, 12)

В данном случае массивы Z1 и Z2 имеют размеры (100,100) и (120,120) соответственно. Чтобы их сложить, необходимо провести интерполяцию.

2. Использование RectBivariateSpline

Чтобы заменить interp2d на RectBivariateSpline, важно учитывать, что RectBivariateSpline ожидает входные данные в формате индексации "ij", в то время как numpy.meshgrid по умолчанию использует формат индексации "xy". Это может привести к искажению данных при интерполяции.

2.1 Коррекция кода

Для успешного выполнения интерполяции необходимо внести следующие изменения в реализацию функции:

  1. Транспонирование данных: При создании объектов RectBivariateSpline данные массивов Z1 и Z2 нужно транспонировать:
interp_array_1 = interp.RectBivariateSpline(array_1_x, array_1_y, array_1.T)
interp_array_2 = interp.RectBivariateSpline(array_2_x, array_2_y, array_2.T)
  1. Использование индексирования "ij": Создание нового массива meshgrid следует выполнить с параметром indexing="ij":
new_mesh = np.meshgrid(new_x, new_y, indexing="ij")

2.2 Полная реализация функции

Ниже представлен полный код функции для сложения массивов с помощью интерполяции:

import numpy as np
import scipy.interpolate as interp

def add_arrays_with_axes(array_1, array_1_x, array_1_y, array_2, array_2_x, array_2_y):
    new_x = np.linspace(min(array_1_x[0], array_2_x[0]), max(array_1_x[-1], array_2_x[-1]), num=100)
    new_y = np.linspace(min(array_1_y[0], array_2_y[0]), max(array_1_y[-1], array_2_y[-1]), num=100)

    interp_array_1 = interp.RectBivariateSpline(array_1_x, array_1_y, array_1.T)
    interp_array_2 = interp.RectBivariateSpline(array_2_x, array_2_y, array_2.T)

    array_1_resampled = interp_array_1(new_x, new_y)
    array_2_resampled = interp_array_2(new_x, new_y)

    array_1_resampled = np.nan_to_num(array_1_resampled, nan=0.0)
    array_2_resampled = np.nan_to_num(array_2_resampled, nan=0.0)

    summed_array = array_1_resampled + array_2_resampled

    return summed_array, new_x, new_y

3. Обратная связь по результатам

Использование вышеописанного подхода позволило добиться согласованности интерполяций, что позволяет корректно суммировать массивы Z1 и Z2. Важно заметить, что RectBivariateSpline выполняет экстраполяцию данных, тогда как interp2d заменяет неизвестные значения на NaN; это может оказать влияние на результирующие значения, особенно если данные находятся на границах заданных осей.

Финальная проверка и визуализация результатов позволят вам убедиться в корректности интерполяции, а также обеспечат ясное представление о том, как массивы выглядят после наложения.

Таким образом, использование RectBivariateSpline предоставляет более современные и эффективные методы для интерполяции двумерных массивов, что позволяет корректно обрабатывать задачи с разными осями и формами массивов.

Оцените материал
Добавить комментарий

Капча загружается...