数值积分漫谈:如何让用Python求积分?

定积分代表函数图像下方与 x 轴围成的区域面积(考虑正负)。比如,计算   就是在求函数  从  到  这段区间下的面积。

虽然牛顿-莱布尼茨公式告诉我们可以通过找到  的原函数 ,然后计算  来得到定积分的值,但在实际应用中,寻找原函数或者计算原函数的值可能非常麻烦甚至难以实现。这时,就可以借助数值积分的方法。

数值积分的基本思想是:不直接去找原函数,而是想办法用一些简单的数学操作,让计算机帮我们一步一步地“近似”算出这个面积。这有点像我们用方格纸估算不规则图形面积一样,只不过现在我们用更高效、更精确的办法。

下面,我们就来介绍三种常用的数值积分方法:梯形法 (Trapezoidal Rule)辛普森法 (Simpson's Rule) 和  自适应辛普森法 (Adaptive Simpson's Rule) 。

方法一:梯形法——把曲边变直边

这是最直观、最容易理解的一种方法。

  • 想法: 我们把要求面积的区间  切成很多小段,每一段都很窄。然后,在每一个小段上,我们不再看曲线本身,而是用一条直线连接这段曲线两端的点。这样,每一小段下方就变成了一个梯形
  • 计算:

  • 把区间  分成  个相等的小段,每个小段的宽度是 。

  • 对于第  个小段 ,它下方面积就近似等于一个梯形的面积:

面积 3. 把所有  个小梯形加起来得到整个区间  下面积的一个近似值:

  • 特点: 原理简单,容易编程实现。但把曲线都用直线代替了,所以精度一般。增加分割的小段数 (也就是减小 )可提高精度,但要更多计算量。

Image

方法二:辛普森法——用抛物线拟合曲线

梯形法是用直线逼近曲线,那我们能不能用更“弯”的线,让近似更准确一些呢?辛普森法就是这样一种方法,它用抛物线来近似一小段曲线。

  • 想法: 不是用直线连接两个点,而是用一条抛物线去尽可能地“贴合”三个点:一个小段的两个端点和中间的那个点。
  • 计算:

  • 同样先把区间  分成  个相等的小段(这里  必须是偶数),宽度为 。

  • 但是,我们现在是每次考察连续的两个小段,也就是一个宽度为  的大段 。
  • 找到通过这三个点 , ,  的唯一一条抛物线。
  • 计算这条抛物线与 x 轴在  区间内围成的面积,有一个专门的公式:

面积 5. 对所有这样的不重叠的大段都进行计算,并将结果相加。

  • 特点: 因为用了二次曲线(抛物线)去拟合,所以对于光滑的函数,它的精度通常比梯形法高得多。这意味着达到相同的精度,辛普森法可能只需要更少的分割份数 ,计算效率更高。

Image

方法三:自适应辛普森法——聪明地选择在哪里多花功夫

梯形法和辛普森法都需要我们事先规定好要把区间分成多少份 ()。但函数图像有时变化平缓,有时变化剧烈。如果全程都用同样细的划分,可能会在平缓的地方做了过多不必要的计算;如果划分得太粗,则在弯曲厉害的地方误差就会很大。

自适应辛普森法是一种更“智能”的策略。

  • 想法: “哪里需要更高的精度,就在哪里进行更细致的划分”。它会自动判断哪些区域的函数变化快(需要细分),哪些区域变化慢(可以粗略处理)。
  • 工作方式 (简化版):

  • 先在整个大区间  上用一次辛普森公式算一个积分值(记作 A)。

  • 再将区间  从中间  分开,分别在  和  上各用一次辛普森公式算积分,然后把两个结果加起来(记作 B)。
  • 比较 A 和 B。如果它们非常接近(差异小于我们预设的要求),那就认为用一个大抛物线来近似整个区间已经足够好了,接受 B 作为结果。
  • 如果 A 和 B 相差较大,说明函数在这个大区间内变化太快,一个抛物线拟合不准。于是,就放弃 B,转而去分别检查  和  这两个小区间。对每个小区间,重复上述 1-4 的步骤。
  • 这个过程会递归地进行下去,直到所有子区间的“粗糙近似”和“细分近似”都足够接近为止。

  • 特点: 它能在函数变化剧烈的地方自动加密网格(增加计算点),在变化平缓的地方减少计算点。这样既能保证整体的计算精度,又能有效地节省总的计算量。这是一种非常实用和高效的数值积分技术。

Image

