9.3. Адаптивные методы#
Анализ большинства методов предполагает фиксированный шаг интегрирования \(\tau\), поэтому и поведение методов предсказывается локально. Однако, мы уже сталкивались с проблемами (5.4 Адаптивное интегрирование), в которых локальное поведение сильно отличается. Для решения ОДУ также существуют методы, подстраивающие шаг интегрирования в зависимости от локального поведения решения.
В данном разделе мы рассмотрим пример адаптивного алгоритма на основе методов Рунге-Кутта.
9.3.1. Выбор шага интегрирования#
Пусть мы решаем уравнение начиная с \(y_i\). Используем два разных метода, пусть у первого метода порядок аппроксимации \(p\), а у второго \(p+1\). Тогда при интегрировании на \(\tau\) мы получим два приближения \(y^{(p)}_{i+1}\) и \(y^{(p+1)}_{i+1}\). Мы ожидаем, что второе приближение намного точнее первого, поэтому возьмём их разницу за оценку погрешности первого метода
Теперь потребуем, чтобы ошибка при интегрировании была меньше заданной пользователем \(\epsilon\). При \(\tau \to 0\) погрешность имеет вид \(E_i(\tau) \approx C \tau^{p+1}\). Если бы мы использовали вместо \(\tau\) шаг \(q \tau\), \(q > 0\), тогда ошибка бы составила
Отсюда следует оценка для подбора \(q\), задающего новую сетку.
откуда
Заметим также, что \(q\) может быть как меньше \(1\) (требуется шаг мельче), так и больше 1 (можно увеличить шаг). Во втором случае \(q\) следует ограничивать сверху, иначе можно проскочить участки, где требуется мелкий шаг.
Теперь можно составить алгоритм решения.
Пусть известно приближение решения \(y_i\) в \(t = t_i\) и шаг интегрирования \(\tau\)
Подсчитать два приближения решения \(y^{(p)}_{i+1}\), \(y^{(p+1)}_{i+1}\) и оценить ошибку.
Если ошибка мала (меньше заданной пользователем), то принять в качестве решения в \(t=t_i + \tau\) \(y_{i+1} = y^{(p+1)}_{i+1}\).
Подстроить шаг \(\tau\), заменив на \(q\tau\) (9.10).
Повторять, пока \(t\) не достигнет \(T\).
9.3.2. Встроенные формулы#
Допустим, мы будем использовать методы Рунге-Кутты второго и третьего порядков. Вообще говоря, нам может понадобится до 5 вычислений функции \(f(t, u)\), поскольку точки, в которых происходит вычисление могут не совпадать. Пять вычислений уже вдвое больше, чем для двухэтапного метода.
Поскольку методы Рунге-Кутты являются параметрическими семействами, то выведены пары методов, использующие общие точки для вычисления \(f(t, u)\). Их называют встроенными методами Рунге-Кутта (embedded Runge-Kutta). За счёт этого свойства, встроенные методы экономят количество вычислений.
В качестве примера мы воспользуемся методом Богацкого-Шампина (Bogacki–Shampine), являющимся встроенным методом Рунге-Кутты и пользующимся приближениями второго и третьего порядков. Его таблица Бутчера имеет вид
Верхняя часть таблицы по-прежнему задаёт коэффициенты \(a_s\) и \(b_{sj}\), для обоих методов они одинаковые. Отличаются же методы весами \(\sigma_s\). Первая строка нижней части таблицы задаёт \(\sigma^{(2)}_s\) веса для метода второго порядка, а вторая строка (она же последняя) задаёт веса \(\sigma^{(3)}_s\) для метода третьего порядка.
Предоставим также явный вид метода
Заметьте, что если в качестве \(y_{i+1}\) брать второе приближение \(y^{(2)}_{i+1}\) на шаге 2 Алгоритма 9.12, то \(k_4\) равняется \(k_1\) на итерации \(i+1\). Данное свойство называют first same as last (FSAL). Это позволяет уменьшить количество вычислений \(f(t, u)\), и в итоге получаем одно вычисление при инициализации алгоритма и по три вычисления за шаг.
Наиболее популярным является метод Дорманда-Принса (Dormand-Prince), использующий методы Рунге-Кутты четвёртого и пятого порядка аппроксимации и ту же идею для оценки ошибки.
9.3.3. Реализация#
Метод Богацкого-Шампина
"""
rk23(problem; tol[, maxsteps, maxadjuststeps])
Решает задачу Коши `problem` адаптивным методом Богацкого-Шампина.
Погрешность вычислений задаётся `tol`, а максимальное количество шагов
интегрирования `maxsteps`. Число шагов, разрешённое для адаптации шага `maxadjuststeps`.
"""
function rk23(problem::CauchyODEProblem;
tol::Real,
maxsteps::Integer=10000,
maxadjuststeps::Integer=20,
)
t₀, T = problem.bound
trange = [t₀]
u = [problem.u₀]
k₁ = problem.f(t₀, problem.u₀)
τ = 0.5 * tol^(1/3)
for i in 1:maxsteps
tᵢ, uᵢ = trange[i], u[i]
tᵢ == T && break
if tᵢ + τ == tᵢ
@warn "Достигнут предел машинной точности по τ"
break
end
for j in 1:maxadjuststeps
k₂ = problem.f(tᵢ + τ/2, uᵢ + τ*k₁/2)
k₃ = problem.f(tᵢ + 3τ/4, uᵢ + 3τ*k₂/4)
unew2 = uᵢ + τ*(2k₁ + 3k₂ + 4k₃)/9 # РК2 приближение
k₄ = problem.f(tᵢ + τ, unew2)
Δ = τ * (-5k₁/72 + k₂/12 + k₃/9 - k₄/8) # разница РК2 и РК3 приближений
err = norm(Δ, Inf)
maxerr = tol * (1 + norm(uᵢ, Inf))
accepted = err < maxerr
if accepted
push!(trange, tᵢ + τ)
push!(u, unew2)
k₁ = k₄ # FSAL: k₄ = f(tᵢ + τ, uᵢ₊₁) == new k₁
end
# подбор нового шага
q = 0.8 * (maxerr/err)^(1/3) # оценка шага из погрешности
q = min(q, 4.0) # ограничиваем максимальное увеличение
τ = min(q*τ, T - trange[end]) # не выходим за предел T
accepted && break
j == maxadjuststeps && error("Число шагов по подбору τ превышено.")
end
i == maxsteps && @warn "Число шагов превышено, конечное время не достигнуто"
end
return trange, u
end
Ниже представлен пример решения задачи Коши
problem = CauchyODEProblem(
f=(t, u) -> exp(t - u*sin(u)),
tstart=0,
tend=5,
u₀=0,
)
plt = plot(;
title=L"u' = e^{t - u \sin{u}},\quad u(0) = 0,\quad t\in [0, 5]",
leg=:topleft,
layout=(2,1),
)
t, u = rk23(problem; tol=1e-5, maxsteps=1000)
plot!(t, u; label="РК23", marker=:o, subplot=1, ylabel=L"u", xlim=(-0.1, 5.1))
plot!(t[1:end-1], diff(t);
subplot=2,
title="",
yaxis=(:log10, L"\tau"),
label="",
xlabel=L"t",
xlim=(-0.1, 5.1),
)
plt