# QR-алгоритм для вычисления собственных значений матриц

Этот модуль реализует QR-алгоритм, итерационный метод для нахождения собственных значений матрицы.  Он включает базовую версию алгоритма, а также версию со сдвигами для ускорения сходимости.  Реализация использует библиотеку Numba для повышения производительности за счет JIT-компиляции функций.

## Теоретическая справка

### QR-алгоритм

QR-алгоритм — это метод вычисления всех собственных значений матрицы.  Он основан на следующем принципе:

1. **QR-разложение:**  Любая квадратная матрица $A$ может быть разложена в произведение ортогональной матрицы $Q$ и верхней треугольной матрицы $R$:  $A = QR$.
2. **Итерация:**  На каждом шаге алгоритма выполняется следующее преобразование:
   * Вычисляется QR-разложение текущей матрицы $A_k$:  $A_k = Q_k R_k$.
   * Формируется новая матрица $A_{k+1}$ путем перемножения $R_k$ и $Q_k$ в обратном порядке:  $A_{k+1} = R_k Q_k$.

Матрицы $A_k$ и $A_{k+1}$ подобны и, следовательно, имеют одинаковые собственные значения.  При определенных условиях последовательность матриц $A_k$ сходится к верхней треугольной матрице, на диагонали которой расположены собственные значения исходной матрицы $A$.

### QR-алгоритм со сдвигами

Для ускорения сходимости QR-алгоритма используется метод сдвигов.  Идея заключается в том, чтобы на каждом шаге вычитать из матрицы $A_k$ некоторую величину $\mu_k$, называемую сдвигом, умноженную на единичную матрицу $I$:

1. **Сдвиг:**  Вычисляется сдвиг $\mu_k$, который обычно выбирается равным последнему диагональному элементу матрицы $A_k$:  $\mu_k = A_k[n-1, n-1]$.
2. **QR-разложение сдвинутой матрицы:**  Вычисляется QR-разложение матрицы $A_k - \mu_k I$:  $A_k - \mu_k I = Q_k R_k$.
3. **Обратное перемножение и добавление сдвига:**  Формируется новая матрица $A_{k+1}$ по формуле:  $A_{k+1} = R_k Q_k + \mu_k I$.

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

## Описание функций

### `QR_step_njit(Ak, FLOAT_TOLERANCE=1e-12)`

Выполняет один шаг базового QR-алгоритма.

**Параметры:**

* `Ak` (`np.ndarray`):  Входная квадратная матрица.
* `FLOAT_TOLERANCE` (`float`, по умолчанию: 1e-12):  Пороговое значение для проверки вырожденности матриц.

**Возвращает:**

* `Ak_next` (`np.ndarray`):  Матрица, полученная после одного шага QR-алгоритма.  Если QR-разложение невозможно, возвращает матрицу, заполненную `np.nan`.

**Пример:**

```python
import numpy as np
from matplobblib.nm.eigen.qr import QR_step_njit  # Предполагается, что код сохранен в matplobblib/nm/eigen/qr/__init__.py

A = np.array([[4, 3], [6, 3]])
A_next = QR_step_njit(A)
print("A_next:\n", A_next)
```

### `QR_step_shift_njit(Ak, FLOAT_TOLERANCE=1e-12)`

Выполняет один шаг QR-алгоритма со сдвигом.

**Параметры:**

* `Ak` (`np.ndarray`):  Входная квадратная матрица.
* `FLOAT_TOLERANCE` (`float`, по умолчанию: 1e-12):  Пороговое значение для проверки вырожденности матриц.

**Возвращает:**

* `Ak_next` (`np.ndarray`):  Матрица, полученная после одного шага QR-алгоритма со сдвигом.  Если QR-разложение невозможно, возвращает матрицу, заполненную `np.nan`.

**Пример:**

```python
import numpy as np
from matplobblib.nm.eigen.qr import QR_step_shift_njit

A = np.array([[4, 3], [6, 3]])
A_next = QR_step_shift_njit(A)
print("A_next:\n", A_next)
```

### `QR_alg(A, tol=1e-9, miter=1000)`

Выполняет QR-алгоритм с сдвигами для нахождения собственных значений матрицы.

**Параметры:**

* `A` (`np.ndarray`):  Входная квадратная матрица.
* `tol` (`float`, по умолчанию: 1e-9):  Пороговое значение для проверки сходимости (по поддиагональным элементам).
* `miter` (`int`, по умолчанию: 1000):  Максимальное количество итераций.

**Возвращает:**