自适应辛普森法的基本思路就是“在平坦处粗略,在陡峭处精细”,在大多数情况下都能高效地给出高精度的结果。然而,当函数呈现出极其复杂、高频的周期性变化时,其可靠性会受到严峻的挑战。问题的核心在于自适应辛普森法的误差判据,是通过比较一个区间上的辛普森积分结果与将其二等分后的两个子区间积分结果之和来估计误差。对于高频振荡函数,即使在一个极小的子区间内,无休止地触发更深层次的递归细分。最终,算法可能因递归深度超出系统限制而堆栈溢出,或因计算步数达到预设上限而被迫中止,甚至陷入一个近乎死循环的计算泥潭,无法完成计算。

自适应辛普森法并非在所有情况下都能保证“完成”计算并给出一个可靠的结果。这个困境深刻地揭示了一个计算科学领域的普遍真理——世界上没有完美的算法。选择数值方法不仅是选择一个公式,更是一场对问题本质的理解和权衡。在追求精度的道路上,我们必须承认工具的局限性,并准备好为那些“不守规矩”的函数,寻找更专门的钥匙。

现实世界没有标准答案,那些人为制定的有标准答案的远没有现实精彩。


Python 动画演示

为了让大家更直观地看到这三种方法是如何一步步逼近真实面积的,我们编写了一段 Python 代码。这段代码不仅能计算积分,还能生成生动的动画 GIF 文件,展示每一步的逼近过程。

import numpy as np
import matplotlib.pyplot as plt
from scipy import integrate
from matplotlib.animation import FuncAnimation

plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei', 'WenQuanYi Micro Hei']
plt.rcParams['axes.unicode_minus'] = False

def f(x):
    return np.pi * np.cos(np.exp(x)) / (x ** np.e)

a, b = 1.0, 4.0
true_value, _ = integrate.quad(f, a, b)

def animate_trapezoidal(func, a, b, n=20, filename='trapezoidal.gif'):
    x = np.linspace(a, b, n + 1)
    y = func(x)
    h = (b - a) / n

    fig, ax = plt.subplots(figsize=(10, 5))
    xs = np.linspace(a, b, 500)
    ax.plot(xs, func(xs), 'k-', lw=2, label=r'$f(x)$')

    def update(i):
        ax.cla()
        ax.plot(xs, func(xs), 'k-', lw=2, label=r'$f(x)$')
        for j in range(0, i+1):
            xi = [x[j], x[j+1], x[j+1], x[j]]
            yi = [0, 0, y[j+1], y[j]]
            ax.fill(xi, yi, color='skyblue', alpha=0.6, edgecolor='blue', linewidth=0.8)
        approx = (h / 2) * (y[0] + 2 * np.sum(y[1:i+1]) + y[i+1])
        error = abs(approx - true_value)
        ax.set_title(f'梯形法 动画演示\n第{i+1}次迭代, 积分 ≈ {approx:.6f}, 误差 = {error:.2e}', fontsize=14)
        ax.set_xlabel('x')
        ax.set_ylabel('f(x)')
        ax.legend()
        ax.grid(True, linestyle='--', alpha=0.5)

    ani = FuncAnimation(fig, update, frames=n, interval=1000, repeat=False)
    ani.save(filename, writer='pillow')
    plt.show()

