我那个“四元数系列”的笔记断断续续写到了第13篇。说实话,定下“四元数散度和旋度”这个标题时我犹豫了一阵,散度和旋度是三维矢量分析里的老朋友,四元数是个四维超复数,硬拼在一起怎么看都有点生拉硬扯。但真正动手推了一遍之后,我发现这个组合不但不违和,反而给出了一个非常漂亮的结论:如果把nabla算子当成一个“纯四元数”去乘一个矢量场,那么散度会落在乘法的实部,旋度会落在乘法的虚部,两者一次全部到手。
这篇内容适合三类人:一是做四元数姿态解算的朋友,已经对四元数乘法滚瓜烂熟,但很少从微分几何的角度重新审视它;二是刚学完矢量分析、想知道“散度旋度还能怎么理解”的数学物理方向学生;三是在折腾四元数场论、想找一套统一记号的研究者。你不需要四元数用得炉火纯青,只要知道四元数是什么、会做基本乘法,就能完整跟上后面的推导。
1. 为什么“四元数”会和“散度旋度”出现在同一个标题里
1.1 三个概念各自的“地盘”
先快速对齐一下概念。四元数是哈密顿当年在都柏林桥边灵光一现搞出来的东西,形式上是一个实部加三个虚部:
q = w + xi + yj + zk
它最出名的是乘法不满足交换律,以及被大规模用于三维旋转表示。散度(divergence)和旋度(curl)则是三维矢量场微分的两个主角:散度度量一个区域里“源”的总强度——把矢量场理解成水流的话,散度就是数水管里流出了多少水;旋度度量“转”的强弱——把一个小风车放进流场里,转得越快的地方旋度越大。
表面上看,四元数是四维的,散度旋度是三维的,两者不是一个量纲。但这里有一个容易被忽略的细节:四元数的虚部(xi + yj + zk)恰好就是一个三维向量。换句话说,每一个四元数都可以拆成“一个实数加一个三维向量”,而这个拆分,正好给散度旋度留出了天然的位置。
1.2 不是“四元数的散度和旋度”,而是“用四元数乘法重写散度和旋度”
标题容易让人产生一个误会:难道四元数本身也有散度和旋度吗?严格说不是。
我们讨论的是这样一种操作:把nabla算子∇视为一个纯四元数(实部为零、虚部是偏导算符),把参与运算的矢量场也视为纯四元数,然后做四元数乘法。乘出来的结果里,实部对应散度的负值,虚部对应旋度。也就是说,这不是给四元数“发明”了散度和旋度,而是把经典矢量分析中的两个概念,装进了四元数乘法的代数框架里。
这个视角的价值在于统一。以前你写div和curl是两个不同的记号,运算规则也各管各的;现在它们只是同一个四元数乘法的两个投影方向,很多恒等式写起来会格外对称。这种统一在理论推导和符号计算里非常有用,后面我会详细展示。
1.3 搜索时会撞见的“JS散度”
准备这篇笔记时我顺手搜了一下关键词,首页混进来不少“JS散度”。这是信息论里的Jensen-Shannon散度,用来度量两个概率分布之间的差异,跟本文说的矢量场散度没有任何关系。如果你是从算法题、机器学习资料里搜到这篇文章,先确认一下自己到底在找哪个散度。信息论里的JS散度和矢量分析里的散度,只是中文翻译撞了车。
还有一个容易混淆的词是“两向量相乘求散度和旋度”。这其实正是本文的核心内容:两个向量(包括nabla算符这种形式向量)在四元数乘法下,确实能同时吐出散度和旋度,但前提是你要理解负号和叉积方向约定,不然很容易算完符号是反的。
2. 核心公式的诞生:两向量相乘为何能同时吐散度和旋度
2.1 先记住这个漂亮的乘法结构
四元数乘法的一般形式是:
(w1, v1)(w2, v2) = (w1w2 - v1·v2, w1v2 + w2v1 + v1×v2)
其中v1、v2是三维向量部分。如果两个四元数都是纯四元数,也就是实部w1 = w2 = 0,那么公式直接退化成一个非常精致的结构:
(0, a)(0, b) = (-a·b, a×b)
看明白这个式子的意义:a和b是两个三维向量,把它们都填进四元数的虚部、实部置零,乘出来的四元数,实部是“负的内积”,虚部是“叉积”。也就是说,四元数乘法把向量分析里最常用的两种运算——点积和叉积——捆绑进了同一个代数结构里。这个式子是一把钥匙,后面所有内容都从它派生。
2.2 让nabla算子客串“纯四元数”
nabla算子在直角坐标系下定义为:
∇ = i·∂/∂x + j·∂/∂y + k·∂/∂z
它本来是一个“形式向量”微分算子。现在我们把i、j、k理解为四元数的三个虚单位,那么这个∇就自然成为一个“纯四元数系数微分算子”,实部为0。
对一个三维矢量场F = (Fx, Fy, Fz),同样把它写成纯四元数形式:
F = Fx·i + Fy·j + Fz·k
然后做四元数乘法∇F。这里要特别注意运算顺序:先把∂/∂x作用到Fx、Fy、Fz上,再按四元数乘法规则把结果组合起来。展开后,实部是:
-(∂Fx/∂x + ∂Fy/∂y + ∂Fz/∂z)
这一项正是负的散度。虚部是:
i(∂Fz/∂y - ∂Fy/∂z) + j(∂Fx/∂z - ∂Fz/∂x) + k(∂Fy/∂x - ∂Fx/∂y)
这一项正是旋度。于是得到一个极其漂亮的公式:
∇F = -div(F) + curl(F)
用自然语言说:nabla算子左乘一个“纯四元数形式的矢量场”,实部自动给出负散度,虚部自动给出旋度。
2.3 为什么散度前面带一个负号
很多第一次看到这个公式的人会问:为什么是“负散度”?不是“正散度”?
答案藏在四元数的乘法表里。纯四元数相乘时,实部来自三个虚单位的平方项:i² = j² = k² = -1。你可以自己乘一下:
(ix + jy + kz)·(iFx + jFy + kFz)
展开实部时,i·i项贡献-xFx,但不是+xFx,因为i² = -1。三个分量加在一起,实部天然就是负的内积。换句话说,负号不是我们人为加进去的,而是由“虚单位平方等于-1”这个代数规则决定的。
这其实是一件好事。它提醒我们:四元数框架下的散度,天然带一个负号约定,任何推导都必须把这个负号焊死在公式里。我在后面讲踩坑时还会提到,不少符号错乱都是从这里开始的。
2.4 两个能用手算验证的例子
理论推导完,最好做两个可以直接手算的验证,避免公式停留在抽象层面。
例一:取F = r = (x, y, z),也就是位置矢量本身。
经典计算:
- div r = ∂x/∂x + ∂y/∂y + ∂z/∂z = 3
- curl r = 0
套用四元数公式:∇r = -3。结果是一个纯实数-3,虚部为零。这说明位置矢量场是纯散度场,完全没有旋转分量,而且散度为正值时,四元数乘法的实部为-3。
例二:取旋转流场F = (-y, x, 0)。
这个场描述的是绕z轴做刚体旋转的线速度场。
- div F = 0
- curl F = (0, 0, 2)
套用公式:∇F = 2k。结果是一个纯四元数,实部为零,虚部只有k方向分量2。完美对应旋度指向z轴、大小为2。
这两个例子特别适合自己动手算一遍,计算量小,却能完整印证公式的两个方向:一个纯散度、一个纯旋度,四元数乘法的实部和虚部分别接住了它们。
3. 当被作用的是完整四元数场——梯度也跟着进来了
3.1 拆开一个完整的四元数场
上一节只处理了纯四元数场,也就是只有虚部、实部恒为零的场。但更一般的情况是,场本身有实部。记:
q(x,y,z) = s(x,y,z) + u(x,y,z)·i + v(x,y,z)·j + w(x,y,z)·k
其中s是标量函数,v = (u,v,w)是三维矢量函数。这类四元数场在物理里很常见,比如把标量势和矢量势拼在一起,或者把电磁场写成某种四元数形式。
现在计算∇q。推导过程我建议自己拿纸笔走一遍,只给出关键思路:先把i·∂q/∂x、j·∂q/∂y、k·∂q/∂z三项分别展开,利用四元数乘法表(i·j = k,j·i = -k,等等)整理实部和虚部,然后合并。
3.2 三种微分算子的“合体结果”
展开并合并后得到:
∇q = -div(v) + grad(s) + curl(v)
用四元数语言完整写出来:
- 实部:-div(v),只与矢量部分的散度有关,标量部分不贡献实部
- 虚部:grad(s) + curl(v),是标量部分的梯度与矢量部分的旋度之和
这个公式把矢量分析里的三个主角一次全请了出来:梯度、散度、旋度,被一个nabla算符的四元数乘法统一起来。以前你写“梯度场”“散度场”“旋度场”时它们是三套概念,现在它们只是同一个代数对象的不同投影。
3.3 退化情况的对照表
为了印象更直观,我整理了一张退化情况表:
| 场的类型 | q的形式 | ∇q结果 | 说明 |
|---|---|---|---|
| 纯标量场 | s | grad(s) | 只有梯度,虚部 |
| 纯矢量场 | v | -div(v) + curl(v) | 实部装散度,虚部装旋度 |
| 完整四元数场 | s + v | -div(v) + grad(s) + curl(v) | 梯度、散度、旋度全部出现 |
看到没有,纯标量场时四元数乘法自动退化为梯度运算,纯矢量场时退化为“负散度加旋度”,完整四元数场时三者并存。这个统一的视角在推导恒等式时非常省力,尤其是涉及多个场相乘、需要交换运算顺序时,四元数记号的紧凑性会明显降低出错概率。
3.4 物理直觉:一个对象装两类信息
为什么我要强调“完整四元数场”这个结构?因为在很多物理框架里,一个场往往同时携带标量信息和矢量信息。比如电磁学中,标量势φ和矢量势A各自独立,却可以通过某种四元数组合放在一起。这时∇q的实部和虚部就成了两个天然的信息通道:
- 实部通道:负责“源”的信息,即散度
- 虚部通道:负责“变化与旋转”的信息,即梯度和旋度
这种“一个对象、两条通道”的视角,是四元数分析相对于经典向量分析的核心优势。它不是给你新的物理规律,而是给已有的规律换一套更整齐的记账方式。
4. 左乘还是右乘,结果完全不同
4.1 非交换性是第一个坑
四元数乘法不满足交换律,∇q和q∇不相等。很多人第一次在这上面栽跟头:明明按公式算对了∇F = -div + curl,换成F∇就发现对不上号。
这一点必须在一开始就建立意识:我们讨论的是“nabla算子左乘场”还是“场左乘nabla算子”,这两个操作在四元数框架下是两个不同的东西。经典向量分析里不会有这个纠结,因为点积是交换的、叉积反交换但有明确规则;而四元数乘法既有内积的影子又有叉积的影子,顺序一换,符号会乱。
4.2 右乘结果对比
我直接给出右乘的关键结果。对纯矢量场F,定义右乘算符:
(∂F/∂x)i + (∂F/∂y)j + (∂F/∂z)k
展开合并后得到:
F∇ = -div(F) + (curl_x·i - curl_y·j + curl_z·k)
也就是说,实部仍然等于负散度,但虚部不是完整的旋度矢量,而是旋度矢量把y分量取反之后的镜像。我用一张表把左乘和右乘的差异列清楚:
| 操作 | 实部 | 虚部 |
|---|---|---|
| ∇F(左乘) | -div(F) | curl(F) = (curl_x, curl_y, curl_z) |
| F∇(右乘) | -div(F) | (curl_x, -curl_y, curl_z) |
这个镜像现象是四元数内在手性的体现。i、j、k在乘法表里不是完全对称的,ijk = -1这条规则偷偷给坐标系钦定了一个手性,所以右乘会把某个分量翻转。换用JPL约定或别的虚部基序,镜像的形式还会改变。
4.3 实际使用时的选择建议
既然左乘右乘不一致,实战中到底该用哪个?我的建议很明确:默认使用左乘∇F形式,因为它的虚部正好是标准旋度,含义清晰、不容易混淆。右乘形式更适合某些特定对称性分析,但如果你没有非用不可的理由,就别给自己添麻烦。
在做四元数姿态解算时,类似的问题也存在:角速度到底放在四元数左边乘还是右边乘,取决于你用的是“世界系”还是“载体系”约定。这个行业里最怕的就是左手一套约定、右手一套约定,写代码时看着都对,结果同一个旋转在不同模块里方向相反。记住一个原则:选好约定后,把它写进项目文档的第一页,并配上一条最基本的测试用例。
5. 从公式到姿态解算——旋转场的旋度到底意味着什么
5.1 为什么姿态解算最终选了四元数
热词里提到了“三维姿态:roll、pitch、yaw,或四元数”,这里展开几句。
欧拉角(roll、pitch、yaw)直观但不省心,最大的问题是万向锁:当pitch达到±90度时,roll和yaw的旋转轴重合,系统失去一个自由度,姿态解算出现奇异。此外,欧拉角的插值不平滑,两个姿态之间直接插值三个角会得到怪异的中间姿态。四元数则不存在这些问题:单位四元数q = (cos(θ/2), n·sin(θ/2))可以直接表示绕单位轴n旋转θ角,不会出现奇异,插值时用球面线性插值(SLERP)能得到平滑的中间姿态,所以姿态解算领域基本都被四元数接管了。
5.2 四元数微分方程与角速度的联系
姿态解算的核心微分方程是:
dq/dt = (1/2)·q⊗ω
其中ω是载体坐标系下的角速度,写成纯四元数形式。这个方程把四元数随时间的变化率和角速度联系起来。如果已知角速度,就能积分出姿态;如果已知姿态序列,也能反解出角速度。
但这里有一个很少被讨论的点:上面的ω是时间的函数,描述刚体在某个时刻的“瞬时旋转”。如果旋转不是整体的,而是随空间位置变化的——比如流体微团的局部旋转、变形体的扭转变形——那么角速度ω就不再只依赖时间t,而是同时依赖空间位置(x,y,z)。此时ω(x,y,z)变成一个矢量场,可以对其求散度和旋度。
5.3 用四元数方法处理“旋转场”
当一个旋转场ω(r)随空间变化时,本文前面推的公式就派上用场了:
∇ω = -div(ω) + curl(ω)
这里:
- div(ω)度量角速度场的“源汇”。例如在某个区域持续有涡旋注入角速度,div就反映了这种注入强度
- curl(ω)度量角速度场本身的不均匀旋转。注意这已经是旋度的旋度概念了,物理上对应“角速度的剪切不均匀程度”
举一个直观例子:刚体整体匀速旋转时,角速度场是常矢量场,div和curl都为零,∇ω = 0;而流体中的涡旋在不同半径处角速度不同,角速度场不再是常值,curl(ω)就会给出关于涡旋空间结构的信息。用四元数乘法一次就能同时得到两个量,在流体力学、陀螺仪数据分析里可以把中间步骤省掉不少。
5.4 对工程写法的一个启发
我自己做姿态解算调试时,会把“四元数微分方程”和“三维矢量场的散度旋度”分开来看。前者的任务是递推动作,属于常微分方程范畴;后者是空间分析的视角,属于场论范畴。很多代码用四元数只处理了前者,却忽视了后者其实也能用四元数统一表达。如果你的项目里同时涉及姿态变化和空间流场分析,可以考虑把这两套逻辑都建立在四元数乘法上,代码结构会更清爽。
6. 踩坑记录与自我验证方法
6.1 约定不一致,神仙难救
我在这个主题上踩过的最大一个坑,是四元数乘法的方向约定。
市面上一共流行两套主要的四元数约定:Hamilton约定(也常直接称“标准四元数”)和JPL约定。两者虚部符号相反,乘法顺序相反,直接导致同样的代码在A库和B库之间迁移时结果差一个负号。再加上左乘右乘的选择差异,一共会出现四种排列组合。我见过一个项目,姿态解算模块用的是Hamilton约定,传感器融合模块用的是JPL约定,两个模块单独单元测试都通过,一联调就转飞。
我的建议是:
- 项目中只允许出现一种约定,并在接口文档里显式声明
- 在模块入口处写一个“公约数测试”:给定一个已知绕z轴旋转90度的四元数,验证输出是否和预期一致
- 任何库函数,先跑通一个有标准答案的用例,再接入业务逻辑
这一步看着琐碎,但能省下后面至少一个晚上的调试时间。数学公式的负号问题,往往不是不会,而是约定漂移。
6.2 四元数的“散度”搜出来一堆JS散度
前面已经提过,搜索“四元数散度”时会被信息论里的JS散度(Jensen-Shannon散度)干扰。这里再说细一点:如果你是在做机器学习相关的东西,看到“散度”两个字先确认到底是概率分布不等式里的散度,还是矢量微积分里的散度。前者关心两个分布之间的差异程度,后者关心矢量场的源汇强度,除了共享一个译名,数学对象完全不同。
我自己搜索时一般会加上限定词,比如“四元数 散度 矢量场”“nabla 四元数 旋度”,能过滤掉不少无关结果。工欲善其事,必先利其器,搜索词的准确性能直接决定资料质量。
6.3 用SymPy做符号验证
手推四元数公式很容易因为漏项或符号错乱翻车,尤其是实部虚部分开展开的时候。我推荐一个很有效的自检方法:用SymPy做符号验证。下面是一段可以直接运行的验证代码。
import sympy as sp x, y, z = sp.symbols('x y z', real=True) # 定义矢量场 F = (-y, x, 0) Fx, Fy, Fz = -y, x, sp.Integer(0) # 经典散度和旋度,用于对照 div = sp.diff(Fx, x) + sp.diff(Fy, y) + sp.diff(Fz, z) curl = sp.Matrix([ sp.diff(Fz, y) - sp.diff(Fy, z), sp.diff(Fx, z) - sp.diff(Fz, x), sp.diff(Fy, x) - sp.diff(Fx, y), ]) # 四元数乘法版本:∇F = -div + curl real_part = -div vector_part = curl print('div =', sp.simplify(div)) print('curl =', sp.simplify(curl[0]), sp.simplify(curl[1]), sp.simplify(curl[2])) print('nabla(F) 实部 =', sp.simplify(real_part)) print('nabla(F) 虚部 =', sp.simplify(vector_part[0]), sp.simplify(vector_part[1]), sp.simplify(vector_part[2]))输出会明确显示“实部 = 0、虚部 = (0, 0, 2)”,和手算一致。当你自己设计一个四元数场时,把未知函数符号填进去跑一遍,就能快速发现实部虚部是否放反、y分量符号是否写错。PyTorch、JAX这类框架跑偏微分也很快,但做符号层面的验证SymPy最舒服。
6.4 什么时候该用四元数求散度旋度
最后说一点个人体会:四元数方法适合做统一表达和理论推导,不适合当日常计算工具。
日常需要算某个具体矢量场的散度旋度时,直接用经典公式div、curl最省事,没有理由绕道四元数。但当你面对以下场景时,四元数方法是更优解:
- 需要在同一个代数结构里同时处理梯度、散度、旋度,且希望避免大量无关公式的来回转换
- 在做符号推导,需要简洁的乘法记法让表达式短一些
- 已经在用四元数处理姿态或旋转数据,希望把场论分析也整合进同一套数学语言
我不会假装四元数是万能的。它的乘法非交换,虚部基有手性问题,这些特性在工程里都会增加认知负担。但它把散度和旋度拼进一个公式这件事,至少在数学上是足够精巧、也足够值得记住的一个视角。
我自己的习惯是,把∇F = -div + curl这个式子贴在工位前面,不是为了每次计算都想起它,而是为了提醒自己:很多看似毫无关联的数学结构,换个代数包装就能拼到同一张桌上。技巧在其次,这种“包装感”才是四元数真正教给我的东西。