* `eigenvalues` (`np.ndarray`):  Массив собственных значений матрицы.  Если алгоритм не сходится за `miter` итераций, возвращает приближенные значения из текущей матрицы.

**Примеры:**

```python
import numpy as np
from matplobblib.nm.eigen.qr import QR_alg

# Пример 1: Симметричная матрица
A = np.array([[4, 1, -2], [1, 2, 0], [-2, 0, 3]])
eigenvalues = QR_alg(A)
print("Собственные значения:\n", eigenvalues)

# Пример 2: Матрица без сходимости
A = np.array([[0, 1, 0], [0, 0, 1], [0, 0, 0]])
eigenvalues = QR_alg(A, miter=10)
print("Собственные значения (не сошелся):\n", eigenvalues)
```

## Зависимости

* `numba`:  Для JIT-компиляции функций и повышения производительности.
* `numpy`:  Для работы с матрицами и массивами.
* `tqdm`:  Для отображения прогресс-бара во время выполнения алгоритма.
* `matplobblib.matrices`:  Модуль, содержащий вспомогательные функции для работы с матрицами (предполагается, что он существует в вашей библиотеке).  В частности, требуются функции:
  * `multiply_matrices_njit`:  Для умножения матриц.
  * `scalar_multiply_matrix_njit`:  Для умножения матрицы на скаляр.
  * `subtract_matrices_njit`:  Для вычитания матриц.
  * `QR_dec`:  Для выполнения QR-разложения матрицы.

## Использование

1. Убедитесь, что у вас установлены все необходимые зависимости.  Вы можете установить их с помощью pip:

   ```bash
   pip install numba numpy tqdm
   ```
2. Сохраните код модуля (файл `__init__.py`) в директории `matplobblib/nm/eigen/qr/`.
3. Импортируйте и используйте функции из модуля в вашем коде, как показано в примерах выше.

## Замечания

