Решение PDE с неявным эйлером в python - неверный вывод - PullRequest
14 голосов
/ 19 марта 2019

Я попытаюсь объяснить точно, что происходит и моя проблема.

Это немного математично, и SO не поддерживает латекс, поэтому, к сожалению, мне пришлось прибегнуть к изображениям.Я надеюсь, что все в порядке.

enter image description here

enter image description here

Я не знаю, почему это инвертировано,Извини за это.Во всяком случае, это линейная система Ax = b, где мы знаем A и b, поэтому мы можем найти x, что является нашим приближением на следующем шаге по времени.Мы продолжаем делать это до времени t_final.

Это код

import numpy as np

tau = 2 * np.pi
tau2 = tau * tau
i = complex(0,1)

def solution_f(t, x):
    return 0.5 * (np.exp(-tau * i * x) * np.exp((2 - tau2) * i * t) + np.exp(tau * i * x) * np.exp((tau2 + 4) * i * t))

def solution_g(t, x):
    return 0.5 * (np.exp(-tau * i * x) * np.exp((2 - tau2) * i * t) - np.exp(tau * i * x) * np.exp((tau2 + 4) * i * t))

for l in range(2, 12):
    N = 2 ** l #number of grid points
    dx = 1.0 / N #space between grid points
    dx2 = dx * dx
    dt = dx #time step
    t_final = 1
    approximate_f = np.zeros((N, 1), dtype = np.complex)
    approximate_g = np.zeros((N, 1), dtype = np.complex)

    #Insert initial conditions
    for k in range(N):
        approximate_f[k, 0] = np.cos(tau * k * dx)
        approximate_g[k, 0] = -i * np.sin(tau * k * dx)

    #Create coefficient matrix
    A = np.zeros((2 * N, 2 * N), dtype = np.complex)

    #First row is special
    A[0, 0] = 1 -3*i*dt
    A[0, N] = ((2 * dt / dx2) + dt) * i
    A[0, N + 1] = (-dt / dx2) * i
    A[0, -1] = (-dt / dx2) * i

    #Last row is special
    A[N - 1, N - 1] = 1 - (3 * dt) * i
    A[N - 1, N] = (-dt / dx2) * i
    A[N - 1, -2] = (-dt / dx2) * i
    A[N - 1, -1] = ((2 * dt / dx2) + dt) * i

    #middle
    for k in range(1, N - 1):
        A[k, k] = 1 - (3 * dt) * i
        A[k, k + N - 1] = (-dt / dx2) * i
        A[k, k + N] = ((2 * dt / dx2) + dt) * i
        A[k, k + N + 1] = (-dt / dx2) * i

    #Bottom half
    A[N :, :N] = A[:N, N:]
    A[N:, N:] = A[:N, :N]

    Ainv = np.linalg.inv(A)

    #Advance through time
    time = 0
    while time < t_final:
        b = np.concatenate((approximate_f, approximate_g), axis = 0)
        x = np.dot(Ainv, b) #Solve Ax = b
        approximate_f = x[:N]
        approximate_g = x[N:]
        time += dt
    approximate_solution = np.concatenate((approximate_f, approximate_g), axis=0)

    #Calculate the actual solution
    actual_f = np.zeros((N, 1), dtype = np.complex)
    actual_g = np.zeros((N, 1), dtype = np.complex)
    for k in range(N):
        actual_f[k, 0] = solution_f(t_final, k * dx)
        actual_g[k, 0] = solution_g(t_final, k * dx)
    actual_solution = np.concatenate((actual_f, actual_g), axis = 0)

    print(np.sqrt(dx) * np.linalg.norm(actual_solution - approximate_solution))

Он не работает.По крайней мере, не в начале, это не должно начинаться так медленно.Я должен быть безусловно устойчивым и сходиться к правильному ответу.

Что здесь не так?

Ответы [ 2 ]

6 голосов
/ 27 марта 2019

L2-норма может быть полезной метрикой для проверки сходимости, но не идеальна при отладке, поскольку не объясняет, в чем проблема.Хотя ваше решение должно быть безусловно стабильным, отсталый Эйлер не обязательно сходится к правильному ответу.Так же, как форвард Эйлер, как известно, нестабилен (антидиссипативен), отсталый Эйлер, как известно, диссипативен.Постановка ваших решений подтверждает это.Численные решения сходятся к нулю.Для приближения следующего порядка Крэнк-Николсон является разумным кандидатом.Приведенный ниже код содержит более общий тета-метод, так что вы можете настроить неявное решение.тета = 0,5 дает CN, тета = 1 дает BE, а тета = 0 дает FE.Несколько других вещей, которые я подправил:

  • Я выбрал более подходящий временной шаг dt = (dx ** 2) / 2 вместо dt = dx.Это последнее не сходится к правильному решению с использованием CN.
  • Это незначительное замечание, но поскольку t_final не гарантирует кратность dt, вы не сравнивали решения на одном шаге по времени.
  • Что касается вашего комментария о том, что он медленный: по мере увеличения пространственного разрешения ваше разрешение по времени также должно увеличиваться.Даже в вашем случае с dt = dx вы должны выполнить (1024 x 1024) * 1024 матричное умножение 1024 раза.Я не нашел, что это займет особенно много времени на моей машине.Я удалил некоторую ненужную конкатенацию, чтобы немного ускорить ее, но изменение шага по времени на dt = (dx ** 2) / 2, к сожалению, действительно затормозит.Вы могли бы попытаться скомпилировать с Numba, если вы обеспокоены скоростью.

