Неявные методы и BDF для жёстких задач

Мы знаем диагноз — жёсткость — и знаем лекарство — A-устойчивость. Осталось понять, какие методы её обеспечивают на практике, чем за неё платят и как выбрать солвер в реальном коде.

BDF (Backward Differentiation Formulas, формулы обратного дифференцирования) — семейство многошаговых неявных методов, аппроксимирующих производную по нескольким предыдущим точкам и текущей (ещё неизвестной); они жёстко-устойчивы и лежат в основе солверов scipy 'BDF' и (через близкие идеи) 'Radau'.

Зачем нужны неявные методы

Из предыдущего урока ясно: на жёсткой задаче явный метод вынужден держать шаг по самой быстрой моде, и это убивает производительность. Неявные методы вроде неявного Эйлера A-устойчивы — их шаг ограничен только требуемой точностью, а не устойчивостью. Можно идти крупным шагом по гладкой медленной динамике, не боясь взрыва от давно затухшей быстрой моды. Это превращает «5000 шагов» из первого урока в «несколько десятков». Ради этого выигрыша инженеры и терпят главное неудобство неявных методов — необходимость решать уравнение на каждом шаге.

В чём «неявность» и её цена

Сравните две формулы Эйлера:

Явный:    y_{n+1} = y_n + h * f(t_n,     y_n)        # правая часть готова
Неявный:  y_{n+1} = y_n + h * f(t_{n+1}, y_{n+1})    # y_{n+1} стоит в обеих частях

В явной формуле неизвестное y_{n+1} выражено напрямую — подставил и посчитал. В неявной оно входит и в левую, и в правую часть под знаком функции f. Это уравнение относительно y_{n+1}, которое в общем случае нелинейно и аналитически не решается. На каждом шаге приходится решать его численно — обычно методом Ньютона, а для систем — методом Ньютона с матрицей Якоби, то есть решать ещё и СЛАУ внутри каждой итерации. Вот цена A-устойчивости: один шаг неявного метода дороже шага явного в разы. Но шагов нужно несравнимо меньше, и на жёсткой задаче неявный метод выигрывает с огромным запасом.

Как работает под капотом

Для линейного теста y' = λ*y неявность снимается алгеброй, и метод Ньютона не нужен. Подставим f = λ*y в неявный Эйлер: y_{n+1} = y_n + h*λ*y_{n+1}. Соберём y_{n+1} в одну сторону: y_{n+1}*(1 - h*λ) = y_n, откуда y_{n+1} = y_n / (1 - h*λ). Множитель за шаг — ровно тот самый R(z) = 1/(1 - z) из прошлого урока. Прогоним неявный Эйлер на жёстком y' = -50*y крупным шагом h = 0.05, при котором явный Эйлер взрывался, и сравним их бок о бок.

import math

def explicit_euler(h, steps, lam=-50.0, y0=1.0):
    y = y0
    out = [y]
    for _ in range(steps):
        y = y + h * (lam * y)
        out.append(y)
    return out

def implicit_euler(h, steps, lam=-50.0, y0=1.0):
    y = y0
    out = [y]
    factor = 1.0 / (1.0 - h * lam)   # для линейного теста неявность решается явно
    for _ in range(steps):
        y = y * factor
        out.append(y)
    return out

h = 0.05
exp_vals = explicit_euler(h, 5)
imp_vals = implicit_euler(h, 5)
print(f"шаг h={h}, множитель неявного = 1/(1-50h) = {1/(1 - h*-50):.4f}")
print("  n |   явный    |  неявный  |  точное exp(-50*t)")
for n in range(6):
    t = n * h
    exact = math.exp(-50 * t)
    print(f"  {n} | {exp_vals[n]:+9.4f} | {imp_vals[n]:8.4f} | {exact:.4f}")

Вывод:

шаг h=0.05, множитель неявного = 1/(1-50h) = 0.2857
  n |   явный    |  неявный  |  точное exp(-50*t)
  0 |   +1.0000 |   1.0000 | 1.0000
  1 |   -1.5000 |   0.2857 | 0.0821
  2 |   +2.2500 |   0.0816 | 0.0067
  3 |   -3.3750 |   0.0233 | 0.0006
  4 |   +5.0625 |   0.0067 | 0.0000
  5 |   -7.5938 |   0.0019 | 0.0000

Картина исчерпывающая. Явный Эйлер при h = 0.05 расходится — значения растут и скачут по знаку. Неявный Эйлер при том же шаге устойчиво затухает: множитель 0.2857 меньше единицы, и решение монотонно идёт к нулю, повторяя качественное поведение точного exp(-50*t). Точность при крупном шаге, конечно, грубая (неявный «садится» к нулю медленнее точного), но это вопрос точности, а не выживания — и решается он уменьшением шага или методом более высокого порядка. Главное: неявный метод не взрывается там, где явный безнадёжен.

Что такое BDF

Неявный Эйлер — это BDF первого порядка (BDF1). Идея BDF: аппроксимировать производную y' в точке t_{n+1} не вперёд, а назад — через несколько уже известных предыдущих значений плюс искомое y_{n+1}. Чем больше точек используется, тем выше порядок. Формулы для чтения:

BDF1 (неявный Эйлер):  (y_{n+1} - y_n) / h = f(t_{n+1}, y_{n+1})
BDF2:  (3*y_{n+1} - 4*y_n + y_{n-1}) / (2*h) = f(t_{n+1}, y_{n+1})
BDF3:  (11*y_{n+1} - 18*y_n + 9*y_{n-1} - 2*y_{n-2}) / (6*h) = f(t_{n+1}, y_{n+1})

Правая часть берётся в НОВОЙ точке t_{n+1} => метод неявный.

