2025, Nov 22 12:02
Пакетное вычисление собственных значений и векторов 4×4 в NumPy без циклов
Векторизация вычисления собственных значений и векторов матриц 4×4 в NumPy: сборка батча, np.linalg.eigh за один вызов и сортировка без Python-циклов быстро.
Векторизация задач линейной алгебры в NumPy особенно оправдана, когда одну и ту же операцию приходится повторять для множества входных данных. Типичный случай — вычисление собственных значений и собственных векторов для структурированной матрицы 4×4, зависящей от нескольких параметров. Цель здесь — запустить весь пакет сразу, без явного цикла на Python.
Базовый вариант: разложение на собственные значения и векторы для одного вызова
Следующая функция строит матрицу 4×4 из входов (x1, y1, x2, y2), затем вычисляет и сортирует её собственные значения и собственные векторы с помощью np.linalg.eigh.
import numpy as np
def phi(px, py):
return px + 1j * py
def phi_bar(px, py):
return np.conj(phi(px, py))
def solve_block(u1, v1, u2, v2):
alpha = 10
u = u1 + u2
v = v1 + v2
M = np.array([
[alpha, phi(u, v), phi(u, v), phi_bar(u, v)],
[phi_bar(u, v), alpha, 0, phi(u, v)],
[phi_bar(u, v), 0, -alpha, phi(u, v)],
[phi(u, v), phi_bar(u, v), phi_bar(u, v), -alpha]
])
w, V = np.linalg.eigh(M)
order = np.argsort(w)
return w[order], V[:, order]
Что мешает векторизации
Эта реализация работает со скалярами и возвращает результат для одной матрицы. Когда на вход поступают одномерные массивы одинаковой формы, вызывать функцию в цикле нежелательно. Задача — вычислить все собственные значения/векторы за один проход, пакетно, при этом сохранить прежний способ построения матрицы и порядок сортировки.
Векторизованный подход
Идея в том, чтобы собрать «стек» массивов формы “batch × 4 × 4” и передать его напрямую в np.linalg.eigh. Кроме того, стоит избежать повторных вычислений одинаковых подвыражений: один раз посчитать их и переиспользовать при формировании пакета.
def solve_batched(u1_arr, v1_arr, u2_arr, v2_arr):
alpha = 10
u_arr = u1_arr + u2_arr
v_arr = v1_arr + v2_arr
z = phi(u_arr, v_arr)
z_conj = phi_bar(u_arr, v_arr)
H = np.stack([
np.stack([np.full_like(z, alpha), z, z, z_conj], axis=-1),
np.stack([z_conj, np.full_like(z, alpha), np.zeros_like(z), z], axis=-1),
np.stack([z_conj, np.zeros_like(z), -np.full_like(z, alpha), z], axis=-1),
np.stack([z, z_conj, z_conj, -np.full_like(z, alpha)], axis=-1)
], axis=-2)
evals, evecs = np.linalg.eigh(H)
idx = np.argsort(evals, axis=-1)
evals = np.take_along_axis(evals, idx, axis=-1)
evecs = np.take_along_axis(evecs, idx[..., None], axis=-1)
return evals, evecs
Почему это работает
Объединив матрицы в один массив, мы получаем разложение на собственные значения и векторы для всего пакета за единичный вызов. Предварительное вычисление phi(u, v) и его сопряжения избавляет от лишней работы при заполнении блоков. Финальный шаг повторяет базовый вариант: собственные значения и векторы последовательно сортируются вдоль последней оси.
Зачем это нужно
Этот приём позволяет находить все собственные значения и векторы для множества наборов входных данных без явного цикла на Python. Конструкция матрицы остаётся прежней; меняется лишь представление данных — они формируются в пакет, который обрабатывается за один раз.
Выводы
Если матрица зависит от покомпонентных выражений над входными массивами, сначала вычислите общие промежуточные величины, затем соберите стопку 4×4-блоков по каждому входу и, наконец, один раз вызовите np.linalg.eigh для всего пакета. Постобработку оставляйте такой же, как в скалярной версии: сортируйте пары собственных значений и векторов вдоль последней оси, чтобы последующий код получал устойчивый порядок.