Фізика та симуляція
Квітень 2026 · 18 хв читання · Числові методи · Обчислювальна фізика · JavaScript

Лінійна алгебра для симуляцій: розріджені розв'язувачі, розклади та власні значення

Кожна фізична симуляція зрештою зводиться до лінійної системи Ax = b. Чи то рівняння Пуассона для тиску в гідродинаміці, глобальна матриця жорсткості в скінченноелементному аналізі чи лапласіан у теплопровідності, вибір лінійного розв'язувача визначає, чи симуляція виконається за мілісекунди, чи за години. Ця стаття оглядає алгоритми — від прямого LU-розкладу до ітераційних методів Крилова — що живлять сучасну обчислювальну фізику.

1. Чому лінійні системи домінують у симуляції

Фізичні закони — збереження маси, імпульсу та енергії — стають лінійними системами при дискретизації в просторі та часі. Три типові приклади:

МСЕ: глобальна матриця жорсткості

Метод скінченних елементів для механіки конструкцій дає:

K · u = f K ∈ ℝ^(N×N) : глобальна матриця жорсткості (розріджена, симетрична додатно визначена) u ∈ ℝ^N : вектор переміщень (невідомий) f ∈ ℝ^N : вектор зовнішніх сил

N — кількість ступенів свободи (3 на вузол у 3D). Для сітки зі 100 000 вузлів N ≈ 300 000. K має ширину стрічки, визначену зв'язністю сітки — приблизно 20–30 ненульових на рядок — що дає близько 6 мільйонів ненульових елементів із загальних 90 мільярдів: розрідженість 99.993%.

Рідина: рівняння Пуассона для тиску

У симуляціях нестисливих Нав'є–Стокса (напр. рідина FLIP, SPH), забезпечення ∇·u = 0 вимагає розв'язування:

∇²p = (ρ/Δt) · ∇·u* (рівняння Пуассона для тиску) // Дискретизоване на сітці з 5-точковим шаблоном (2D) або 7-точковим (3D) A · p = b де A — матриця дискретного лапласіана

Дискретний лапласіан на сітці 256³ має N ≈ 16.7 мільйона невідомих. Пряма факторизація вимагала б O(N³) ≈ 4.6 × 10²¹ операцій — абсолютно нездійсненно. Саме тому існують ітераційні розв'язувачі.

Спільні властивості

2. Формати розріджених матриць: CSR, CSC, COO

Формати розріджених матриць зберігають лише ненульові значення та їхні позиції, обмінюючи пам'ять на накладні витрати масивів індексів.

COO — координатний формат

Три масиви: індекси рядків, індекси стовпців, значення. Невпорядкований, допускає дублікати елементів (row, col) (підсумовуються при перетворенні). Найкращий для складання матриці.

Матриця: [ 5 0 0 ] Формат COO: [ 0 8 2 ] row = [0, 1, 1, 2, 2] [ 0 3 7 ] col = [0, 1, 2, 1, 2] val = [5, 8, 2, 3, 7]

CSR — стиснений за рядками

Найпоширеніший формат для арифметики. Замінює масив рядків на рядковий вказівник (довжиною N+1, де rowptr[i+1] − rowptr[i] — кількість ненульових у рядку i):

rowptr = [0, 1, 3, 5] // рядок 0 має 1 нн, рядок 1 — 2, рядок 2 — 2 col = [0, 1, 2, 1, 2] val = [5, 8, 2, 3, 7] // Множення матриці на вектор (CSR): for i in 0..N: y[i] = 0 for j in rowptr[i]..rowptr[i+1]: y[i] += val[j] * x[col[j]]

CSC — стиснений за стовпцями

Ролі рядків та стовпців помінялися. Переважний за угодою LAPACK та для стовпцево-орієнтованих операцій (напр. вибору головного елемента при факторизації). Власний формат MATLAB — CSC.

Порівняння форматів

