Почему мое уравнение вязкой волны разрушается при моделировании python? - PullRequest
0 голосов
/ 16 июня 2020

Я пытаюсь реализовать уравнение 16 из этой статьи :

enter image description here

Уравнение 16 выше является уравнением вязкой волны, и предполагается, что он начнется с большого размера и со временем d ie выйдет, в конечном итоге приближаясь к нулю. Однако когда я запускаю свою симуляцию, кажется, что он взрывается. Если вы посмотрите на изображения ниже (итерации 0–4), кажется, что он работает правильно (то есть синусоидальная волна, кажется, становится меньше, и это хорошо). Однако на уровнях 5,6 и выше он начинает взрываться (вы можете увидеть ось Y, показывающую увеличение давления на порядки).

Вот результат:

Iteration 0

enter image description here

enter image description here

enter image description here

enter image description here

enter image description here

enter image description here

Вот код, который использует Метод конечных разностей :

import numpy as np
import math
import matplotlib
import matplotlib.pyplot as plt
import time

x_mesh_param = 1000
# function that increments time steps (solves for the next state of p)
# returns 'p2'
# assuming no boundary conditions because we only care about what a hydrophone
# receives.  Don't care about what happens to wave beyond hydrophone.
def iterate(p0,p1,x_mesh,dt,v,c0 ):

    # step 3 on pg. 9

    dx=x_mesh[1]-x_mesh[0]
    p2 = np.zeros(p1.shape) # this is where we store next state of p

    for i in range(1,len(x_mesh)-1):
        p_txx = (p1[i+1]-p0[i+1]-2*(p1[i]-p0[i])+p1[i-1]-p0[i-1])/ (dt* dx**2)
        p_xx  = (p1[i+1]-2*p1[i]+p1[i-1]) / (dx**2)
        p2[i] = (dt)**2 * (4.0/3 * v * p_txx + c0**2*p_xx)+2*p1[i]-p0[i]

    # what happens at p2[0] (begin) and p2[-1] (end)

    # forward difference (NO BOUNDARY conditions)
    p_txx = (p1[2]-p0[2]-2*(p1[1]-p0[1])+p1[0]-p0[0])/(dt*dx**2)
    p_xx = (p1[2]-2*p1[1]+p1[0]) / (dx**2)
    #p2[0] = (dt)**2 * (4.0/3 * v * p_txx + c0**2*p_xx)+2*p1[0]-p0[0]

    # taylor series
    #p2[0]=1/(1-1/dt+1/(2*(dt)**2))* ((1-1/dt+1/((dt)**2))*p2[1]-p2[2]/(2*(dt**2)))

    #p2[0] = p1[1] # NEUMANN 

    # if you comment out lines 34,37,39 you get Dirichlet Conditions

    # backward difference
    p_txx = (p1[-1]-p0[-1]-2*(p1[-2]-p0[-2])+p1[-3]-p0[-3])/(dt*dx**2)
    p_xx = (p1[-1]-2*p1[-2]+p1[-3]) / (dx**2)
    #p2[-1] = (dt)**2 * (4.0/3 * v * p_txx + c0**2*p_xx)+2*p1[-1]-p0[-1]





    # Dirichlet if line 46 commented out
    return p2

def firstIteration(p1,x_mesh,dt,v,c0 ):


    # step 3 on pg. 9

    dx=x_mesh[1]-x_mesh[0]
    p2 = np.zeros(p1.shape) # this is where we store next state of p

    for i in range(1,len(x_mesh)-1):
        p_txx = 0
        p_xx  = (p1[i+1]-2*p1[i]+p1[i-1]) / (dx**2)
        p2[i] = (dt)**2 * (4.0/3 * v * p_txx + c0**2*p_xx)+p1[i] # assuming p1[i]-p0[i]=0 (coming from assumption that d/dt (p(x,0)) =0 (initial cond. of motion of fluid)

    # what happens at p2[0] (begin) and p2[-1] (end)


    # forward difference (NO BOUNDARY conditions)
    p_txx = 0
    p_xx = (p1[2]-2*p1[1]+p1[0]) / (dx**2)
    #p2[0] = (dt)**2 * (4.0/3 * v * p_txx + c0**2*p_xx)+p1[0]

    # taylor series
    #p2[0]=1/(1-1/dt+1/(2*(dt)**2))* ((1-1/dt+1/((dt)**2))*p2[1]-p2[2]/(2*(dt**2)))

    #p2[0] = p1[1] # NEUMANN 

    # if you comment out lines 34,37,39 you get Dirichlet Conditions

    # backward difference
    p_txx = 0
    p_xx = (p1[-1]-2*p1[-2]+p1[-3]) / (dx**2)
    #p2[-1] = (dt)**2 * (4.0/3 * v * p_txx + c0**2*p_xx)+p1[-1]

    return p2