* QR-алгоритм гарантированно сходится только для диагонализуемых матриц.  Для недиагонализуемых матриц алгоритм может не сойтись или сходиться к псевдотреугольной форме.
* Численная устойчивость алгоритма обеспечивается проверкой нормы матриц `Q` и `R` при QR-разложении.  Если норма меньше заданного порога (`FLOAT_TOLERANCE`), возвращается матрица, заполненная `np.nan`, что указывает на проблему с разложением.
* Использование Numba значительно ускоряет вычисления за счет компиляции функций в машинный код.
* Выбор сдвига $\mu_k = A_k[n-1, n-1]$ является наиболее простым вариантом.  Существуют более сложные стратегии выбора сдвигов, которые могут обеспечить еще более быструю сходимость, особенно для матриц с комплексными собственными значениями (например, двойной сдвиг).
* Реализация предполагает, что вспомогательные функции для работы с матрицами находятся в модуле `matplobblib.matrices`.  Убедитесь, что этот модуль существует и содержит необходимые функции.
* Для работы с прогресс-баром требуется установленная библиотека `tqdm`.  Если она не установлена, прогресс-бар не будет отображаться, но алгоритм будет работать корректно.
* При работе с большими матрицами или при необходимости высокой точности может потребоваться настройка параметров `tol` и `miter`.  Уменьшение `tol` и увеличение `miter` повышают точность, но увеличивают время вычислений.
* В случае, если алгоритм не сходится за заданное количество итераций, возвращаются приближенные собственные значения, полученные на последней итерации.  Это может быть полезно для получения хотя бы какой-то информации о собственных значениях, даже если они не вычислены с полной точностью.
* Для более глубокого понимания алгоритма и его свойств рекомендуется обратиться к указанным в коде источникам, в частности, к книге "Matrix Computations" Golub G.H., Van Loan C.F.
* Если вам требуется вычислить только часть собственных значений (например, несколько наибольших или наименьших), а не все, возможно, более эффективными будут другие методы, такие как степенной метод или метод Ланцоша.  QR-алгоритм наиболее эффективен, когда требуется найти все собственные значения.
* При работе с симметричными или эрмитовыми матрицами можно использовать специализированные варианты QR-алгоритма, которые могут быть более эффективными.  Например, можно сначала привести матрицу к трехдиагональному виду с помощью преобразований Хаусхолдера, а затем применить QR-алгоритм.
* Для дальнейшего улучшения производительности можно рассмотреть возможность использования многопоточных вычислений, особенно при работе с очень большими матрицами.  Numba поддерживает распараллеливание вычислений, что может быть использовано для ускорения работы алгоритма.
* Важно помнить, что QR-алгоритм является итерационным методом, и его сходимость может зависеть от свойств конкретной матрицы.  В некоторых случаях может потребоваться экспериментировать с параметрами алгоритма или использовать другие методы для достижения желаемой точности и производительности.
* При работе с матрицами, которые близки к вырожденным, следует быть особенно осторожным, так как QR-разложение может быть неустойчивым.  В таких случаях может потребоваться использовать регуляризацию или другие методы для обеспечения корректной работы алгоритма.
* Для отладки и анализа работы алгоритма можно добавить в код вывод промежуточных результатов, например, значений матрицы `Ak` на каждой итерации.  Это может помочь понять, как происходит сходимость и выявить возможные проблемы.
* В реальных приложениях часто требуется не только вычислить собственные значения, но и собственные векторы матрицы.  Хотя данный код фокусируется только на вычислении собственных значений, QR-алгоритм можно расширить для вычисления и собственных векторов.  Это потребует отслеживания преобразований, выполняемых над матрицей в процессе итераций.
* При работе с комплексными матрицами потребуется использовать комплексную арифметику и, возможно, более сложные стратегии выбора сдвигов (например, двойной сдвиг).  Данная реализация предназначена для работы с вещественными матрицами.
* Для более надежной работы с матрицами различных типов и размеров можно добавить в код проверки входных данных, например, проверку на то, является ли входная матрица квадратной, и обработку возможных ошибок.
* Вместо использования глобальной константы `FLOAT_TOLERANCE` можно передавать ее как параметр в функцию `QR_dec` (предполагается, что это функция из модуля `matplobblib.matrices`), чтобы обеспечить большую гибкость и контроль над параметрами алгоритма.
* Для более удобного использования модуля можно создать класс, который инкапсулирует все функции, связанные с QR-алгоритмом.  Это позволит более четко организовать код и упростить его использование в других проектах.
* При работе с очень большими матрицами, которые не помещаются в оперативную память, можно рассмотреть возможность использования алгоритмов, работающих с матрицами на диске.  Однако это потребует значительной переработки кода и использования специализированных библиотек.
* Для визуализации процесса сходимости алгоритма можно построить графики изменения диагональных и поддиагональных элементов матрицы `Ak` в зависимости от номера итерации.  Это может помочь лучше понять, как работает алгоритм и как влияют на его сходимость различные параметры.
* Вместо использования функции `np.diag(Ak, k=-1)` для получения поддиагональных элементов можно использовать более эффективные способы доступа к элементам матрицы, например, с помощью срезов.  Однако для небольших матриц разница в производительности будет незначительной.
* Для более точного контроля за количеством итераций можно использовать цикл `while` с явным условием продолжения, а не цикл `for` с максимальным количеством итераций.  Это позволит завершить алгоритм сразу после достижения заданной точности, даже если это произойдет раньше, чем будет достигнуто максимальное количество итераций.
* Вместо вывода сообщения "QR-алгоритм не сошелся за ..." можно возбуждать исключение, чтобы более четко обработать ситуацию, когда алгоритм не сходится.  Это позволит вызывающей программе решить, как реагировать на эту ситуацию (например, попробовать другие параметры алгоритма или использовать другой метод).
* Для более удобного тестирования и отладки кода можно использовать библиотеку `unittest` для написания модульных тестов.  Это позволит проверить корректность работы алгоритма на различных входных данных и убедиться, что изменения в коде не приводят к ошибкам.
* Вместо использования функции `np.array([Ak[i, i] for i in range(n)])` для извлечения диагональных элементов можно использовать более простой и эффективный способ: `np.diag(Ak)`.
* Для повышения читаемости кода можно вынести проверку сходимости в отдельную функцию.
* Вместо использования функции `subtract_matrices_njit(multiply_matrices_njit(R, Q), scalar_multiply_matrix_njit(-mu, identity))` в функции `QR_step_shift_njit` можно использовать более простую и понятную формулу: `multiply_matrices_njit(R, Q) + scalar_multiply_matrix_njit(mu, identity)`.  Это эквивалентно, но более читаемо.
* Вместо создания единичной матрицы `identity` внутри функции `QR_step_shift_njit` можно передавать ее как параметр в функцию или создавать один раз вне функции и использовать повторно.  Это позволит избежать лишних операций выделения памяти и может немного повысить производительность.
* Для более четкого разделения функциональности можно вынести вычисление сдвига `mu` в отдельную функцию.  Это также упростит возможность использования других стратегий выбора сдвигов в будущем.
* Вместо использования функции `tqdm` напрямую можно использовать контекстный менеджер `with tqdm(...) as pbar:` для более корректной работы прогресс-бара, особенно в случае возникновения ошибок.
