integrate.ode设置t0值超出数据范围
我想解决这个微分方程 dy/dt = -2y + data(t),在 t=0 到 t=3 之间,初始条件是 y(t=0)=1。
我写了以下代码:
import numpy as np
from scipy.integrate import odeint
from scipy.interpolate import interp1d
t = np.linspace(0, 3, 4)
data = [1, 2, 3, 4]
linear_interpolation = interp1d(t, data)
def func(y, t0):
print 't0', t0
return -2*y + linear_interpolation(t0)
soln = odeint(func, 1, t)
当我运行这段代码时,出现了几个错误,像这样:
ValueError: x_new 中的一个值超出了插值范围。
odepack.error: 调用名为 func 的 Python 函数时发生错误
我的插值范围是 0.0 到 3.0。打印 func 中的 t0
的值时,我发现 t0
有时确实超出了我的插值范围,比如:3.07634612585, 3.0203768998, 3.00638459329,等等。这就是为什么 linear_interpolation(t0)
会引发 ValueError
异常的原因。
我有几个问题:
integrate.ode
是怎么让t0
变化的?为什么它会让t0
超过我插值范围的上限(3.0)?尽管出现了这些错误,
integrate.ode
还是返回了一个看起来正确的值的数组。那么,我应该只是捕获并忽略这些错误吗?无论微分方程、t
范围和初始条件是什么,我都应该忽略它们吗?如果我不应该忽略这些错误,避免它们的最佳方法是什么?我有两个建议:
在
interp1d
中,我可以设置bounds_error=False
和fill_value=data[-1]
,因为超出我插值范围的t0
看起来接近t[-1]
:linear_interpolation = interp1d(t, data, bounds_error=False, fill_value=data[-1])
但首先我想确保无论其他
func
和其他data
是什么,t0
都会始终接近t[-1]
。例如,如果integrate.ode
选择了一个低于我插值范围的t0
,那么填充值仍然是data[-1]
,这就不正确了。也许了解integrate.ode
是如何让t0
变化的会帮助我确认这一点(见我的第一个问题)。在
func
中,我可以把linear_interpolation
的调用放在一个 try/except 块里,当我捕获到ValueError
时,我重新调用linear_interpolation
,但用截断后的t0
:def func(y, t0): try: interpolated_value = linear_interpolation(t0) except ValueError: interpolated_value = linear_interpolation(int(t0)) # truncate t0 return -2*y + interpolated_value
至少这个解决方案允许
linear_interpolation
在integrate.ode
让t0
>= 4.0 或t0
<= -1.0 时仍然引发异常。这样我就能被提醒到不一致的行为。但这并不是很清晰,而且截断现在看起来有点随意。
也许我只是对这些错误想得太多了。请告诉我你的看法。
1 个回答
在使用odeint
这个求解器时,看到它在你请求的最后时间之后评估你的函数是很正常的。大多数常微分方程(ODE)求解器都是这样工作的——它们会根据内部的错误控制算法,选择一些时间步长,然后用自己的方法来计算用户请求的时间点的解。有些求解器(比如Sundials库中的CVODE求解器)允许你设定一个时间的上限,超过这个时间求解器就不再评估你的方程,但odeint
没有这个选项。
如果你不介意从scipy.integrate.odeint
切换到scipy.integrate.ode
,那么"dopri5"
和"dop853"
这两个求解器就不会在请求的时间之后评估你的函数。不过有两个注意事项:
ode
求解器在定义微分方程的参数顺序上有不同的约定。在ode
求解器中,t
是第一个参数。(是的,我知道,这让人有点不爽...)"dopri5"
和"dop853"
求解器适用于非刚性系统。如果你的问题是刚性的,它们仍然会给出正确的答案,但会比刚性求解器做更多的计算。
这里有一个脚本,展示了如何解决你的例子。为了强调参数的变化,我把func
改名为rhs
。
import numpy as np
from scipy.integrate import ode
from scipy.interpolate import interp1d
t = np.linspace(0, 3, 4)
data = [1, 2, 3, 4]
linear_interpolation = interp1d(t, data)
def rhs(t, y):
"""The "right-hand side" of the differential equation."""
#print 't', t
return -2*y + linear_interpolation(t)
# Initial condition
y0 = 1
solver = ode(rhs).set_integrator("dop853")
solver.set_initial_value(y0)
k = 0
soln = [y0]
while solver.successful() and solver.t < t[-1]:
k += 1
solver.integrate(t[k])
soln.append(solver.y)
# Convert the list to a numpy array.
soln = np.array(soln)
接下来这部分内容会讲讲如何继续使用odeint
。
如果你只对线性插值感兴趣,可以简单地用数据的最后两个点进行线性延伸。扩展data
数组的一个简单方法是把值2*data[-1] - data[-2]
添加到数组的末尾,t
数组也可以这样做。如果t
中的最后一个时间步长很小,这样的延伸可能不够长,无法避免问题,因此在下面我使用了更通用的延伸方法。
示例:
import numpy as np
from scipy.integrate import odeint
from scipy.interpolate import interp1d
t = np.linspace(0, 3, 4)
data = [1, 2, 3, 4]
# Slope of the last segment.
m = (data[-1] - data[-2]) / (t[-1] - t[-2])
# Amount of time by which to extend the interpolation.
dt = 3.0
# Extended final time.
t_ext = t[-1] + dt
# Extended final data value.
data_ext = data[-1] + m*dt
# Extended arrays.
extended_t = np.append(t, t_ext)
extended_data = np.append(data, data_ext)
linear_interpolation = interp1d(extended_t, extended_data)
def func(y, t0):
print 't0', t0
return -2*y + linear_interpolation(t0)
soln = odeint(func, 1, t)
如果仅仅用最后两个数据点来线性延伸插值线太粗糙,那么你就需要使用其他方法来在给odeint
的最后t
值之外进行外推。
另一种选择是将最后的t
值作为参数传递给func
,并在func
中明确处理大于这个值的t
值。可以像这样做,其中extrapolation
是你需要自己搞清楚的内容:
def func(y, t0, tmax):
if t0 > tmax:
f = -2*y + extrapolation(t0)
else:
f = -2*y + linear_interpolation(t0)
return f
soln = odeint(func, 1, t, args=(t[-1],))