避免 Runge-Kutta 4(5) 求解器的 ZeroDivisionError

huangapple go评论75阅读模式
英文:

Avoiding ZeroDivisionError for Runge-Kutta 4(5) solver

问题

我试图创建一个Runge-Kutta 4(5)求解器来解决微分方程y' = 2t,初始条件为y(0) = 0.5。到目前为止,我有这个:

def rk45(f, u0, t0, tf=100000, epsilon=0.00001, debug=False):
    # ...
    # 代码略
    # ...
    return np.array(u_array), np.array(t_array)

def test_dydx(y, t):
    return 2 * t

initial = 0.5
sol_rk45 = rk45(test_dydx, initial, t0=0, tf=2, debug=True)

当我运行时,得到这个:

t0 = 0, u0 = 0.5, h = 0.002
R = 5.551115123125783e-14
t = 0.002, u = 0.5000039999999999, h = 0.19463199004973464
R = 0.0

---------------------------------------------------------------------------
ZeroDivisionError

这是因为第4阶解u1和第5阶解u2非常接近,导致它们的差异基本为零,当我计算delta时,得到1/0,显然会导致ZeroDivisionError。

解决这个问题的一种方法是不计算delta,而是使用一个更简单版本的RK45。但是,这样做似乎对我来说毫无意义,因为它消除了RK45方法相对于RK4方法的自适应步长优势。

有没有办法保持自适应步长,同时避免遇到ZeroDivisionErrors?

英文:

I am trying to create a Runge-Kutta 4(5) solver to solve the differential equation y' = 2t with the initial condition y(0) = 0.5. This is what I have so far:

def rk45(f, u0, t0, tf=100000, epsilon=0.00001, debug=False):
    h = 0.002
    u = u0
    t = t0
    # solution array
    u_array = [u0]
    t_array = [t0]
    
    if debug:
        print(f"t0 = {t}, u0 = {u}, h = {h}")
    while t < tf:
        h = min(h, tf-t)
        k1 = h * f(u, t)
        k2 = h * f(u+k1/4, t+h/4)
        k3 = h * f(u+3*k1/32+9*k2/32, t+3*h/8)
        k4 = h * f(u+1932*k1/2197-7200*k2/2197+7296*k3/2197, t+12*h/13)
        k5 = h * f(u+439*k1/216-8*k2+3680*k3/513-845*k4/4104, t+h)
        k6 = h * f(u-8*k1/27+2*k2-3544*k3/2565+1859*k4/4104-11*k5/40, t+h/2)
        u1 = u + 25*k1/216+1408*k3/2565+2197*k4/4104-k5/5
        u2 = u + 16*k1/135+6656*k3/12825+28561*k4/56430-9*k5/50+2*k6/55
        R = abs(u1-u2) / h
        print(f"R = {R}")
        delta = 0.84*(epsilon/R) ** (1/4)
        
        if R <= epsilon:
            u_array.append(u1)
            t_array.append(t)
            u = u1
            t += h
        h = delta * h
        if debug:
            print(f"t = {t}, u = {u1}, h = {h}")
    return np.array(u_array), np.array(t_array)

def test_dydx(y, t):
    return 2 * t

initial = 0.5
sol_rk45 = rk45(test_dydx, initial, t0=0, tf=2, debug=True)

When I run it, I get this:

t0 = 0, u0 = 0.5, h = 0.002
R = 5.551115123125783e-14
t = 0.002, u = 0.5000039999999999, h = 0.19463199004973464
R = 0.0
---------------------------------------------------------------------------
ZeroDivisionError 

This is because the 4th order solution u1 and 5th order solution u2 are so close together that their difference is essentially zero, and when I calculate delta I get 1/0 which obviously results in a ZeroDivisionError.

One way to solve this is to not calculate delta and use a much simpler version of RK45:

def rk45(f, u0, t0, tf=100000, epsilon=0.00001, debug=False):
    h = 0.002
    u = u0
    t = t0
    # solution array
    u_array = [u0]
    t_array = [t0]
    
    if debug:
        print(f"t0 = {t}, u0 = {u}, h = {h}")
    while t < tf:
        h = min(h, tf-t)
        k1 = h * f(u, t)
        k2 = h * f(u+k1/4, t+h/4)
        k3 = h * f(u+3*k1/32+9*k2/32, t+3*h/8)
        k4 = h * f(u+1932*k1/2197-7200*k2/2197+7296*k3/2197, t+12*h/13)
        k5 = h * f(u+439*k1/216-8*k2+3680*k3/513-845*k4/4104, t+h)
        k6 = h * f(u-8*k1/27+2*k2-3544*k3/2565+1859*k4/4104-11*k5/40, t+h/2)
        u1 = u + 25*k1/216+1408*k3/2565+2197*k4/4104-k5/5
        u2 = u + 16*k1/135+6656*k3/12825+28561*k4/56430-9*k5/50+2*k6/55
        R = abs(u1-u2) / h
        
        if R <= epsilon:
            u_array.append(u1)
            t_array.append(t)
            u = u1
            t += h
        else:
            h = h / 2
        if debug:
            print(f"t = {t}, u = {u1}, h = {h}")
    return np.array(u_array), np.array(t_array)

But this, while it works, seems incredibly pointless to me because it negates the adaptive step size advantage of a RK45 method as compared to an RK4 method.

Is there any way to preserve an adaptive step size while not running into ZeroDivisionErrors?

答案1

得分: 1

第一变体

没有最佳的,“第一原则” 步长选择策略,因此要有一个合理的选择足够。所以作为第一个改进,使除零操作不可能,使用在 delta 计算中

(epsilon/(1e-2*epsilon + R))

这将在步长因子中设置一个上限。

第二变体

当误差大于允许值时,您有一种处理方法。

然而,缺少的是当误差远小于误差容限时的处理方法。因此在“接受”块的末尾插入一行

if 32*R < epsilon: h *= 2

这是很重要的,特别是当解向平衡点收敛时,您希望增加步长(直到显式方法的稳定边界)。

附言

为了减少灾难性取消的数量或严重程度,您可能希望使用 k 的线性组合以及步长系数的差异来计算误差。

英文:

First variant

There is no optimal, "first principles" step size selection strategy, so to have a reasonable one has to be sufficient. So as a first improvement make a division by zero impossible, use in the delta computation

(epsilon/(1e-2*epsilon + R))

This will put in a ceiling in the step factor.

Second variant

You have a treatment for when the error is larger than permitted.

However, what is missing is a treatment for when the error is drastically smaller than the error tolerance. So insert a line

if 32*R < epsilon: h *= 2

at the end of the "accept" block. This is for instance important when the solution converges towards an equilibrium as then you want to increase the step size (up to the stability border for explicit methods).

PS

To reduce the amount or severity of catastrophic cancellation you might want to compute the error using the linear combination of the k's with the differences of the step coefficients.

答案2

得分: 0

你可以尝试捕获错误,然后在触发时处理,或者使用小数以获得更高的精度。小数中的数字存储为元组,而浮点数是数字的近似值。在需要高精度时建议使用小数。

try/except

try:
z = x / y
except ZeroDivisionError:
z = 0

decimal

from decimal import *
getcontext().prec = 6
Decimal(1) / Decimal(7)

输出: Decimal('0.142857')

from decimal import *
getcontext().prec = 28
Decimal(1) / Decimal(7)

输出: Decimal('0.1428571428571428571428571429')

有关示例,请参见Error python : [ZeroDivisionError: division by zero]Is floating point arbitrary precision available?

和小数模块文档: decimal — Decimal fixed point and floating point arithmetic

英文:

You can try to catch the error and then handle it when triggered or use decimals for higher precision. The digits in decimals are stored as tuples vs floats being an approximation of a number. When high precision is important, it's recommended to use decimals.


try/except

try:
z = x / y
except ZeroDivisionError:
z = 0

decimal

from decimal import *
getcontext().prec = 6
Decimal(1) / Decimal(7)

Output: Decimal('0.142857')

from decimal import *
getcontext().prec = 28
Decimal(1) / Decimal(7)

Output: Decimal('0.1428571428571428571428571429')


For examples see Error python : [ZeroDivisionError: division by zero]
and Is floating point arbitrary precision available?

And decimal module docs: decimal — Decimal fixed point and floating point arithmetic

huangapple
  • 本文由 发表于 2023年4月19日 18:15:22
  • 转载请务必保留本文链接:https://go.coder-hub.com/76053313.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定