def animate_simpson(func, a, b, n=20, filename='simpson.gif'):
    if n % 2 == 1:
        n += 1
    x = np.linspace(a, b, n + 1)
    y = func(x)
    h = (b - a) / n

    fig, ax = plt.subplots(figsize=(10, 5))
    xs = np.linspace(a, b, 500)
    ax.plot(xs, func(xs), 'k-', lw=2, label=r'$f(x)$')

    def update(i):
        ax.cla()
        ax.plot(xs, func(xs), 'k-', lw=2, label=r'$f(x)$')
        for j in range(0, min((i+1)*2, n), 2):
            x0, x1, x2 = x[j], x[j+1], x[j+2]
            y0, y1, y2 = y[j], y[j+1], y[j+2]

            xx = np.linspace(x0, x2, 50)
            yy = ((xx-x1)*(xx-x2))/((x0-x1)*(x0-x2))*y0 + ((xx-x0)*(xx-x2))/((x1-x0)*(x1-x2))*y1 + ((xx-x0)*(xx-x1))/((x2-x0)*(x2-x1))*y2
            ax.fill_between(xx, 0, yy, color='lightcoral', alpha=0.6, edgecolor='red', linewidth=0.8)
        ax.set_title(f'辛普森法 动画演示\n第{i+1}次迭代', fontsize=14)
        ax.set_xlabel('x')
        ax.set_ylabel('f(x)')
        ax.legend()
        ax.grid(True, linestyle='--', alpha=0.5)

    ani = FuncAnimation(fig, update, frames=n//2, interval=1000, repeat=False)
    ani.save(filename, writer='pillow')
    plt.show()

def simpson_basic(func, a, b):
    c = (a + b) / 2.0
    return (b - a) / 6.0 * (func(a) + 4.0 * func(c) + func(b))

def adaptive_simpson_with_log(func, a, b, tol, max_depth=15, log=None, depth=0):
    if log isNone:
        log = []
    if depth > max_depth:
        log.append((a, b, True, depth))
        return simpson_basic(func, a, b)
    c = (a + b) / 2.0
    whole = simpson_basic(func, a, b)
    left = simpson_basic(func, a, c)
    right = simpson_basic(func, c, b)
    error = abs(left + right - whole)
    if error < 15 * tol:
        log.append((a, b, True, depth))
        return left + right
    else:
        log.append((a, b, False, depth))
        left_val = adaptive_simpson_with_log(func, a, c, tol/2, max_depth, log, depth+1)
        right_val = adaptive_simpson_with_log(func, c, b, tol/2, max_depth, log, depth+1)
        return left_val + right_val

#  修改后的自适应辛普森动画函数使用抛物线填充不是直线
def plot_adaptive_simpson(func, a, b, tol=1e-5, filename='adaptive_simpson.gif'):
    log = []
    approx = adaptive_simpson_with_log(func, a, b, tol, max_depth=12, log=log)
    error = abs(approx - true_value)

    fig, ax = plt.subplots(figsize=(10, 5))
    xs = np.linspace(a, b, 500)
    ax.plot(xs, func(xs), 'k-', lw=2, label=r'$f(x)$')

    cmap = plt.get_cmap('viridis')
    max_depth = max(d for (_, _, _, d) in log)

    def update(i):
        ax.cla()
        ax.plot(xs, func(xs), 'k-', lw=2, label=r'$f(x)$')
        for j in range(i + 1):
            a_, b_, accepted, depth = log[j]
            if accepted:
                # 构造三点左端点中点右端点
                x0, x2 = a_, b_
                x1 = (x0 + x2) / 2.0
                y0, y1, y2 = func(x0), func(x1), func(x2)

                #  [x0, x2] 上生成密集点用于绘制抛物线
                xx = np.linspace(x0, x2, 50)
                # 使用拉格朗日二次插值与辛普森一致
                yy = ((xx - x1) * (xx - x2)) / ((x0 - x1) * (x0 - x2)) * y0 + \
                     ((xx - x0) * (xx - x2)) / ((x1 - x0) * (x1 - x2)) * y1 + \
                     ((xx - x0) * (xx - x1)) / ((x2 - x0) * (x2 - x1)) * y2

                color = cmap(depth / max(max_depth, 1))
                ax.fill_between(xx, 0, yy, color=color, alpha=0.6, edgecolor='purple', linewidth=0.7)
        ax.set_title(f'自适应辛普森法 动画演示\n第{i+1}次迭代', fontsize=14)
        ax.set_xlabel('x')
        ax.set_ylabel('f(x)')
        ax.legend()
        ax.grid(True, linestyle='--', alpha=0.5)

    ani = FuncAnimation(fig, update, frames=len(log), interval=1000, repeat=False)
    ani.save(filename, writer='pillow')
    plt.show()

if __name__ == "__main__":
    print("参考真值(SciPy quad):", f"{true_value:.10f}")

    # Generate animations and save them as GIF files.
    animate_trapezoidal(f, a, b, n=12, filename='trapezoidal.gif')
    animate_simpson(f, a, b, n=12, filename='simpson.gif')
    plot_adaptive_simpson(f, a, b, tol=1e-4, filename='adaptive_simpson.gif')

预览时标签不可点

Close

更多

Name cleared

微信扫一扫赞赏作者

Like the AuthorOther Amount

赞赏后展示我的头像

作品

暂无作品

Like the Author

Other Amount

¥

最低赞赏 ¥0

OK

Back

Other Amount

更多

赞赏金额

¥

最低赞赏 ¥0

1

2

3

4

5

6

7

8

9

0

.

Python语言程序设计 · 目录

Python语言程序设计

上一篇打造绿色便携 Python:初步理解嵌入式 Python 与路径机制下一篇面向对象编程入门:用武侠角色理解 Python 中的“类”

Close

更多

搜索「」网络结果

Close

调整当前正文文字大小

更多

100%