Дискретизация Задачи Коши. Частный случай, общий подход#
Рассмотрим общий подход к численному решению ОДУ.
Задача Коши ставится следующим образом:
Далее будем полагать, что решение существует и единственно.
В общем случае аналитически не решается - а решать-то хочется.
Понятное дело, на компьютере не можем работать с континуумом значений. Необходимо произвести дискретизацию задачи.
Зададим сетку с равным шагом:
Также зададим дискретную функцию \(y_n\) на этой сетке, от которой хотим
Таким образом, \(y_n\) - это значения решения дискретизованного ОДУ на заданной сетке.
Важно. Заметьте, \(u(t)\) - истинное (математическое) решение ОДУ, а \(y_n\) - это сеточно-заданная функция. Таки разные вещи.
Вспомним про численную аппроксимацию производной. Выберем какую-нибудь численную схему, например, самую простую. Тогда
Подставляя это в изначальное уравнение, мы получаем дискретизованную Задачу Коши или разностная задача:
Примечание. Конкретно такой вид дискретизации называется явная схема Эйлера. Естественно, дискретизовывать можно по разному, выбирая различные разностные схемы и правые части, которые формально стремятся к истинной задаче Коши при \(\tau \rightarrow 0\).
Явную схему Эйлера уже понятно, как решать. Это же просто реккурента:
Далее постараемся обобщить этот подход.
Show code cell source
from bokeh.io import output_notebook
from bokeh.resources import INLINE
output_notebook(INLINE)
Show code cell source
import numpy as np
import sympy
from bokeh.io import show, output_file
from bokeh.layouts import column
from bokeh.models import ColumnDataSource, Slider, CustomJS, MultiLine
from bokeh.plotting import figure
from bokeh.palettes import Sunset10
blue, yellow = Sunset10[0], Sunset10[5]
from bokeh.models import Label
# Define the ODE to be solved
t = sympy.symbols('t')
y = sympy.Function('y')(t)
f = -y + sympy.sin(t)
# Set initial conditions and parameters
y0 = 1.0 # initial value of y
t0 = 0.0 # initial time
T = 10.0 # final time
# Calculate the exact solution using sympy and convert to numpy function
y_exact = sympy.dsolve(sympy.Eq(y.diff(t), f), y, ics={y.subs(t, t0): y0})
y_exact_func = sympy.lambdify(t, y_exact.rhs, 'numpy')
t_arr_dense = np.linspace(t0, T, 1000) # denser time array for exact solution
y_exact_arr = y_exact_func(t_arr_dense)
# Define the initial value of h for the plot
h_init = 0.5
# Initialize arrays to store the results
t_arr = np.arange(t0, T + h_init, h_init) # time array
y_arr = np.zeros_like(t_arr) # solution array
y_arr[0] = y0 # set the initial value of y
# Solve the ODE using Explicit Euler scheme
for i in range(0, t_arr.size - 1):
y_arr[i + 1] = y_arr[i] + h_init * f.subs([(y, y_arr[i]), (t, t_arr[i])]).evalf()
# Create a separate ColumnDataSource for the dense arrays
dense_source = ColumnDataSource(data=dict(t_arr_dense=t_arr_dense, y_exact_arr=y_exact_arr))
# Create the ColumnDataSource for the other arrays
source = ColumnDataSource(data=dict(
t_arr=t_arr,
y_arr=y_arr,
y_exact_func=y_exact_func(t_arr),
))
# Create a separate ColumnDataSource for the vertical intervals
intervals_source = ColumnDataSource(data=dict(xs=[[t_arr[i], t_arr[i]] for i in range(len(t_arr))],
ys=[[y_arr[i], y_exact_func(t_arr[i])] for i in range(len(t_arr))]))
# Create the plot
plot = figure(x_range=(t0, T), y_range=(-1.5, 1.1), width=500, height=300,
title='ODE Solution with Explicit Euler Scheme')
plot.toolbar.active_drag = None
plot.line('t_arr_dense', 'y_exact_arr', line_color='black', source=dense_source)
plot.circle('t_arr', 'y_exact_func', radius = 0.05, line_color='red', fill_color=yellow, source=source)
plot.circle('t_arr', 'y_arr', radius = 0.05, line_color='blue', fill_color=blue, source=source)
plot.multi_line('xs', 'ys', line_color='grey', line_dash='dotted', source=intervals_source).level = 'underlay' # Add vertical intervals
import sys
import os
try:
sys.stderr = open(os.devnull, 'w') # for wsl build
except:
pass
label = Label(
text=r"$$y(x)\text{ - True Solution}$$",
x=10, y=65,
x_units="screen", y_units="screen",
)
label1 = Label(
text=r'$$y^{(τ)}=\{y_n\}_{n=0}^N \text{ - Discretized ODE Solution}$$',
x=20, y=35,
x_units="screen", y_units="screen",
)
plot.circle(0.25, -1.05, radius = 0.05, line_color='blue', fill_color=blue)
label2 = Label(
text=r"$$[y(x)]^{(τ)}=\{y(x_n)\}_{n=0}^N\text{ - True Solution Projection}$$",
x=20, y=5,
x_units="screen", y_units="screen",
)
plot.circle(0.25, -1.35, radius = 0.05, line_color='red', fill_color=yellow)
plot.add_layout(label)
plot.add_layout(label1)
plot.add_layout(label2)
# Create a slider widget for h
slider = Slider(start=0.01, end=1.0, value=h_init, step=0.01, title='h', sizing_mode = "stretch_width")
# Define the JavaScript callback function
update_plot_code = """
const h = this.value;
const t0 = 0.0;
const T = 10.0;
const y0 = 1.0;
const t_arr = Array.from(Array(Math.ceil((T - t0) / h) + 1), (_, i) => t0 + i * h);
const y_arr = new Array(t_arr.length).fill(0);
y_arr[0] = y0;
for (let i = 0; i < t_arr.length - 1; i++) {
const t = t_arr[i];
const y = y_arr[i];
y_arr[i + 1] = y + h * (Math.sin(t) - y);
}
const t_arr_dense = Array.from(Array(1000), (_, i) => t0 + i * (T - t0) / 999);
const y_exact_func = (t) => 1.5*Math.exp(-t) + Math.sin(t)/2 - Math.cos(t)/2;
const y_exact_arr = t_arr_dense.map(y_exact_func);
const y_exact_func_arr = t_arr.map(y_exact_func);
// Add code to create vertical intervals
const xs = [];
const ys = [];
for (let i = 0; i < t_arr.length; i++) {
const y_min = Math.min(y_arr[i], y_exact_func_arr[i]);
const y_max = Math.max(y_arr[i], y_exact_func_arr[i]);
xs.push([t_arr[i], t_arr[i]]);
ys.push([y_min, y_max]);
}
source.data = {
t_arr: t_arr,
y_arr: y_arr,
y_exact_func: y_exact_func_arr,
};
dense_source.data = {
t_arr_dense: t_arr_dense,
y_exact_arr: y_exact_arr,
};
intervals_source.data = {
xs: xs,
ys: ys,
};
"""
callback = CustomJS(args=dict(source=source, dense_source=dense_source, intervals_source=intervals_source), code=update_plot_code)
slider.js_on_change('value', callback)
# Trigger the initial update of the plot
callback.code.replace("this.value", str(h_init))
# Create a layout with the plot and slider
layout = column(plot, slider)
# Display the layout
show(layout)
Примечание. Видим, что при больших значениях шага, решение дискретизованного ОДУ (синие точки) не совпадает с истинным (математическим) решением. Обратите так же внимание на обозначения в легенде.
Примеры различных простых дискретизаций#
Пусть задача Коши \(\dfrac{d y}{d x}=f(t, y)\)
Тогда, несколько вариантов соответствющих ей разностных задач \(\hat{L}_{\tau} y^{(\tau)}=F^{(\tau)}\):
Явная схема Эйлера
Неявная схема Эйлера
Неявная она потому, что в общем случае на каждом шаге приходится решать нелинейное уравнение на \(y_{n+1}\).
Симметризованная схема
Схема с центральной разностью
Видно, что строить их достаточно просто - главное обеспечить «аля» сходимость \(\hat{L}_{\tau} \rightarrow \hat{L}\) и \(F^{(\tau)} \rightarrow F(x)\).
Примечание. Первые три схемы относятся к двуслойным схемам, а четвёртая к трёхслойным. Подумайте почему.
Примечание. Начальные условия в виде производной в какой-то точке также необходимо дискретизовывать, и это влияет на точность. Смотреть пример ниже.
Для определения того, какая схема лучше и корректна ли схема вообще, необходимо ввести какие-то числовые параметры.