Формат Пам'ять Матвек Складання Найкращий для
COO 3×nnz Повільний (промах кешу) Чудове Побудови із сітки
CSR 2×nnz + N+1 Швидкий (рядкова локальність кешу) Погане Матвек для CG, GMRES
CSC 2×nnz + N+1 Швидкий (доступ за стовпцями) Погане LU-факторизації
BSR 2×nnz/bs + N/bs+1 Найшвидший (SIMD-блоки) Помірне Блочних ступенів свободи МСЕ

3. Прямі розв'язувачі: LU, Холецький, QR

Прямі методи факторизують A один раз, а потім розв'язують через трикутну зворотну підстановку за O(N) операцій — але сама факторизація може бути дорогою, а заповнення (поява ненульових там, де A була нулем) обмежує масштабованість до N ≲ 10⁵ у 3D.

LU-розклад

Для загальної матриці n×n: A = P · L · U, де P — матриця перестановки (частковий вибір головного елемента), L — нижня трикутна з одиничною діагоналлю, U — верхня трикутна.

// Розв'язати Ax = b: PA = LU → LUx = Pb 1. Пряма підстановка: Ly = Pb O(n²) 2. Зворотна підстановка: Ux = y O(n²) Вартість факторизації: O(n³/3) флопів (щільна) Розріджене заповнення: залежить від порядку виключення (AMD, вкладене розсікання)

Розріджений LU через UMFPACK або SuperLU використовує порядки, що зменшують заповнення. Порядок вкладеного розсікання дає O(N^1.5) заповнення для 2D- сіток та O(N^2) для 3D — набагато краще за природний порядок, але все ще гірше за ітераційні розв'язувачі для дуже великих 3D-задач.

Розклад Холецького (SPD)

Для симетричних додатно визначених матриць: A = LLᵀ, де L — нижня трикутна з додатною діагоналлю. Вибір головного елемента не потрібен (стійкість гарантована для SPD). Приблизно вдвічі швидше за LU — зберігається й обробляється лише нижній трикутник.

L[i][i] = sqrt(A[i][i] - sum(L[i][k]^2 for k < i)) L[j][i] = (A[j][i] - sum(L[j][k]*L[i][k] for k < i)) / L[i][i] (j > i)

Застосування: матриці жорсткості МСЕ, коваріаційні матриці в імовірнісних методах, множники SPD-передобумовлювачів (неповний Холецький IC(0)).

QR-розклад

Факторизує A = QR, де Q — ортогональна (Qᵀ = Q⁻¹), а R — верхня трикутна. Випадки застосування:

Обчислюється через відбивачі Хаусхолдера або повороти Гівенса. QR Хаусхолдера числово кращий за класичний Грам-Шмідт — похибка ортогональності становить O(ε·κ(A)) проти O(ε·κ(A)²) для CGS.

4. Ітераційні розв'язувачі: CG, GMRES, BiCGSTAB

Ітераційні методи починають з початкового наближення x₀ й покращують його по одному «кроку» за раз. Кожен крок коштує O(nnz) для розрідженого множення матриці на вектор. Вони ніколи повністю не факторизують A — що робить їх застосовними до задач, де прямі методи нездійсненні.

Спряжені градієнти (CG) — лише SPD

Метод CG (Hestenes & Stiefel, 1952) мінімізує квадратичну форму f(x) = ½ xᵀAx − bᵀx за послідовністю A-спряжених напрямків пошуку. Для SPD-матриці він збігається щонайбільше за N кроків у точній арифметиці (N = розмір матриці), але на практиці збігається до машинної точності значно раніше.

