进阶:符号微积分
前提知识
- 微积分基础:导数定义、基本求导公式、Taylor 级数概念
- 表达式树与递归数据结构(见 多项式代数)
- oCAS 表达式系统(
Atom、AtomNode、AtomArena)
基础概念
表达式树上的符号微积分
在符号计算中,函数 $f(x)$ 被表示为一棵表达式树(expression tree)。树的叶节点是常数或变量,内部节点是运算符(Add、Mul、Pow)或函数调用(sin、exp、log 等)。
例如,$f(x) = x^2 + \sin(x)$ 的表达式树为:
Add
├── Pow
│ ├── Var("x")
│ └── Num(2)
└── Fun("sin", [Var("x")])
符号微分的核心思想是模式匹配(pattern matching):对表达式树的每个节点,根据其类型应用对应的微分规则,递归地构造导数表达式树。
基本求导规则
给定可微函数 $f(x)$ 和 $g(x)$:
| 规则 | 公式 |
|---|---|
| 常数 | $\frac{d}{dx}[c] = 0$ |
| 恒等 | $\frac{d}{dx}[x] = 1$ |
| 和 | $\frac{d}{dx}[f + g] = f' + g'$ |
| 积 | $\frac{d}{dx}[f \cdot g] = f' \cdot g + f \cdot g'$ |
| 链式 | $\frac{d}{dx}[f(g(x))] = f'(g(x)) \cdot g'(x)$ |
| 幂(指数为常数 $n$) | $\frac{d}{dx}[f^n] = n \cdot f^{n-1} \cdot f'$ |
| 指数(底为常数 $a$) | $\frac{d}{dx}[a^g] = a^g \cdot \ln a \cdot g'$ |
| 一般幂 | $\frac{d}{dx}[f^g] = f^g \cdot (\ln f \cdot g' + g \cdot f'/f)$ |
初等函数导数表
| $f(u)$ | $f'(u)$ |
|---|---|
| $\sin u$ | $\cos u$ |
| $\cos u$ | $-\sin u$ |
| $\exp u$ | $\exp u$ |
| $\ln u$ | $u^{-1}$ |
| $\sqrt{u}$ | $(2\sqrt{u})^{-1}$ |
| $\tan u$ | $\sec^2 u$ |
| $\sec u$ | $\sec u \cdot \tan u$ |
| $\arctan u$ | $(1 + u^2)^{-1}$ |
注意:上表给出的是 $f'(u)$(关于 $u$ 的导数),实际 $\frac{d}{dx}[f(u(x))] = f'(u) \cdot u'(x)$,即还需乘以链式法则因子 $u'(x)$。
Taylor 级数
设 $f$ 在点 $a$ 的某邻域内无穷可微,则 $f$ 在 $a$ 处的 Taylor 级数为:
$$ f(x) = \sum_{n=0}^{\infty} \frac{f^{(n)}(a)}{n!} (x - a)^n $$
其中 $f^{(n)}(a)$ 是 $f$ 在 $a$ 处的 $n$ 阶导数。截断到 $N$ 阶得到 Taylor 多项式:
$$ T_N(x) = \sum_{n=0}^{N} \frac{f^{(n)}(a)}{n!} (x - a)^n $$
常见展开(在 $a = 0$ 处,即 Maclaurin 级数):
$$ e^x = 1 + x + \frac{x^2}{2!} + \frac{x^3}{3!} + \cdots $$
$$ \sin x = x - \frac{x^3}{3!} + \frac{x^5}{5!} - \cdots $$
$$ \cos x = 1 - \frac{x^2}{2!} + \frac{x^4}{4!} - \cdots $$
$$ \ln(1+x) = x - \frac{x^2}{2} + \frac{x^3}{3} - \cdots \quad (|x| < 1) $$
核心理论
表达式树上的递归微分
符号微分的核心算法是对表达式树进行结构递归(structural recursion)。对每个节点类型,应用对应的微分规则,递归地对子表达式求导。
算法 diff(expr, var):
- 常数
Num(c):返回Num(0)。 - 变量
Var(v):若 $v = \text{var}$,返回Num(1);否则返回Num(0)。 - 和
Add(a₁, a₂, …, aₙ):逐项求导,返回Add(diff(a₁), diff(a₂), …, diff(aₙ))。 - 积
Mul(a₁, a₂, …, aₙ):应用广义乘积法则。对 $n$ 个因子的乘积 $a_1 a_2 \cdots a_n$:
$$ \frac{d}{dx}\left[\prod_{i=1}^n a_i\right] = \sum_{i=1}^n \left(\frac{d a_i}{dx} \cdot \prod_{j \neq i} a_j\right) $$
即对每个位置 $i$,将第 $i$ 个因子替换为其导数,其余因子保持不变,然后将 $n$ 个结果相加。
-
幂
Pow(base, exp):分三种情况——- 指数为常数 $n$:$\frac{d}{dx}[b^n] = n \cdot b^{n-1} \cdot b'$
- 底为常数 $a$:$\frac{d}{dx}[a^u] = a^u \cdot \ln a \cdot u'$
- 一般情形(广义幂法则):$\frac{d}{dx}[b^e] = b^e \cdot (\ln b \cdot e' + e \cdot b'/b)$
-
函数调用
Fun(name, [u]):查表得到 $f'(u)$,再乘以 $u'$(链式法则)。 -
未知函数:返回未求值形式
Derivative(f(x), x)。
关键性质:由于 oCAS 的表达式使用 hash-consing(结构共享),递归微分产生的中间表达式自动共享相同子树。微分完成后,通过重写引擎化简(例如 $0 \cdot f \to 0$,$1 \cdot f \to f$),消除冗余项。
Taylor 展开的递推算法
Taylor 展开的直接实现可以利用逐次微分:
算法 taylor(expr, var, point, order):
- 令
current = expr,sum = 0。 - 对 $n = 0, 1, \dots, \text{order}$:
- 计算 $f^{(n)}(\text{point})$:将
current中的var替换为point(即求值)。 - 计算系数 $c_n = f^{(n)}(\text{point}) / n!$。
- 若 $n = 0$,项为 $c_0$;否则项为 $c_n \cdot (x - \text{point})^n$。
- 累加:$\text{sum} \mathrel{+}= \text{term}$。
- 若 $n < \text{order}$,更新
current = diff(current, var)。
- 计算 $f^{(n)}(\text{point})$:将
- 化简并返回
sum。
阶乘的处理:$1/n!$ 以幂的形式 $n!^{-1}$ 表示,避免引入浮点数。mul_by_factorial_inverse 函数计算 $n!$(使用 i64 乘法,对 $n \leq 20$ 安全),然后构造 $c \cdot (n!)^{-1}$。
复杂度:循环 $n = 0, \dots, N$ 中每一步执行一次替换(求值),但仅在 $n < N$ 时执行一次符号微分。符号微分本身是 $O(|\text{expr}|)$ 的($|\text{expr}|$ 为表达式树大小),但化简可能增加节点数。总体而言,$N$ 阶 Taylor 展开需要 $N$ 次符号微分和 $N+1$ 次替换。
替换的语义
substitute(expr, var, replacement) 将表达式中所有自由出现的变量 var 替换为 replacement。
实现策略:oCAS 使用自底向上变换(bottom-up transform)——通过 transform 函数遍历表达式树,对每个叶节点检查是否为要替换的变量。若是,返回 replacement;否则保持不变。
深拷贝 vs 引用:
- oCAS 的
Atom是 arena 上的 Copy 句柄(指针),而非拥有所有权的值。transform在 arena 上分配新节点来构造结果,但未修改的子树直接复用原句柄(零拷贝)。 - 这意味着
substitute的语义是纯函数式的:原表达式不变,结果是新分配的表达式树,其中被替换的路径指向新节点,未受影响的子树与原树共享。 - 与"破坏性修改"不同,这保证了引用透明性:对同一表达式多次替换不会互相干扰。
替换的局限:substitute 是语法替换而非语义替换。它替换的是字面上的变量名,而非"值"。例如,对 $\int_0^x f(t), dt$ 中的 $t$ 做替换 $t \to y$ 会错误地改变积分变量——这类语义替换需要更精细的绑定分析。
在 oCAS 中的实现
diff:符号微分
文件:ocas-calc/src/derivative.rs
#![allow(unused)] fn main() { pub fn diff<'a>(ctx: &'a AtomArena<'a>, expr: Atom<'a>, var: Symbol) -> Atom<'a> }
实现结构:
diff 是入口函数,它调用内部的 diff_raw 执行实际递归,然后用 simplify 化简结果,最后用 normalize 归一化。
#![allow(unused)] fn main() { pub fn diff<'a>(ctx: &'a AtomArena<'a>, expr: Atom<'a>, var: Symbol) -> Atom<'a> { let rules = calculus_rules(ctx, &crate::pattern_alloc::VecAlloc); let raw = diff_raw(ctx, expr, var); let simplified = simplify(ctx, raw, &rules, 20); normalize(ctx, simplified) } }
diff_raw 对 AtomNode 的每个变体进行模式匹配:
Num(_):常数,导数为 0。Var(v):若v == var则导数为 1,否则为 0。Add(args):逐项调用diff_raw,结果收集到新的Add节点。Mul(args):广义乘积法则——对 $n$ 个因子,生成 $n$ 个乘积项(每项将一个因子替换为其导数),然后相加。Pow(base, exp):三种情况(指数常数 / 底常数 / 一般),见上文。Fun(name, args):委托给diff_function。
diff_function 维护一张硬编码导数表,覆盖 sin、cos、exp、log、sqrt、tan、sec、atan 八个函数。对每个函数 $f(u)$,表中存储 $f'(u)$(关于 $u$ 的导数),然后乘以链式法则因子 $\frac{du}{dx}$。
对于表中没有的函数,返回未求值形式:
#![allow(unused)] fn main() { ctx.fun("Derivative", &[ctx.fun(name_str, args), ctx.var(var.as_str())]) }
即 Derivative(f(x), x),表示无法自动求导的导数。
化简管线:diff 产生的原始导数表达式可能包含冗余(如 0 * sin(x)、1 * cos(x))。simplify 使用 calculus_rules(微积分专用规则 + 默认化简规则)进行最多 20 轮不动点化简,然后 normalize 将结果归一化为规范形式。
taylor:Taylor 展开
文件:ocas-calc/src/series.rs
#![allow(unused)] fn main() { pub fn taylor<'a>( ctx: &'a AtomArena<'a>, expr: Atom<'a>, var: Symbol, point: Atom<'a>, order: usize, ) -> Atom<'a> }
实现细节:
- 预计算
(x - point)作为公共子表达式x_minus_p。 - 循环 $n = 0, \dots, \text{order}$:
substitute(ctx, current, var, point)求 $f^{(n)}(a)$。mul_by_factorial_inverse计算 $f^{(n)}(a) / n!$。- 构造项 $c_n \cdot (x - a)^n$($n = 0$ 时省略幂因子)。
- 累加到
sum。 - 若 $n < \text{order}$,
current = diff(ctx, current, var)计算下一阶导数。
- 化简 + 归一化。
mul_by_factorial_inverse:
#![allow(unused)] fn main() { fn mul_by_factorial_inverse<'a>(ctx: &'a AtomArena<'a>, expr: Atom<'a>, n: usize) -> Atom<'a> { if n == 0 { return expr; } let mut fact: i64 = 1; for i in 2..=n { fact = fact.checked_mul(i as i64).expect("factorial fits in i64"); } ctx.mul(&[expr, ctx.pow(ctx.num(fact), ctx.num(-1))]) } }
将 $1/n!$ 表示为 $(n!)^{-1}$,即 Pow(Num(n!), Num(-1)),保持精确有理数表示。checked_mul 确保 $n \leq 20$($20! = 2,432,902,008,176,640,000 < 2^{63}$)。
示例:
#![allow(unused)] fn main() { // e^x 在 x=0 处展开到 3 阶 let x = ctx.var("x"); let expr = ctx.fun("exp", &[x]); let result = taylor(&ctx, expr, Symbol::new("x"), ctx.num(0), 3); // result = "1 + x + ((2^-1)*(x^2)) + ((6^-1)*(x^3))" // 即 1 + x + x²/2 + x³/6 // sin(x) 在 x=0 处展开到 5 阶 let sin_x = ctx.fun("sin", &[x]); let result = taylor(&ctx, sin_x, Symbol::new("x"), ctx.num(0), 5); // result = "x + (-1*(6^-1)*(x^3)) + ((120^-1)*(x^5))" // 即 x - x³/6 + x⁵/120(偶数阶系数为零,自动消失) }
substitute:变量替换
文件:ocas-calc/src/series.rs
#![allow(unused)] fn main() { pub fn substitute<'a>( ctx: &'a AtomArena<'a>, expr: Atom<'a>, var: Symbol, replacement: Atom<'a>, ) -> Atom<'a> }
实现:
#![allow(unused)] fn main() { pub fn substitute<'a>( ctx: &'a AtomArena<'a>, expr: Atom<'a>, var: Symbol, replacement: Atom<'a>, ) -> Atom<'a> { transform(ctx, expr, |a| match a.node() { AtomNode::Var(v) if *v == var => Some(replacement), _ => None, }) } }
使用 transform(自底向上变换)遍历表达式树。闭包对每个节点检查:若为 Var(v) 且 v == var,返回 Some(replacement)(替换);否则返回 None(保持不变)。
关键性质:
- 纯函数式:不修改原表达式,返回新树。未修改的子树通过
Atom的 Copy 语义共享。 - 深替换:替换所有自由出现,包括嵌套在函数参数中的(如
sin(x)中的x)。 - 无捕获检查:不检查替换后是否产生变量捕获——这是语法层面的替换。
integrate:符号积分
文件:ocas-calc/src/integral/mod.rs
#![allow(unused)] fn main() { pub fn integrate<'a>(ctx: &'a AtomArena<'a>, expr: Atom<'a>, var: Symbol) -> Atom<'a> }
oCAS 的积分器采用分层管线架构(详见 符号积分算法 和 Risch 积分算法):
- 直接查表层(
integrate_raw):常数、变量、和式以及简单乘积/幂/函数的直接积分规则。 - 有理函数层(
rational.rs):部分分式分解 + Hermite 约化 + 对数部分(对数导数恒等式、配方、Rothstein–Trager)。 - Risch 算法层(
risch.rs):微分域塔 $\mathbb{Q}(x, t_1, \dots, t_n)$ 上的结构定理。 - 三角层(
trig.rs):Euler 公式重写为复指数($t = e^{ix}$)→ 对重写结果再次尝试 Risch →realify回转实数形式。 - 特殊函数层(
special.rs):erf、erfi、Ei、Si、Ci、Shi、Chi、Fresnel 等特殊函数表。 - 启发式层(
heuristic.rs):分部积分(LIATE 排序)、三角替换($\sqrt{a^2 - x^2} \to a\sin\theta$)、Weierstrass $t = \tan(x/2)$ 有理化、Euler 替换。 - 回退:返回未求值形式
Integral(expr, var)。
integrate_with_fuel 变体接受 Fuel 预算,防止化简阶段无限循环。积分遍历本身受 MAX_DEPTH = 8 的内部递归深度限制(Risch 层另有 MAX_RISCH_DEPTH = 16):
#![allow(unused)] fn main() { pub fn integrate_with_fuel<'a>( ctx: &'a AtomArena<'a>, expr: Atom<'a>, var: Symbol, fuel: &Fuel, ) -> Result<Atom<'a>> }
进阶话题
微分与化简的交互
diff 的正确性不仅取决于微分规则本身,还取决于化简质量。例如:
- $\frac{d}{dx}[x^2 + 3x] = 2x + 3$——需要化简 $1 \cdot 3$ 为 $3$。
- $\frac{d}{dx}[e^{\ln x}] = e^{\ln x} \cdot \frac{1}{x} = 1$——需要化简 $e^{\ln x} \to x$。
oCAS 通过 calculus_rules 和默认规则的多轮化简处理这类情况。但某些恒等式(如 $\sin^2 x + \cos^2 x = 1$)可能需要专门的三角化简规则。
未求值导数与积分
当 diff 遇到未知函数(不在导数表中的函数)时,返回 Derivative(f, x)。类似地,integrate 对无法处理的积分返回 Integral(f, x)。
这些未求值形式是合法的表达式节点——它们可以参与后续的符号运算。例如,$\frac{d}{dx}[\text{Derivative}(f, x)]$ 会返回 Derivative(Derivative(f, x), x),表示二阶导数。
与数值求值的衔接
符号微分产生的表达式可以传递给 ExpressionEvaluator(见 求值与 JIT)进行高效数值求值。典型工作流:
- 符号微分:
diff(&ctx, f, x)→ 导数表达式 $f'(x)$。 - 编译求值器:
ExpressionEvaluator::compile(f')。 - 数值求值:
evaluator.evaluate(&[x_val])→ $f'(x_{\text{val}})$。
这比数值差分(有限差分)精确得多,且避免了步长选择问题。
参考文献
- Geddes, K. O., Czapor, S. R. & Labahn, G. Algorithms for Computer Algebra. Kluwer Academic Publishers, 1992. — 第 11 章"Symbolic Differentiation"涵盖表达式树上的递归微分算法、化简策略和导数表设计。
- Bronstein, M. Symbolic Integration I: Transcendental Functions. Springer, 2005. — Risch 算法的权威参考,oCAS 积分器的设计依据。
- Cohen, H. A Course in Computational Algebraic Number Theory. Springer, 1993. — 第 1 章包含算法复杂度分析的一般方法论。