5.2. Метод Ромберга#
Метод Ромберга (Romberg’s method) позволяет быстро интегрировать с заданной точностью. Метод основывается на применении экстраполяции Ричардсона к формуле трапеций при удвоении числа узлов.
5.2.1. Экстраполяция Ричардсона#
Частный случай этого метода мы рассматривали при выводе Формулы Симпсона.
[89] Рассмотрим вычисление интеграла \(I_h\) на равномерной сетке с шагом \(h\). На каждом интервале отрезка применяется одна и та же квадратурная формула. Для некоторых формул (например, для формулы трапеций) удаётся получить вид погрешности \(I - I_h\). Предположим, что это разложение выглядит так
Здесь \(0 < \alpha_1 < \alpha_2 < ... < \alpha_m\) – степени разложения, а \(a_i\) его ненулевые коэффициенты.
Теперь рассмотрим (5.10) на последовательности сеток с шагами \(h_0 = h\), \(h_1 = qh\), \(h_2 = q^2 h\), …, \(h_k = q^k h\), \(k=0,...,m\), \(0 < q < 1\). Тогда, согласно (5.10), запишем
Введём обозначение \(I^{(1)}_{h_k} = I_{h_k}\) и запишем для двух сеток
Скомбинируем выражения так, чтобы слагаемое при \(a_1\) занулить, получим
где
Таким образом, мы выполнили один шаг экстраполяции Ричардсона. Важно отметить, что согласно (5.12) \(I^{(2)}\) приближает \(I\) лучше, чем \(I^{(1)}\), у которых ошибка порядка \(h^{\alpha_1}\).
Процесс экстраполяции можно продолжить, тогда в общем случае получим рекурретное выражение
Точность приближения для (5.13) составляет
Важно отметить структуру формулы (5.13): \((j+1)\)-ое приближение на сетке с шагом \(h_{k-1}\) можно получить по \((j)\)-приближениям на сетках с шагами \(h_k\) и \(h_{k-1}\).
\ j 1 2 3
k --------------
0 01 → 02 → 03
↗ ↗
1 11 → 12
↗
2 21
Теперь рассмотрим частный случай экстраполяции Ричардсона, метод Ромберга.
Положим в основу экстраполяции (5.13) формулу трапеций \(I_{h_k}^{(1)}=T_f(h_k)\) и будем мельчить сетку вдвое (\(q=0.5\)). Для составной формулы трапеций разложение (5.11) содержит только чётные степени, т.е. \(\alpha_j = 2 j\). Тогда, из (5.13) получим
Прежде чем переходить к реализации, рассмотрим особенность формулы трапеции при измельчении сетки, это позволит избежать перевычислений в (5.14).
5.2.2. Удвоение числа узлов#
Рассмотрим равномерную сетку \(a = x_0 < x_1 < x_2 < x_3 < x_4 = b\) из \(n=4\) интервалов с шагом \(h_4\) и сетку из \(n=2\) интервалов \(a = x_0 < x_2 < x_4 =b\) с шагом \(2h_4\).
На крупной сетке формула трапеций даёт
А на сетке вдвое мельче
Сравнивая (5.15) и (5.16), обнаруживаем их связь
Таким образом, мы в частном случае получили, что вычислив интеграл на крупной сетке, мы можем вычислить его на сетке вдвое мельче, используя только значения в новых узлах (\(x_1\), \(x_3\)).
Это верно и в общем случае
Данная формула позволяет вычислить интеграл с заданной точностью. Под точностью здесь понимается разница между новой аппроксимацией и старой
Например, при \(\text{tol} = 10^{-6}\) мы можем добиться точности до 6 знаков после запятой. Конечно, ошибки округления и обусловленность задачи никуда не делись.
Также отметим, что выражение (5.18) является абсолютной, а не относительной точностью.
Реализация может быть такой
Формула трапеций с контролем точности
"""
Вычисляет интеграл ∫`f`dx на [`a`, `b`] с точностью `atol` по формуле трапеций,
удваивая число разбиений интервала, но не более `maxstep` раз.
Возвращает значение интеграла.
"""
function trapezoid_tol(f, a, b; atol=1e-3, maxstep::Integer=100)
nc, hc = 1, float(b - a)
Tc = hc * (f(a) + f(b)) / 2
for step in 1:maxstep
Tp, np = Tc, nc
hc /= 2
nc *= 2
Tc = Tp / 2 + hc * sum(f, (a + hc*(2i-1) for i in 1:np))
abs(Tc - Tp) < atol && return Tc
end
error("Точность не удовлетворена.")
end
5.2.3. Реализация#
Итак, хранить \(I_{h_{k-1}}^{(j)}\) будем в элементе матрицы I[k, j]
.
Первый элемент I[1, 1]
заполним вручную: он соответствует формуле трапеций на сетке из одного интервала (2 узла). Затем, пока не достигнем необходимой точности, будем делать следующее
Измельчим сетку вдвое и подсчитаем интеграл по формуле трапеций на ней (5.17), так получим из
I[k, 1]
элементI[k+1, 1]
;Вычислим по доступной побочной диагонали слева направо все элементы от
I[k, 2]
вплоть доI[1, k+1]
по формуле перехода (5.14);Будем продолжать 1 и 2 до тех пор, пока приближения
I[1, k+1]
иI[2, k]
станут неотличимы в заданной точностиatol
, или пока не исчерпаем количество измельченийmaxstep
.
Пример заполнения матрицы I
. Допустим, максимальное измельчение maxstep = 4
, т.е. от \(h=b-a\) до \(h = (b-a) / 2^4\). Ниже приведена схема вычислений и обозначения
⋅: элемент матрицы, который не пригодится в вычислениях;
□: элемент, который, возможно, предстоит определить;
○ и ●: уже вычисленные элементы, причём ● сравниваются между собой в конце итерации;
стрелка вниз показывает переход по умельчению сетки (шаг 1);
остальные стрелки показывают переход по формуле (шаг 2).
1 итерация 2 итерация 3 итерация 4 итерация
○ → ● □ □ □ ○ ○ → ● □ □ ○ ○ ○ → ● □ ○ ○ ○ ○ → ●
↓ ↗ ↗ ↗ ↗
● □ □ □ ⋅ ○ → ● □ □ ⋅ ○ ○ → ● □ ⋅ ○ ○ ○ → ● ⋅
↓ ↗ ↗ ↗
□ □ □ ⋅ ⋅ ○ □ □ ⋅ ⋅ ○ → ○ □ ⋅ ⋅ ○ ○ → ○ ⋅ ⋅
↓ ↗ ↗
□ □ ⋅ ⋅ ⋅ □ □ ⋅ ⋅ ⋅ ○ □ ⋅ ⋅ ⋅ ○ → ○ ⋅ ⋅ ⋅
↓ ↗
□ ⋅ ⋅ ⋅ ⋅ □ ⋅ ⋅ ⋅ ⋅ □ ⋅ ⋅ ⋅ ⋅ ○ ⋅ ⋅ ⋅ ⋅
Метод Ромберга с контролем точности
"""
Вычисляет интеграл ∫`f`dx на отрезке [`a`, `b`] методом Ромберга.
Разбивает отрезок пополам не более `maxstep > 0` раз.
Возвращает значение интеграла, если приближения отличаются не более чем на `atol`.
"""
function romberg(f, a, b; atol=1e-6, maxstep::Integer=100)
maxstep = max(1, maxstep) # хотя бы одно разбиение
I = Matrix{Float64}(undef, maxstep+1, maxstep+1)
I[1, 1] = (b - a) * (f(a) + f(b)) / 2
for i in 2:maxstep+1
let hc = (b - a) / 2^(i-1), np = 2^(i-2)
I[i, 1] = I[i-1, 1] / 2 + hc * sum(f, (a + hc * (2i-1) for i in 1:np))
end
for k in i-1:-1:1
I[k, i-k+1] = (2^i*I[k+1, i-k] - I[k, i-k]) / (2^i - 1)
end
abs(I[1, i] - I[2, i-1]) < atol && return I[1, i]
end
error("Точность не удовлетворена.")
end
Сравним время вычисления интеграла
нашими реализациями trapezoid_tol и romberg.
julia> using BenchmarkTools
julia> foo(x) = x * exp(sin(2x));
julia> a, b, atol = 0, 3, 1e-6;
julia> @benchmark trapezoid_tol($foo, a, b; atol=$atol)
BenchmarkTools.Trial: 10000 samples with 1 evaluation.
Range (min … max): 121.110 μs … 452.256 μs ┊ GC (min … max): 0.00% … 0.00%
Time (median): 121.582 μs ┊ GC (median): 0.00%
Time (mean ± σ): 122.370 μs ± 6.715 μs ┊ GC (mean ± σ): 0.00% ± 0.00%
▇█▄▄▁▁ ▁
████████▇▆▆▅▆▄▅▃▃▄▄▁▄▅▄▅▅▅▆▅▆▅▅▄▅▄▃▅▅▄▄▄▁▄▄▄▄▄▄▄▄▄▄▃▃▄▅▅▅▁▃▄▅ █
121 μs Histogram: log(frequency) by time 144 μs <
Memory estimate: 272 bytes, allocs estimate: 17.
julia> @benchmark romberg($foo, a, b; atol=$atol)
BenchmarkTools.Trial: 10000 samples with 1 evaluation.
Range (min … max): 8.434 μs … 3.711 ms ┊ GC (min … max): 0.00% … 99.35%
Time (median): 13.210 μs ┊ GC (median): 0.00%
Time (mean ± σ): 20.341 μs ± 103.650 μs ┊ GC (mean ± σ): 20.16% ± 4.08%
▃ ▃█▆▄▃▄▄▃▂▁ ▁
█▅▆▅███████████▇█▇▇▇▇▇▇▆▆█▇▇▇▇▇▇▇▆▆▆▅▅▅▅▄▃▅▄▅▃▃▄▄▅▅▄▄▃▄▃▃▄▃▂ █
8.43 μs Histogram: log(frequency) by time 57.9 μs <
Memory estimate: 79.89 KiB, allocs estimate: 6.
Получаем, что на данной машине реализация метода Ромберга (несмотря на аллоцирование матрицы), примерно в 6 раз (≈ 120 мкс / 20 мкс) быстрее вычислила интеграл.
Если немного модифицировать код и посмотреть, сколько разбиений сетки произошло при вычислениях, то получим, что trapezoid_tol
потребовалось 12 разбиений сетки, а romberg
всего 8. Это означает, что trapezoid_tol
вычисляла функцию примерно на \(2^{12} - 2^8 \approx 3800\) раз больше.
В свою очередь, одно вычисление функции foo
стоит около 14 нс.
julia> x = 1.5;
julia> @benchmark foo($x)
BenchmarkTools.Trial: 10000 samples with 998 evaluations.
Range (min … max): 13.746 ns … 171.441 ns ┊ GC (min … max): 0.00% … 0.00%
Time (median): 14.281 ns ┊ GC (median): 0.00%
Time (mean ± σ): 14.639 ns ± 3.666 ns ┊ GC (mean ± σ): 0.00% ± 0.00%
▂█▆▃█▄▂▁▁ ▂
██████████▇▆▆▆▆▆▅▇▄▅▅▆▅▅▄▅▄▃▁▅▃▄▃▁▄▄▄▅▁▃▅▄▄▁▁▄▄▁▃▁▅▄▅▃▄▃▅▁▄▄ █
13.7 ns Histogram: log(frequency) by time 24.7 ns <
Memory estimate: 0 bytes, allocs estimate: 0.
Значит, romberg
только на вычислениях функции «сэкономил» примерно 3800 × 14 нс ≈ 53 мкс, что составляет около 40% времени вычисления trapezoid_tol
.