Алгоритм CG: r₀ = b − A·x₀; p₀ = r₀; k = 0 while ‖rₖ‖ > tol: αₖ = (rₖᵀrₖ) / (pₖᵀApₖ) // довжина кроку x_{k+1} = xₖ + αₖ·pₖ r_{k+1} = rₖ − αₖ·A·pₖ βₖ = (r_{k+1}ᵀr_{k+1}) / (rₖᵀrₖ) // оновлення напрямку p_{k+1} = r_{k+1} + βₖ·pₖ k = k + 1 Вартість ітерації: 1 SpMV (A·pₖ) + 4 скалярні добутки + 3 операції axpy Збіжність: ‖eₖ‖_A ≤ 2·((√κ−1)/(√κ+1))^k · ‖e₀‖_A де κ = λmax/λmin

GMRES — загальні матриці

Метод узагальненого мінімального залишку (Saad & Schultz, 1986) обробляє несиметричні та невизначені системи. Він будує ортонормований базис Крилова {v₁, v₂, ..., vₖ} через ітерацію Арнольді й мінімізує ‖b − Axₖ‖₂ над підпростором Крилова Kₖ = span{v₁, ..., vₖ}.

Підпростір Крилова: K_k(A, r₀) = span{r₀, Ar₀, A²r₀, ..., A^(k-1)r₀} Мінімізувати: ‖b − A·xₖ‖₂ над x ∈ x₀ + K_k вартість: k SpMV + O(k²) Грам-Шмідта на цикл пам'ять: O(k·N) — обмежує параметр перезапуску k (зазвичай k = 30–200)

GMRES(k) з перезапуском перезапускає базис Крилова кожні k кроків, щоб контролювати пам'ять. Компроміс: менше k використовує менше пам'яті, але може стагнувати на складних системах. Гнучкий GMRES (FGMRES) дозволяє передобумовлювачу змінюватися між ітераціями.

BiCGSTAB

Стабілізований біспряжений градієнт (van der Vorst, 1992) — це варіант для несиметричних систем, що уникає зростання пам'яті GMRES. Він використовує дві послідовності залишків для досягнення плавнішої збіжності, ніж BiCG (який може поводитися хаотично). Пам'ять: O(N) — лише фіксована кількість векторів незалежно від кількості ітерацій. Переважний над GMRES, коли пам'яті обмаль.

5. Передобумовлювачі: Якобі, ILU, багатосіткові

Передобумовлювач M апроксимує A й перетворює систему на M⁻¹Ax = M⁻¹b. Збіжність CG/GMRES залежить від числа обумовленості κ(M⁻¹A) — хороший передобумовлювач зменшує κ за невелику ціну.

Передобумовлювач Якобі (діагональний)

M = diag(A) → M⁻¹x = x / diag(A) Вартість: O(N) — просто поділити кожну компоненту на діагональний елемент Ефект: нормалізує рядки; усуває легко виправну діагональну переважність Добре працює для: діагонально переважних систем, першого проходу на погано масштабованій A

Неповний LU: ILU(0) та ILU(k)