def simulate():
    states = []
    L=100 
    #dt=.02
    dt=0.001
    v = .001/998
    c0 = 1480

    x_mesh = np.linspace(0,L,x_mesh_param)


    state_initial = np.array([math.sin(x/20*math.pi) for x in x_mesh])
    states.append(state_initial)

    states.append(firstIteration(states[0], x_mesh,dt,v,c0 ))

    for i in range(50):
        new_state = iterate(states[-2], states[-1], x_mesh,dt,v,c0)
        states.append(new_state)

    return states



if __name__=="__main__":
    states = simulate()
    L=100
    x_mesh = np.linspace(0,L,x_mesh_param)

    counter=0
    for s in states:
        fig, ax = plt.subplots()
        ax.plot(x_mesh, s)

        ax.set(xlabel='distance (m)', ylabel='pressure (atm)',
               title='Pressure vs. distance, iteration = '+str(counter))
        ax.grid()

        plt.show()
        print counter
        counter = counter + 1

Вы увидите есть основная программа, которая вызывает simulate (). Затем simulate () вызывает firstIteration (), который пытается обработать граничное условие. Об остальном позаботится итерация (). Я в основном использую центральную, прямую и обратную разности для вычисления производных волнового уравнения.

Основная программа просто распечатывает целую фигуру с разными временными шагами.

1 Ответ

3 голосов
/ 16 июня 2020

Вы реализовали числовой решатель для линейного (без произведения p) уравнения в частных производных, которое, кажется, работает для ряда временных шагов, а затем взрывается с быстрыми колебаниями.

Математически происходит следующее: ваша дискретная система уравнений имеет решения, которые экспоненциально растут со временем. Числовой шум в наименее значащих цифрах ваших чисел с плавающей запятой будет экспоненциально расти, пока не примет решение, которое вы ищете.

Если P - вектор (массив из 2N значений), состоящий из значения давления N на текущем и предыдущем временных шагах, все сложение и вычитание значений в вашем коде можно переписать в матричной форме (представляющей один временной шаг):

P := D @ P

где D матрица формы (2N, 2N). Если P0 было начальным состоянием системы, то состояние системы после n временных шагов будет

Pn = np.linalg.matrix_power(D, n) @ P0

Здесь matrix_power - матричное возведение в степень, т.е. matrix_power(D, 3) == D @ D @ D. Если D имеет собственные значения с модулем> 1, то будут экспоненциально растущие решения. Ваши данные показывают, что наибольшее собственное значение составляет около 1000, что означает, что оно выросло в 1e + 18 раз за 6 временных шагов, тогда как физически релевантный собственный вектор составляет около 0,9. Обратите внимание, что числа с плавающей запятой имеют точность около 1e-16.

Вы можете проверить собственные значения, используя np.linalg.eigvals, желательно с небольшой матрицей, например N=100. Если вам повезет, то уменьшения размеров шага dx и dt может быть достаточно, чтобы собственные значения оказались ниже единицы.

Быстрое и грязное решение

Это быстро реализовать, но не очень эффективно. Вы можете попытаться удалить большие собственные значения из D:

import numpy as np
evals, evecs = np.linalg.eig(D)
evals_1 = np.where(np.abs(evals) > 1, 0, evals)
D_1 = evecs @ np.diag(evals_1) @ np.linalg.inv(evecs)

Например, с небольшой матрицей (собственные значения 0,9 и 1000):

D = np.array([[500.45, -499.55], 
              [-499.55, 500.45]])

# D_1 calculated as above.
D_1 = np.array([[0.45, 0.45],
                [0.45, 0.45]])