BDF1 и BDF2 A-устойчивы; BDF порядков 3–6 уже не A-устойчивы (барьер Далквиста не позволяет), но обладают более слабым, зато достаточным на практике свойством — жёсткой устойчивостью: они устойчивы в той части левой полуплоскости, которая важна для жёстких задач (большие отрицательные вещественные части), и лишь в узком секторе у мнимой оси теряют устойчивость. Этого достаточно для подавляющего большинства жёстких систем, поэтому BDF до 5-го порядка — рабочая лошадка промышленных солверов (легендарный код DASSL, scipy.integrate с method='BDF').

Как выбрать метод на практике

В реальном Python вы почти никогда не пишете солвер сами — берёте scipy.integrate.solve_ivp и выбираете метод параметром. Правило выбора простое:

from scipy.integrate import solve_ivp

# НЕЖЁСТКАЯ задача (плавная динамика, один масштаб времени):
sol = solve_ivp(f, [0, 10], y0, method='RK45')   # Dormand-Prince, по умолчанию

# ЖЁСТКАЯ задача (разные масштабы, явные методы взрываются):
sol = solve_ivp(f, [0, 10], y0, method='BDF')     # многошаговый неявный
sol = solve_ivp(f, [0, 10], y0, method='Radau')   # неявный Рунге-Кутты, очень устойчив
sol = solve_ivp(f, [0, 10], y0, method='LSODA')   # САМ переключается жёсткий/нежёсткий

# подсказать якобиан ускоряет неявные методы:
sol = solve_ivp(f, [0, 10], y0, method='BDF', jac=jacobian)

Логика такая. Если задача нежёсткая — RK45 (метод Дормана-Принса, адаптивный явный) даёт лучшее соотношение точности и скорости. Если жёсткая — нужен неявный: BDF (быстрый, многошаговый), Radau (очень устойчивый неявный Рунге-Кутты, хорош для высокой точности и сложных систем) или LSODA, который автоматически детектирует жёсткость по ходу интегрирования и сам переключается между Adams (нежёсткий режим) и BDF (жёсткий). Если вы не уверены, жёсткая задача или нет — начните с RK45; если он зависает, делает миллионы шагов или выдаёт предупреждение о малом шаге, переключайтесь на BDF или LSODA. Передача якобиана (jac=) ускоряет неявные методы, потому что избавляет от его численной оценки на каждой ньютоновской итерации.

Частые ошибки

  • Применять RK45 к жёсткой задаче и удивляться зависанию. Явный адаптивный метод не взорвётся (он уменьшит шаг), но сделает это ценой огромного числа шагов и будет считать вечность. Симптом — солвер «думает» минутами на простой с виду системе.
  • Забыть, что неявность стоит денег. BDF и Radau решают нелинейное уравнение на каждом шаге методом Ньютона. На нежёсткой задаче это лишние затраты — там RK45 быстрее. Неявный метод оправдан только жёсткостью.
  • Не передавать якобиан для больших систем. Без jac= scipy оценивает матрицу Якоби численно, тратя много вызовов f. Для систем из сотен уравнений аналитический якобиан ускоряет счёт в разы.
  • Считать BDF высоких порядков A-устойчивыми. A-устойчивы только BDF1 и BDF2. Порядки 3–6 лишь жёстко-устойчивы — этого хватает для типичных жёстких задач, но не для систем с собственными числами у самой мнимой оси (чисто колебательных).
  • Брать слишком высокий порядок «для точности». BDF выше 6-го порядка неустойчивы и в солверах не используются. Точность повышают уменьшением шага и контролем ошибки, а не безграничным ростом порядка.

Итоги

  • Неявные методы A-устойчивы — крупный шаг на жёсткой задаче без взрыва; цена — решение уравнения (метод Ньютона + СЛАУ) на каждом шаге.
  • Неявный Эйлер на жёстком y' = -50*y при h = 0.05 устойчив (множитель 0.2857), тогда как явный расходится.
  • BDF — многошаговые неявные формулы обратного дифференцирования; BDF1=неявный Эйлер, BDF1/2 A-устойчивы, BDF3–6 жёстко-устойчивы.
  • Выбор в scipy: нежёсткое → RK45 (Dormand-Prince); жёсткое → BDF / Radau / LSODA (авто-детект жёсткости).
  • Передача якобиана (jac=) ускоряет неявные методы; высокий порядок BDF не панацея — выше 6 нестабилен.
Проверьте себя
1. Почему для жёстких задач выбирают неявные методы, несмотря на их дороговизну?
AОни дают нулевую ошибку округления
BОни A-устойчивы (или жёстко-устойчивы) — можно брать крупный шаг без взрыва, шаг ограничен только точностью
CОни не требуют знать правую часть f
DОни работают быстрее на каждом отдельном шаге
2. Какова главная вычислительная цена неявных методов?
AНужно хранить все предыдущие значения решения
BНа каждом шаге надо решать (нелинейное) уравнение относительно y_{n+1}, обычно методом Ньютона
CОни требуют комплексной арифметики
DОни расходуют вдвое больше памяти
3. Что вернёт неявный Эйлер на y' = -50·y при крупном шаге h = 0.05, где явный Эйлер взрывается?
AТоже взорвётся, ведь шаг крупный
BУстойчиво затухнет: множитель 1/(1-50·0.05) ≈ 0.286 < 1
CОстанется постоянным
DВыдаст ошибку деления на ноль
4. Какой метод scipy.solve_ivp разумно выбрать первым для ЯВНО нежёсткой задачи с плавной динамикой?
ABDF
BRadau
CRK45 (Dormand-Prince) — адаптивный явный, лучшее соотношение скорости и точности
DLSODA только в режиме BDF