Python 一维波动方程数值解及可视化
一、效果展示
- 两端固定,初值条件为
- 右端为自由端,在前两秒施加外力,随后转为固定端
- 两端施加不同频率外力
二、 求解原理
a. 微分方程
一维波动方程的一般形式如下
b. 差分方程
我们先不考虑初值条件与边界条件,为了在不求该方程解析解的情况下描述方程图像,我们对原始方程进行差分处理。
设 在数轴上被均匀的分割为 等分段,每一段长度为, 则第段位移在第 段时间内,可以表示为
当 且 时,可以认为
进而有
同理可得
忽略高阶项,并将(3), (4)代入方程(1)中,得
上式中可以看到,如果需要求时间第时刻的波动方程,只需要知道 时刻的
我们将上式称为一维波动方程的差分形式。
c. 边界条件处理
- 第一类边界条件
在差分方程中可以写成如下形式
- 第二类边界条件
差分形式: - 第三类边界条件
差分形式:
三、Python 实现
1. 变量设计
# 波速
wave_velocity = 1
# x范围和节点数
x_min, x_max, number_dx = 0, 1, 100
# t范围和节点数
t_min, t_max, number_dt = 0, 2, 300
# x分片
x_ticks = np.linspace(x_min, x_max, number_dx)
t_ticks = np.linspace(t_min, t_max, number_dt)
# 每小段dx, dt的长度
dx = (x_max - x_min) / (number_dx - 1)
dt = (t_max - t_min) / (number_dt - 1)
# 时间步控制变量
time_step = 1
# 微分方程记录数组
u = np.zeros((number_dx, number_dt))
2. 初始化
phi = lambda x: np.sin(6 * np.pi * x)
for i, x in enumerate(self.x_ticks):
# t = 0 时的初值
u[i][0] = phi(x)
3. 迭代函数
def next_step():
global time_step
# 时间还没有结束时
if t_ticks[self._time_step] < t_max:
# 获取当前时间的波动方程各点值
u_t = u[:, time_step]
end = len(u_t) - 1
# 计算下一个时间点的各点坐标值
ddu = list(u_t[0: end - 1] - 2 * u_t[1: end] + u_t[2: end + 1])
# 由于计算时,左右两个边界点无法通过差分计算(超过数值范围)
# 这里将左右两个点先设置成0,之后再代入边界条件进行处理
ddu = np.array([0] + ddu + [0])
u[:, time_step + 1] = (wave_velocity * dt / dx) ** 2 * ddu + 2 * u[:, time_step] - u[:, time_step - 1]
# 时间步自增,并且计算边界点的值
# 这里左右端点都使用第一类边界条件
time_step += 1
u[0][time_step] = 0
u[-1][time_step] = 0
return time_step - 1
else:
# 此时迭代结束
return -1
4. 执行
while True:
result = next_step()
if result == -1:
break
5. 绘制
u_max = np.max(u)
for i in range(number_dt):
plt.clf()
plt.plot(x_ticks, u[:, i])
plt.axis((x_min - x_max / 10, x_max + x_max / 10, -1.2 * u_max, 1.2 * u_max))
plt.xlabel("Distance (x), t = {:.2f}(s)".format(t_ticks[i]))
plt.ylabel("u")
plt.pause(dt / 2)
plt.show()
6. 汇总
import numpy as np
import matplotlib.pyplot as plt
# 波速
wave_velocity = 1
# x范围和节点数
x_min, x_max, number_dx = 0, 1, 100
# t范围和节点数
t_min, t_max, number_dt = 0, 2, 300
# x分片
x_ticks = np.linspace(x_min, x_max, number_dx)
t_ticks = np.linspace(t_min, t_max, number_dt)
# 每小段dx, dt的长度
dx = (x_max - x_min) / (number_dx - 1)
dt = (t_max - t_min) / (number_dt - 1)
# 时间步控制变量
time_step = 1
# 微分方程记录数组
u = np.zeros((number_dx, number_dt))
phi = lambda x: np.sin(3 * np.pi * x)
for i, x in enumerate(x_ticks):
# t = 0 时的初值
u[i][0] = phi(x)
def next_step():
global time_step
# 时间还没有结束时
if t_ticks[time_step] < t_max:
# 获取当前时间的波动方程各点值
u_t = u[:, time_step]
end = len(u_t) - 1
# 计算下一个时间点的各点坐标值
ddu = list(u_t[0: end - 1] - 2 * u_t[1: end] + u_t[2: end + 1])
# 由于计算时,左右两个边界点无法通过差分计算(超过数值范围)
# 这里将左右两个点先设置成0,之后再代入边界条件进行处理
ddu = np.array([0] + ddu + [0])
u[:, time_step + 1] = (wave_velocity * dt / dx) ** 2 * ddu + 2 * u[:, time_step] - u[:, time_step - 1]
# 时间步自增,并且计算边界点的值
# 这里左右端点都使用第一类边界条件
time_step += 1
u[0][time_step] = 0
u[-1][time_step] = 0
return time_step - 1
else:
# 此时迭代结束
return -1
def main():
while True:
result = next_step()
if result == -1:
break
u_max = np.max(u)
for t in range(number_dt):
plt.clf()
plt.plot(x_ticks, u[:, t])
plt.axis((x_min - x_max / 10, x_max + x_max / 10, -1.2 * u_max, 1.2 * u_max))
plt.xlabel("Distance (x), t = {:.2f}(s)".format(t_ticks[t]))
plt.ylabel("u")
plt.pause(dt / 2)
plt.show()
if __name__ == '__main__':
main()
四、 存在的问题及改进
问题概述
- 在上面的代码中,如果
number_dx
和number_dt
设置不当,容易造成溢出。需要多次尝试设置这两个参数值。 - 每次修改边界条件时较为麻烦。
- 当
number_dt
很大时,生成的图像波动速读很慢,观感很差。 - 如果需要在波动的过程中附加其他的外力等条件,需要另外修改代码。
改进方案
- 自动的根据需求调整
number_dx
和number_dt
,以防止溢出。 - 将边界条件封装起来,传入所需要的第几类边界条件时可以自动更改。
- 使用
matplotlib
自带的动画生成函数,并封装帧数、延迟等的调整,使动画美观。 - 提供
callbacks
借口,在每次迭代后,把各点坐标的数组丢入函数中,用户可以设计callback
函数实现丰富的功能。
五、改进后的实现
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as amt
class OneDimensionalFluctuation:
def __init__(self,
*args,
length=1,
wave_velocity=1,
number_split=75,
end_time=2.495,
phi=lambda x: 0,
varphi=lambda x: 0,
callbacks=None,
left_boundary_situation=1,
right_boundary_situation=1,
):
self.wave_velocity = wave_velocity
# init basic variables
self.x_min, self.x_max, self.number_dx = 0, length, number_split # Discretization of x
self.t_min, self.t_max, self.number_dt = 0, end_time, int(number_split * end_time) # Discretization of y
self.x_ticks = np.linspace(self.x_min, self.x_max, self.number_dx)
self.t_ticks = np.linspace(self.t_min, self.t_max, self.number_dt)
self.dx = (self.x_max - self.x_min) / (self.number_dx - 1)
self.dt = (self.t_max - self.t_min) / (self.number_dt - 1)
self._time_step = 1
# init functions
self.phi = phi
self.varphi = varphi
self.callbacks = callbacks
self.left_boundary_situation = left_boundary_situation
self.right_boundary_situation = right_boundary_situation
# the fluctuation
self.u = np.zeros((self.number_dx, self.number_dt))
# init u(x, 0)
for i, x in enumerate(self.x_ticks):
self.u[i][0] = phi(x)
def _apply_boundary_situation(self):
# left
if self.left_boundary_situation == 1:
self.u[0][self._time_step] = 0
elif self.left_boundary_situation == 2:
self.u[0][self._time_step] = self.u[1][self._time_step]
else:
raise ValueError("No such left boundary situation {}".format(self.left_boundary_situation))
# right
if self.right_boundary_situation == 1:
self.u[-1][self._time_step] = 0
elif self.right_boundary_situation == 2:
self.u[-1][self._time_step] = self.u[-2][self._time_step]
else:
raise ValueError("No such right boundary situation {}".format(self.right_boundary_situation))
def _next_step(self):
if self.t_ticks[self._time_step] < self.t_max:
# calculate each position's next value
u_t = self.u[:, self._time_step]
end = len(u_t) - 1
ddu = list(u_t[0: end - 1] - 2 * u_t[1: end] + u_t[2: end + 1])
ddu = np.array([0] + ddu + [0])
self.u[:, self._time_step + 1] = (self.wave_velocity * self.dt / self.dx) ** 2 * ddu + 2 * self.u[:, self._time_step] - self.u[:, self._time_step - 1]
# apply the boundary situation
self._time_step += 1
self._apply_boundary_situation()
return self._time_step - 1
else:
# the iter is over
return -1
def run(self):
while self._time_step < self.number_dt:
if self.callbacks is not None:
self.callbacks(self.t_ticks[self._time_step], self.dx, self.u[:, self._time_step])
result = self._next_step()
if result == -1:
break
def draw(self):
# get the max u value
u_max = np.max(self.u)
for i in range(self.number_dt):
plt.clf()
plt.plot(self.x_ticks, self.u[:, i])
plt.axis((self.x_min - self.x_max / 10, self.x_max + self.x_max / 10, -1.2 * u_max, 1.2 * u_max))
plt.xlabel("Distance (x), t = {:.2f}(s)".format(self.t_ticks[i]))
plt.ylabel("u")
plt.pause(self.dt / 2)
plt.show()
def get_animation(self, name, fps=10, interval=None):
if interval is None:
interval = self.number_dt / (self.t_max + 1)
fig, ax = plt.subplots()
line, = ax.plot(self.x_ticks, self.u[:, 0])
ax.set_xlim(self.x_min - self.x_max / 10, self.x_max + self.x_max / 10)
ax.set_ylim(-1.1 * np.max(self.u), 1.1 * np.max(self.u))
def update(num):
line.set_ydata(self.u[:, num])
ax.set_xlabel("t={:.2f}(s)".format(self.t_ticks[num]))
return line,
ani = amt.FuncAnimation(fig, update, frames=self.number_dt, interval=interval)
ani.save('{}.gif'.format(name), fps=fps)
plt.show()
return ani
使用方法
1. 初始化参数列表
def __init__(self,
*args,
length=1,
wave_velocity=1,
number_split=75,
end_time=2.495,
phi=lambda x: 0,
varphi=lambda x: 0,
callbacks=None,
left_boundary_situation=1,
right_boundary_situation=1,
)
参数名 | 含义 | 类型 | 默认值 |
| 弦的长度 |
| 1 |
| 波速 |
| 1 |
| 节点的分割数(自动防溢出) |
| 75 |
| 波动终止时刻 |
| 2.495 |
| 位移初值条件 |
| 0 |
| 速度初值条件 (目前无效) |
| 0 |
| 回馈函数 |
| None |
| 左边界条件 |
| 1 |
| 右边界条件 |
| 1 |
- 使用时,边界条件只能传入
1
或2
,第三类边界条件还没有实现。varphi
是速度初值条件,目前还没有实现。
2. 一些使用例子
- 两端固定,初值条件为
if __name__ == '__main__':
u = OneDimensionalFluctuation(
length=1,
wave_velocity=1,
number_split=100,
end_time=2,
phi=lambda x: np.sin(3 * np.pi * x),
left_boundary_situation=1,
right_boundary_situation=1
)
u.run()
u.draw()
# u.get_animation("test")
- 左端固定,右端前2s附加外力
def add_right_power(current_time, dx, ut):
if current_time < 2:
ut[-1] = ut[-2] + 10 * dx * np.sin(4 * np.pi * current_time)
if __name__ == '__main__':
u = OneDimensionalFluctuation(
callbacks=add_right_power,
end_time=np.pi
)
u.run()
u.draw()
# u.get_animation("right_power", fps=20, interval=70)
- 右端为自由端,初值条件为
if __name__ == "__main__":
phi = lambda x: np.sin(6 * np.pi * x)
u = OneDimensionalFluctuation(phi=phi, end_time=3, length=1, right_boundary_situation=2, callbacks=add_right_power)
u.run()
u.draw()
# u.get_animation("secondary boundary", fps=20, interval=100)
- 左右两端为自由端,两端施加同频率等大外力
def add_two_power(current_time, dx, ut):
if current_time < 2:
ut[-1] = ut[-2] + 10 * dx * np.sin(4 * np.pi * current_time)
ut[0] = ut[1] + 10 * dx * np.sin(4 * np.pi * current_time)
if __name__ == '__main__':
u = OneDimensionalFluctuation(
length=1,
wave_velocity=1,
number_split=100,
end_time=2,
callbacks=add_two_power,
left_boundary_situation=1,
right_boundary_situation=1,
)
u.run()
u.draw()
# u.get_animation("two_power")
- 左右两端为自由端,两端施加不同频率外力
def add_two_power(current_time, dx, ut):
if current_time < 2:
ut[-1] = ut[-2] + 10 * dx * np.sin(3 * np.pi * current_time)
ut[0] = ut[1] + 10 * dx * np.sin(4 * np.pi * current_time)
if __name__ == '__main__':
u = OneDimensionalFluctuation(
length=1,
wave_velocity=1,
number_split=100,
end_time=1.9,
callbacks=add_two_power,
)
u.run()
u.draw()
附录
OneDimensionalFluctuation API
Attributes
wave_velocity
int
the speed of the wave.x_min
,x_max
,number_dx
float
,float
,int
the minimum, maximum of x and the segment number of x.- t_min, t_max, number_dt
float
,float
,int
the minimum, maximum of t and the segment number of t. - x_ticks
List[float]
the value of x in each segment. - t_ticks
List[float]
the value of t in each segment. - dx
float
the distance between each x segment - dt
float
the distance between each t segment. - _time_step
int[protected]
the recorder of time step. u
List[List[float]]
the array that save the value of the wave.u[i][j]
represent the value of the point at thei-th
segment inx_ticks
when the time isj-th
oft_ticks
.- phi
function(float) -> float
this function is the initial function ofu(x, 0)
, will give the value of x position, and need to return the value ofu(0, x)
. varphifunction(float) -> float
this function is the initial function of x, will give the value of x position, and need to return the value ofu(0, x)
.
this attribute is no use now- callbacks
function(float, float, List[float])
After each iteration, the callbacks function will be called, and each of the argument are:current_iteration_time
,dx
,ut
.ut
is a array of the wave at current iteration, and you can change this array to change the wave.
Example:
def add_right_power(current_time, dx, ut):
if current_time < 2:
ut[-1] = ut[-2] + 10 * dx * np.sin(4 * np.pi * current_time)
This function will add a power to the right point of the wave if the current time is less than 2(s).
- left_boundary_situation
int
only two option: 1 and 2.
1: first boundary situation.
2: second boundary situation. - right_boundary_situation
int
only two option: 1 and 2.
1: first boundary situation.
2: second boundary situation.
Method
- run
solve the problem, you need to run this function before draw and get animation. - draw
draw a gif by matplotlib.pyplot.plc(). - get_animation
show an animation by matplotlib.animation. And will return the Animation Object.
参考