P0 = [1, 1+8e-16]
powm = np.linalg.matrix_power

ns = np.arange(7)
Ps_D = np.array([powm(D, n) @ P0 for n in ns])
Ps_D1 = np.array([powm(D_1, n) @ P0 for n in ns])

import matplotlib.pyplot as plt
fig, ax = plt.subplots()
ax.semilogy(ns, Ps_D[:, 1], 'ro-', label='Using D')
ax.semilogy(ns, Ps_D1[:, 1], 'bs-', label='Using D_1')
ax.set_xlabel('Iteration number')
ax.set_ylabel('P[1]')
ax.grid()
ax.legend()
fig.show()

Time evolution with good and bad eigenvalues

If you went through the trouble of finding the eigenvalues and eigenvectors (which can be slow for large matrices), you can speed up the subsequent calculations quite a bit by replacing matpow(D_1, n) by

Pn = evecs @ np.diag(evals_1**n) @ np.linalg.inv(evecs) @ P0

Quick sanity check for tuning step sizes

It is possible to judge whether it is likely that there are eigenvalues greater than one by setting your initial state to

P = np.zeros(N)
P[N//4] = 1

and executing one single time step. If this results in values >1 in P, you are likely to have too large eigenvalues. (The new value for P is actually the column D[:, N//4] of the matrix).

Implicit methods

Rather than bluntly zeroing large eigenvalues as described above, it may be better to resort to an неявный метод . Для этого необходимо записать матрицу D. Если P - текущее состояние, а Pnew - состояние для следующего временного шага, тогда вы начинаете с уравнения

Pnew = D @ (0.5*(P + Pnew))

Решите это матричное уравнение для Pnew:

# Pnew = inv(1 - 0.5 D) (0.5 D) P
nD = D.shape[0]
D_2 = np.linalg.inv(np.eye(nD) - 0.5*D) @ (0.5*D)

# time step:
Pnew = D_2 @ P

Это метод Крэнка-Николсона . Это требует, чтобы вы построили матрицу D. Применение к тому же примеру, что и выше, дает:

D_2 = np.array([[-0.09191109,  0.91009291],
                [ 0.91009291, -0.09191109]])
evals = np.array[ 0.81818182, -1.00200401]

Это уменьшает наибольшее собственное значение с 1000 до -1,002, что лучше, но все же больше 1 (по абсолютной величине), поэтому, начиная с ошибки округления на 1e-15 он обгонит желаемое решение примерно через 34k временных шагов. Более того, это уменьшает собственное значение 0,9 до 0,8. Вам нужно будет выяснить, какой из них лучше описывает физику. К сожалению, численное решение уравнений в частных производных сложно.

РЕДАКТИРОВАТЬ: Лутц Леманн указал, что работа с инверсиями больших матриц (результат точной сетки по x) не является хорошей идеей. Обратите внимание, что D - это разреженная матрица, лучше представленная как scipy.sparse.csr_matrix, csc_matrix или dia_matrix, чем np.array. Затем вы должны решить

(1 - 0.5*D) @ P_new = 0.5*D @ P

, которое представляет собой матричное уравнение в форме Ax = b с векторами x и b:

A = scipy.sparse.identity(nD) - 0.5*D

for every time step:
   b = D @ (0.5*P)
   P = scipy.sparse.linalg.spsolve(A, b)

Обратите внимание, что spsolve все еще может быть медленный. Это может помочь расположить P с чередованием текущего и предыдущего значений давления, чтобы D было полосно-диагональным и могло быть сохранено как разреженное dia_matrix.

Изменить:

Следовательно, наиболее эффективный подход будет таким:

import scipy.sparse as sp

n = 200 # Example - number of x points
nD = 2*n # matrix size
D = sp.dok_matrix((nD, nD))

# Initialize pressures
P = np.zeros(nD)

# ... initialize P and nonzero matrix elements here ...

# Prepare matrices
D = sp.csc_matrix(D) # convert to efficient storage format
A = sp.identity(nD) - 0.5*D
A_lu = sp.linalg.splu(A)

for _i in range(num_time_steps):
   b = D @ (0.5*P)
   P = A_lu.solve(b)
...