Неповний LU факторизує A ≈ L̃Ũ, де L̃ та Ũ мають той самий шаблон розрідженості, що й A (ILU(0)), або допускають k рівнів додаткового заповнення (ILU(k)). Наближені множники дешеві в застосуванні (розріджені трикутні розв'язування) та суттєво зменшують κ.

ILU(0): обчислити LU, але відкинути будь-яке заповнення в позиціях, де A_{ij} = 0 → L̃ та Ũ мають той самий шаблон розрідженості, що й нижній/верхній трикутники A Застосувати M⁻¹r: пряме розв'язання з L̃, потім зворотне з Ũ

ILU(k) допускає заповнення до відстані k від наявних ненульових. ILU(1) чи ILU(2) часто зменшує кількість ітерацій у 5–10× порівняно з Якобі за ціну в 3–5× більше пам'яті на множник.

Багатосіткові методи: оптимальна збіжність O(N)

Багатосіткові передобумовлювачі (чи самостійні розв'язувачі) досягають загальної роботи O(N), розв'язуючи задачу на ієрархії грубіших сіток і використовуючи розв'язки на грубій сітці для корекції залишків на дрібній сітці.

Геометричні багатосіткові методи вимагають структурованої ієрархії сіток. Алгебраїчні багатосіткові методи (AMG) будують ієрархію автоматично з матриці — BoomerAMG з HYPRE та ML/MueLu (Trilinos) є стандартними бібліотеками в науковому обчисленні. Для рівнянь Пуассона на однорідних сітках багатосіткові методи збігаються за O(1) ітерацій незалежно від розміру сітки.

Передобумовлювач Вартість налаштування Вартість застосування Типове зменшення κ Найкращий для
Немає O(1) O(1) Добре обумовленої A
Якобі O(N) O(N) 2–5× Діагонально переважних
SSOR O(nnz) O(nnz) 5–20× Загальних SPD
IC(0) O(nnz) O(nnz) 10–50× SPD розріджених
ILU(0) O(nnz) O(nnz) 10–50× Несиметричних
AMG O(N log N) O(N) O(N)→O(1)× Еліптичних РЧП

6. Алгоритми власних значень: степеневий, QR, Ланцош

Власні значення з'являються всюди, де симуляція має власні частоти, аналіз стійкості чи модальний розклад. Знаходження всіх власних значень матриці N×N коштує O(N³) — здійсненно лише для N ≲ 10³. Великі розріджені задачі на власні значення вимагають методів, що дешево обчислюють кілька власних значень.

Степеневі ітерації

Знаходять власне значення з найбільшим модулем λ_max та його власний вектор:

// Степеневі ітерації: v₀ = випадковий одиничний вектор repeat: w = A · vₖ vₖ₊₁ = w / ‖w‖ λ_k = vₖᵀ · A · vₖ (відношення Релея) until збіжності Швидкість: ‖похибка‖ зменшується в |λ₂/λ₁| разів за крок

Обернені ітерації (замінити A на (A − σI)⁻¹) знаходять власне значення, найближче до σ, використовуючи одне лінійне розв'язання за крок — ідеально, коли прямий розв'язувач дешевий, а цільове власне значення відоме приблизно. Зсув-обернення з CG/GMRES замінює щільне розв'язання розрідженою ітерацією.

Щільний QR-алгоритм

Для щільних матриць (N ≲ 10³) усі власні значення можна знайти через QR- ітерацію:

A₀ = A Aₖ₊₁ = Rₖ · Qₖ де Aₖ = Qₖ · Rₖ (QR-розклад) Aₖ збігається до (квазі-)верхньої трикутної — власні значення на діагоналі На практиці: спершу звести до форми Гессенберга (O(N³)/3 флопів, потім O(N²) на крок)

Алгоритм Ланцоша — великі розріджені

Алгоритм Ланцоша (1950) будує ортонормований базис Крилова й зводить A до симетричної тридіагональної матриці T розміру k×k (k ≪ N). Власні значення T (значення Рітца) апроксимують крайні власні значення A. Потрібно лише k SpMV — ідеально для знаходження 10–100 найбільших чи найменших власних значень розрідженої A.

Ітерація Ланцоша (симетрична A): v₁ = випадковий одиничний вектор for j = 1, 2, ..., k: w = A · vⱼ αⱼ = vⱼᵀ · w w = w − αⱼ·vⱼ − βⱼ₋₁·vⱼ₋₁ βⱼ = ‖w‖; vⱼ₊₁ = w / βⱼ T = tridiag(α₁,...,αₖ; β₁,...,βₖ₋₁) eigvals(T) → наближені крайні власні значення A

На практиці накопичується втрата ортогональності — що вимагає або повної реортогоналізації (підхід IRLM з ARPACK), або вибіркової ортогоналізації (частковий Ланцош). ARPACK (Arnoldi Package) — стандартна бібліотека: її прив'язка для Python — це `scipy.sparse.linalg.eigsh` та `eigs` зі SciPy.

7. SVD, PCA та модальний розклад

Сингулярний розклад матриці A розміру m×n:

A = U · Σ · Vᵀ U ∈ ℝ^(m×m): ліві сингулярні вектори (ортогональні) Σ ∈ ℝ^(m×n): діагональна, елементи σ₁ ≥ σ₂ ≥ ... ≥ σₚ ≥ 0 V ∈ ℝ^(n×n): праві сингулярні вектори (ортогональні) p = min(m, n)

Теорема Еккарта-Янга стверджує, що найкраще наближення A рангу k (у будь-якій ортогонально інваріантній нормі) — це:

Aₖ = Σᵢ₌₁ᵏ σᵢ · uᵢ · vᵢᵀ ‖A − Aₖ‖₂ = σₖ₊₁ (похибка спектральної норми = наступне сингулярне значення)

Власний ортогональний розклад (POD)

У гідродинаміці та динаміці конструкцій POD (SVD матриці знімків) виокремлює домінантні просторові моди симуляції. Якщо U = [u(t₁) | u(t₂) | ... | u(tₙ)] містить N знімків швидкості, то:

Динамічний модальний розклад (DMD)

DMD (Schmid, 2010) знаходить найкраще наближений лінійний оператор, що відображає знімок Uₖ на Uₖ₊₁:

Ũ₂ ≈ A · Ũ₁ → A = Ũ₂ · Ũ₁⁺ (псевдообернена Мура-Пенроуза через SVD) // На практиці: спроєктувати A на r мод POD (дешево), обчислити власні значення // спроєктованого оператора r×r, потім відновити наближені власні функції A

DMD виокремлює просторово-часові когерентні структури з даних симуляції навіть без доступу до визначальних рівнянь — що робить його інструментом редукованого моделювання на основі даних, застосовним до турбулентності, фінансових часових рядів та кліматичних даних.

8. Розріджений метод спряжених градієнтів на JavaScript

Нижче наведено повну, самодостатню реалізацію розрідженої CSR- матриці та розв'язувача передобумовлених спряжених градієнтів на JavaScript. Це саме той розв'язувач Пуассона для тиску, що використовується в сіткових симуляціях рідин:

// ============================================================
// Розріджена CSR-матриця + передобумовлені спряжені градієнти (PCG)
// Розв'язує A·x = b, де A — SPD і розріджена (формат CSR)
// ============================================================

class SparseCSR {
  constructor(n) {
    this.n = n;
    this.rowptr = new Int32Array(n + 1);
    this.cols   = [];    // буде перетворено на Int32Array
    this.vals   = [];    // Float64Array після побудови
  }

  // Складання з COO-трійок [{row, col, val}]
  static fromCOO(n, coo) {
    // Сортуємо за рядком, потім за стовпцем
    coo.sort((a, b) => a.row !== b.row ? a.row - b.row : a.col - b.col);
    const mat   = new SparseCSR(n);
    const cols  = [], vals = [];
    const rowptr = new Int32Array(n + 1);

    for (const {row, col, val} of coo) {
      rowptr[row + 1]++;
      cols.push(col);
      vals.push(val);
    }
    for (let i = 1; i <= n; i++) rowptr[i] += rowptr[i - 1];
    mat.rowptr = rowptr;
    mat.cols   = new Int32Array(cols);
    mat.vals   = new Float64Array(vals);
    return mat;
  }

  // y = A·x (розріджений добуток матриці на вектор)
  matvec(x, y) {
    const {n, rowptr, cols, vals} = this;
    for (let i = 0; i < n; i++) {
      let s = 0;
      for (let j = rowptr[i]; j < rowptr[i + 1]; j++) s += vals[j] * x[cols[j]];
      y[i] = s;
    }
  }

  // Діагональний передобумовлювач (Якобі)
  diagInv() {
    const d = new Float64Array(this.n);
    const {n, rowptr, cols, vals} = this;
    for (let i = 0; i < n; i++) {
      for (let j = rowptr[i]; j < rowptr[i + 1]; j++) {
        if (cols[j] === i) { d[i] = 1 / vals[j]; break; }
      }
    }
    return d;
  }
}

// Розв'язує A·x = b за допомогою передобумовленого CG
// Повертає {x, iter, residual}
function pcg(A, b, opts = {}) {
  const {tol = 1e-6, maxIter = 1000} = opts;
  const n = A.n;
  const diag  = A.diagInv();          // передобумовлювач Якобі M⁻¹
  const x     = new Float64Array(n);
  const r     = new Float64Array(n);
  const z     = new Float64Array(n);
  const p     = new Float64Array(n);
  const Ap    = new Float64Array(n);

  // r₀ = b − A·x₀  (x₀ = 0 → r₀ = b)
  for (let i = 0; i < n; i++) r[i] = b[i];

  // z₀ = M⁻¹·r₀  (Якобі: покомпонентне множення)
  let rz = 0;
  for (let i = 0; i < n; i++) { z[i] = r[i] * diag[i]; p[i] = z[i]; rz += r[i] * z[i]; }

  let iter = 0;
  while (iter < maxIter) {
    A.matvec(p, Ap);
    let pAp = 0;
    for (let i = 0; i < n; i++) pAp += p[i] * Ap[i];

    const alpha = rz / pAp;
    let rnorm2 = 0;
    for (let i = 0; i < n; i++) {
      x[i] += alpha * p[i];
      r[i] -= alpha * Ap[i];
      rnorm2 += r[i] * r[i];
    }

    if (Math.sqrt(rnorm2) < tol) break;

    let rzNew = 0;
    for (let i = 0; i < n; i++) { z[i] = r[i] * diag[i]; rzNew += r[i] * z[i]; }
    const beta = rzNew / rz;
    rz = rzNew;
    for (let i = 0; i < n; i++) p[i] = z[i] + beta * p[i];
    iter++;
  }
  return {x, iter, residual: Math.sqrt(rz)};
}

// ---- Приклад: 1D-Пуассон на однорідній сітці, 5 внутрішніх точок ----
// −u'' = f,  u(0)=u(1)=0,  f=1   → u_i = xi(1−xi)/2
const N  = 5;
const h  = 1 / (N + 1);
const h2 = h * h;

// Будуємо тридіагональну матрицю лапласіана у форматі COO
const coo = [];
for (let i = 0; i < N; i++) {
  coo.push({row: i, col: i, val:  2 / h2});
  if (i > 0)     coo.push({row: i, col: i-1, val: -1 / h2});
  if (i < N - 1) coo.push({row: i, col: i+1, val: -1 / h2});
}
const A = SparseCSR.fromCOO(N, coo);
const b = new Float64Array(N).fill(1);         // f=1

const {x, iter} = pcg(A, b, {tol: 1e-10});

console.log("PCG збігся за", iter, "ітерацій");
x.forEach((u, i) => {
  const xi  = (i + 1) * h;
  const ref = xi * (1 - xi) / 2;
  console.log(`u[${i}] = ${u.toFixed(8)}  (ref: ${ref.toFixed(8)})`);
});
    
Посібник з вибору розв'язувача: Для N < 5 000 та щільних матриць → Холецький або LU. Для N < 100 000 та розріджених SPD-матриць → розріджений Холецький (CHOLMOD). Для N > 100 000 та SPD еліптичних РЧП → PCG + передобумовлювач AMG. Для несиметричних N > 10 000 → GMRES(30) або BiCGSTAB + ILU(1). Для власних значень (кількох, розріджена) → Ланцош / ARPACK. На практиці: викликайте бібліотеку (LAPACK, ARPACK, HYPRE), а не реалізуйте з нуля — але розуміння алгоритму необхідне для вибору правильного.