Все это говорит о том, что я не нашел огромного успеха с последовательностью CN.Я должен был установить N = 2 ^ 6, чтобы получить что-нибудь в t_final = 1.Увеличение t_final делает это хуже, уменьшение t_final делает его лучше.В зависимости от ваших потребностей, вы можете изучить реализацию TR-BDF2 или других линейных многошаговых методов, чтобы улучшить это.

Код с графиком приведен ниже:

import numpy as np
import matplotlib.pyplot as plt

tau = 2 * np.pi
tau2 = tau * tau
i = complex(0,1)

def solution_f(t, x):
    return 0.5 * (np.exp(-tau * i * x) * np.exp((2 - tau2) * i * t) + np.exp(tau * i * x) * np.exp((tau2 + 4) * i * t))

def solution_g(t, x):
    return 0.5 * (np.exp(-tau * i * x) * np.exp((2 - tau2) * i * t) - np.exp(tau * i * x) * 
np.exp((tau2 + 4) * i * t))

l=6
N = 2 ** l 
dx = 1.0 / N 
dx2 = dx * dx
dt = dx2/2
t_final = 1.
x_arr = np.arange(0,1,dx)

approximate_f = np.cos(tau*x_arr)
approximate_g = -i*np.sin(tau*x_arr)

H = np.zeros([2*N,2*N], dtype=np.complex)
for k in range(N):
    H[k,k] = -3*i*dt
    H[k,k+N] = (2/dx2+1)*i*dt    
    if k==0:
        H[k,N+1] = -i/dx2*dt
        H[k,-1] = -i/dx2*dt     
    elif k==N-1:
        H[N-1,N] = -i/dx2*dt
        H[N-1,-2] = -i/dx2*dt    
    else:
        H[k,k+N-1] = -i/dx2*dt
        H[k,k+N+1] = -i/dx2*dt
### Bottom half
H[N :, :N] = H[:N, N:]
H[N:, N:] = H[:N, :N]

### Theta method. 0.5 -> Crank Nicolson
theta=0.5
A = np.eye(2*N)+H*theta
B = np.eye(2*N)-H*(1-theta)

### Precompute for faster computations
mat = np.linalg.inv(A)@B

t = 0
b = np.concatenate((approximate_f, approximate_g))
while t < t_final:
    t += dt
    b = mat@b

approximate_f = b[:N]
approximate_g = b[N:]
approximate_solution = np.concatenate((approximate_f, approximate_g))

#Calculate the actual solution
actual_f = solution_f(t,np.arange(0,1,dx))
actual_g = solution_g(t,np.arange(0,1,dx))
actual_solution = np.concatenate((actual_f, actual_g))

plt.figure(figsize=(7,5))
plt.plot(x_arr,actual_f.real,c="C0",label=r"$Re(f_\mathrm{true})$")
plt.plot(x_arr,actual_f.imag,c="C1",label=r"$Im(f_\mathrm{true})$")
plt.plot(x_arr,approximate_f.real,c="C0",ls="--",label=r"$Re(f_\mathrm{num})$")
plt.plot(x_arr,approximate_f.imag,c="C1",ls="--",label=r"$Im(f_\mathrm{num})$")
plt.legend(loc=3,fontsize=12)
plt.xlabel("x")

plt.savefig("num_approx.png",dpi=150)

enter image description here

0 голосов
/ 19 марта 2019

Я не собираюсь проходить через всю вашу математику, но я собираюсь предложить предложение.

Использование прямого вычисления для f<sub>xx</sub> и g<sub>xx</sub> кажется хорошим кандидатом для численной нестабильности. Интуитивно понятно, что метод первого порядка должен допускать ошибки второго порядка в терминах. Ошибки второго порядка в отдельных терминах, после прохождения этой формулы, оказываются постоянными ошибками порядка во второй производной. Кроме того, когда размер шага становится небольшим, вы обнаружите, что квадратичная формула превращает даже небольшие ошибки округления в удивительно большие ошибки.

Вместо этого я бы предложил вам превратить это в систему первого порядка из 4 функций: f, f<sub>x</sub>, g и g<sub>x</sub>. А затем перейдите к Эйлеру в этой системе. Интуитивно понятно, что при таком подходе метод первого порядка создает ошибки второго порядка, которые проходят через формулу, которая создает их ошибки первого порядка. И теперь вы сходитесь с нуля с самого начала и также не так чувствительны к распространению ошибок округления.

...