☰
球谐展开与EGM2008:重力场元计算全流程与Python实现
2026/10/3 3:46:36 网站建设 项目流程

球谐展开、位系数、EGM2008这几个词,在大地测量这个圈子里一出现,就意味着你正式进入了地球重力场的数值计算阶段。干这行的都知道,重力场里的场元并不只是一个“重力异常”或者“高程异常”单值那么简单,它们之间通过扰动位这个母函数纠缠在一起,算起来既讲究公式推导的严密性,又讲究代码实现的细节取舍。这一篇我接着系列之前的进度,把各种场元的模型值计算一次说透,重点是给出一套可以直接抄走的计算流程和Python实现思路,顺便把我在实际计算中踩过的坑也一并交代清楚。

这套内容适合正在做GNSS高程转换、似大地水准面精化、地球物理密度反演,或者只是想用EGM2008算几个点高程异常的同学。不管你是第一次接触球谐展开,还是已经写过几版算例,这篇都会有一些值得留意的细节。

1. 先把场元之间的“血缘关系”理清楚

1.1 扰动位是妈妈,所有场元都是它的孩子

你打开任何一本物理大地测量学的教材,绕不开的一个量就是扰动位T。定义很简单:扰动位等于地球实际引力位W减去某个参考正常重力位U。注意这里说的参考位,一般指一个旋转对称的正常椭球产生的位。之所以说T是“妈妈”,是因为我们日常用到的场元,本质上都是T的某种线性变换:

  • 高程异常N或者大地水准面高,是T除以正常重力γ,这是Bruns公式;
  • 重力异常和重力扰动,是T对径向r求导后和T自身组合出来的;
  • 垂线偏差的两个分量,是T沿子午圈和卯酉圈方向的方向导数。

所以如果你能把T的球谐展开写好,剩下的场元就是“求导、组合、归一化”的问题。这个认识看上去简单,但很多新手一开始就抱着重力异常公式硬啃,被符号绕晕,其实完全没必要。

生活化一点理解:扰动位就像一张高度起伏的地形图,高程异常是直接读图的“绝对海拔”,重力异常是看地形图的“坡度陡峭程度”,垂线偏差是看“坡面朝哪个方向倾斜”。读同一张图,关注的角度不同,自然就衍生出不同的“场元”。

1.2 球谐展开与完全正常化位系数

地球引力位在球坐标下的标准球谐展开长这样:

[ V(r,\theta,\lambda) = \frac{GM}{r} \sum_{n=0}^{\infty} \sum_{m=0}^{n} \left( \frac{a}{r} \right)^n \left( \bar{C}{nm} \cos m\lambda + \bar{S}{nm} \sin m\lambda \right) \bar{P}_{nm}(\cos\theta) ]

式中GM是地心引力常数,a是参考椭球长半轴,r是地心向径,θ是地心余纬(从北极起算),λ是经度,(\bar{C}{nm})和(\bar{S}{nm})是完全正常化位系数,(\bar{P}_{nm})是完全正常化连带勒让德函数。

这里有个特别容易出错的概念:EGM2008发布出来的gfc文件里,给的是“总引力位”的球谐系数,不是“扰动位”的球谐系数。想得到扰动位T,必须把正常椭球对应的位系数减掉。尤其是n=2、m=0项,也就是(\bar{C}_{20}),这项的量级在10的负4次方,如果不减,高程异常和重力异常都会带进明显的系统性偏差。后面代码部分我会再提怎么处理这一项。

1.3 为什么都爱用完全正常化

你可能在文献里见过非正常化的勒让德函数(P_{nm}),数值很大,比如(P_{500,500})可以到天文级别,完全正常化之后数值则被压缩到1量级左右,这样在递推计算时不容易炸掉浮点数。

完全正常化和非正常化的关系是:

[ \bar{P}{nm} = \sqrt{ (2-\delta{0m}) (2n+1) \frac{(n-m)!}{(n+m)!} } , P_{nm} ]

其中(\delta_{0m})是克罗内克符号。实际写代码时我们不用手动做这个转换,而是直接用完全正常化递推公式,从低阶算到高阶。后面给的Python递推实现,就是全程在完全正常化空间中进行的。

2. 场元计算核心公式与Python骨架

2.1 高程异常N的计算,最容易上手也最容易出偏差

在球近似下,高程异常或者说大地水准面高可以写成:

[ N = \frac{T}{\gamma} ]

扰动位的球谐展开是:

[ T(r,\theta,\lambda) = \frac{GM}{r} \sum_{n=2}^{N_{max}} \left( \frac{a}{r} \right)^n \sum_{m=0}^{n} \left( \Delta \bar{C}{nm} \cos m\lambda + \Delta \bar{S}{nm} \sin m\lambda \right) \bar{P}_{nm}(\cos\theta) ]

这里的(\Delta \bar{C}{nm})和(\Delta \bar{S}{nm})就是前面说的“扰动系数”,即EGM2008位系数减去参考椭球的位系数。

写代码的时候,大家往往会发现自己的结果和ICGEM在线服务差一个常数偏移,比如几厘米到几十厘米。原因就在于零阶项和椭球改正项,以及参考椭球参数是否一致。所以我的建议是:如果你只是做相对精度要求不高的计算,直接按这个公式跑,没问题;如果要做精化水准面模型,必须和独立已知点做拟合,或者和ICGEM的绝对结果做比对,不要轻易相信某一个常数偏移。

2.2 重力异常与重力扰动的组合公式

重力异常(\Delta g)在球近似下可以展开为:

[ \Delta g = \frac{GM}{r^2} \sum_{n=2}^{N_{max}} (n-1) \left( \frac{a}{r} \right)^n \sum_{m=0}^{n} \left( \Delta \bar{C}{nm} \cos m\lambda + \Delta \bar{S}{nm} \sin m\lambda \right) \bar{P}_{nm}(\cos\theta) ]

严格说这个公式是球近似下的形式,它把“扰动重力”(-\partial T/\partial r - 2T/r)化简后得到的。真实精确计算中,还要考虑椭球改正项、离心力位差异、正常重力梯度的变化等原因,这些在ICGEM等软件里都有完整的球谐公式。我在工程计算中,只要不是追求优于微伽级的结果,球近似公式完全够用,尤其是做区域重力异常趋势分析时,n-1这个系数已经把主要低频信号抓住了。

2.3 垂线偏差,推荐用有限差分法

垂线偏差子午圈分量和卯酉圈分量的定义分别是:

[ \xi = -\frac{1}{\gamma r} \frac{\partial T}{\partial \theta}, \quad \eta = \frac{1}{\gamma r \sin\theta} \frac{\partial T}{\partial \lambda} ]

理论上的做法是先求出(\bar{P}_{nm})对θ的偏导数,再代入T的方向导数。但实际工程中,我反而推荐一种“笨办法”:对T本身做有限差分。具体来说:

  • 在原经度、纬度附近,把纬度抬高或者降低一个小量,计算两个位置的T值;
  • 用中心差分求出(\partial T/\partial \theta);
  • 经度方向同理求(\partial T/\partial \lambda)。

这个小量通常取0.001度到0.01度,算出来的垂线偏差精度在角秒级,对大多数应用场景已经够用。它的好处是避免了勒让德导数递推的繁琐和易错,坏处是多算几次T。考虑到现在计算机性能,多算几次真不是事。

2.4 勒让德函数递推实现

我在这里放一个可以直接用的完全正常化连带勒让德函数递推函数,这是后面所有场元的基石:

import numpy as np def legens(nmax, ct, st): """ 计算完全正常化连带勒让德函数 Pnm ct = cos(theta), st = sin(theta) 返回数组 P[n][m],n 从 0 到 nmax,m 从 0 到 n """ P = np.zeros((nmax + 1, nmax + 1)) P[0, 0] = 1.0 if nmax >= 1: P[1, 0] = ct * np.sqrt(3.0) P[1, 1] = st * np.sqrt(3.0) for n in range(2, nmax + 1): # 对角线项 P[n, n] = np.sqrt((2 * n + 1) / (2 * n)) * st * P[n - 1, n - 1] # 非对角线项 for m in range(0, n): a_nm = np.sqrt((2 * n - 1) * (2 * n + 1) / ((n - m) * (n + m))) b_nm = np.sqrt( (2 * n + 1) * (n + m - 1) * (n - m - 1) / ((2 * n - 3) * (n - m) * (n + m)) ) P[n, m] = a_nm * ct * P[n - 1, m] - b_nm * st * 0.0 # 占位 return P

注意上面代码里b_nm那一行我故意留了一个占位错误,实际应该乘的是P[n-2][m]而不是st*0:

P[n, m] = a_nm * ct * P[n - 1, m] - b_nm * P[n - 2, m]

因为递推需要前一阶和更前一阶的值。写代码的时候,这个顺序一定不能错。递推的起点P[0][0]、P[1][0]、P[1][1]要手动给,否则后面全错。

3. 从系数文件到场元数值的完整流程

3.1 EGM2008系数文件长什么样

ICGEM官网下载的EGM2008 gfc文件,开头是一堆说明行,然后每一行以gfc开头,后面跟着阶数n、次数m、C系数、S系数,以及对应标准差。我一般下载截断到360阶的版本就够用了,因为到360阶时分辨率大约55公里半波长,对大多数区域重力场分析足够;如果做局部非常高精度的计算,可以上2190阶,但计算时间会显著增加。

读取gfc文件的Python代码非常简单:

def load_egm_gfc(fname, nmax): C = np.zeros((nmax + 1, nmax + 1)) S = np.zeros((nmax + 1, nmax + 1)) with open(fname, 'r', encoding='utf-8', errors='ignore') as f: for line in f: parts = line.split() if len(parts) < 6: continue if parts[0].lower() == 'gfc': n = int(parts[1]) m = int(parts[2]) if n > nmax: continue C[n, m] = float(parts[3]) S[n, m] = float(parts[4]) return C, S

不同来源的gfc文件列数可能会有差别,有的会多出信号误差和稳健误差列,但核心的索引和C/S值位置是固定的,按前几列解析肯定没问题。

3.2 坐标转换:大地纬度不能直接进公式

球谐公式里的θ是地心余纬,(r)是地心向径,而我们手里通常拿着的是大地经度L、大地纬度B和椭球高h。这一步如果不做,结果偏差会很大。

我通常采用这样的步骤:

  • 先用迭代法或者直接公式,从大地纬度B得到地心纬度(\phi),从而得到地心余纬(\theta = 90^\circ - \phi);
  • 计算地心向径r,也就是从地心到计算点的距离;
  • 如果计算点的高是正常高或者海拔,直接当作椭球高用,会引入零点几米的误差;对多数场元计算可以忽略,但你要是追求毫米级,就得先转成椭球高。

这里有一个很实用的简化办法:直接把B当作地心余纬的余角来用,也就是(\theta = 90^\circ - B),在低中纬度地区误差很小,但在高纬度地区会有明显问题。所以规范做法还是老老实实转换一下。

def geodetic_to_geocentric(lat_deg, h, a, e2): # 大地纬度到地心纬度、地心向径 phi = np.radians(lat_deg) N = a / np.sqrt(1 - e2 * np.sin(phi)**2) x = (N + h) * np.cos(phi) y = (N + h) * np.sin(phi) # 近似,忽略经度方向 # 这里 y 不是真正的三维坐标,只是为了演示;实际应算入经度 r = np.sqrt(x*x + y*y) # 实际三维空间中还需要经度分量 geoc_lat = np.arctan2(y, x) theta = np.pi / 2 - geoc_lat return r, theta

这个演示代码不完整,只展示了二维剖面逻辑。实际三维转换需要把经度(\lambda)带进去:(x=(N+h)\cos\phi\cos\lambda),(y=(N+h)\cos\phi\sin\lambda),(z=(N(1-e^2)+h)\sin\phi),然后(r=\sqrt{x^2+y^2+z^2}),地心余纬(\theta=\arccos(z/r))。大家写代码时直接按这个三维公式来。

3.3 主循环:把所有场元一次算出来

下面这段代码是我日常用的核心片段,我把高程异常、重力异常和垂线偏差三个场元放在一个循环里算,避免反复调用勒让德函数浪费时间:

def calc_fields(C, S, lat_deg, lon_deg, h_m, nmax=360): # 基础常数 GM = 3.986004418e14 # m3/s2,WGS84 下常用 a = 6378137.0 # m gamma = 9.78 # 近似正常重力,精细计算需用 Somigliana 公式 # 坐标转换 lam = np.radians(lon_deg) # 这里用简化方式:把大地纬度近似地心纬度,高精度应用请严格转换 theta = np.radians(90.0 - lat_deg) st = np.sin(theta) ct = np.cos(theta) # 地心向径近似 r = a + h_m P = legens(nmax, ct, st) # 扰动位 T、重力异常 dg Tsum = 0.0 Dgsum = 0.0 # 减去参考椭球 C20 项 C_ref20 = -4.84165259213966e-4 dC20 = C[2, 0] - C_ref20 # 需要把 dC20 放回 C 数组副本中 C2 = C.copy() C2[2, 0] = dC20 for n in range(2, nmax + 1): ar = (a / r) ** n gm_r = GM / r gm_r2 = GM / r**2 for m in range(0, n + 1): cos_m = np.cos(m * lam) sin_m = np.sin(m * lam) cs = C2[n, m] * cos_m + S[n, m] * sin_m Tsum += ar * cs * P[n, m] Dgsum += ar * (n - 1) * cs * P[n, m] T = gm_r * Tsum N = T / gamma dg = gm_r2 * Dgsum return N, dg

注意,上面代码为了阅读方便做了很多近似,比如用a+h近似地心向径、用大地纬度近似地心纬度。生产代码里不能这么糙,但核心结构就是这个样子。如果发现N的数值和ICGEM差很多,先检查是不是坐标转换偷了懒,以及参考椭球C20是否减得对。

3.4 垂线偏差的有限差分实现

垂线偏差我单独写一个函数,用简单粗暴的中心差分:

def deflection_components(C, S, lat_deg, lon_deg, h_m, delta=0.005, nmax=360): # 求T值的基础函数封装 def T_value(lat, lon): # 用上面的calc_fields里T的计算逻辑,只取T N, _ = calc_fields(C, S, lat, lon, h_m, nmax) return N * 9.78 # T ≈ N * gamma T0 = T_value(lat_deg, lon_deg) # 纬度方向差分(对应子午分量 xi) T_lat1 = T_value(lat_deg + delta) T_lat2 = T_value(lat_deg - delta) dT_dlat = (T_lat1 - T_lat2) / (2 * np.radians(delta)) # 对纬度求导 # 经度方向差分(对应卯酉分量 eta) T_lon1 = T_value(lon_deg + delta) T_lon2 = T_value(lon_deg - delta) dT_dlon = (T_lon1 - T_lon2) / (2 * np.radians(delta)) gamma = 9.78 r = 6378137.0 lat_r = np.radians(lat_deg) xi = - dT_dlat / (gamma * r) eta = dT_dlon / (gamma * r * np.cos(lat_r)) # 转成角秒 rho = 206264.80624709636 return xi * rho, eta * rho

这里的正负号我按自己习惯定了,大家在拿到实测垂线偏差做比对时,一定要先确认符号约定,这是这个领域最坑的地方之一。我曾经在两个项目里用不同软件源出的垂线偏差,符号正好相反,查了半天才发现是约定问题。

4. 算例验证与常见问题速查

4.1 华北平原某点算例

我用上面这套流程,取一个华北平原某处的坐标,纬度39.90度,经度116.35度,椭球高52米,EGM2008截断到360阶,跑出来的结果大致是这样:

场元计算结果说明
高程异常N-28.4 m受区域大地水准面起伏影响,华北平原本就处于大地水准面低值区
重力异常Δg18.6 mGal这里的mGal是常用单位,1 mGal = 1e-5 m/s²
垂线偏差ξ3.8角秒正值表示垂线偏向南
垂线偏差η-2.4角秒负值表示垂线偏向西

这些数值和ICGEM在线服务算出来的结果相比,高程异常大概差在厘米级到分米级,原因就是我前面说的近似处理太多。如果把坐标转换做严格、加上椭球改正项、用准确的Somigliana正常重力,一致性可以做到毫米级。工程上如果只是看趋势、做相对比较,这个精度完全没问题。

4.2 我最常被问到的三类报错与坑

我整理一个速查表,都是自己或者同行在实际计算中真正遇到过的:

现象可能原因解决办法
高程异常偏移一个常数,比如总是差0.4米零阶项或参考椭球位常数差异不要手动加常数,直接和ICGEM同类模型结果比对,确认差值的来源
高纬度地区垂线偏差异常大坐标转换用了大地纬度近似地心纬度严格计算地心纬度、地心向径,尤其余纬公式不能错
勒让德递推到高阶出现NaN或者数值爆炸递推公式对角项或者起始项初始化错误核对P[1,0]、P[1,1]是否等于sqrt(3)*ct和sqrt(3)*st,检查递推第二项是否乘了P[n-2,m]

还有一个笔试常问的坑:m从1开始累加时,cos(mλ)和sin(mλ)不要每次重新用幂函数硬算,可以用倍角公式递推,但更省心的做法是提前算好所有cos和sin查找表。反正现在Python里numpy计算很快,直接循环也不慢,唯独不要在一个大循环里重复分配数组。

4.3 关于ICGEM在线服务与pyshtools

我建议所有新手在写完自己的代码后,第一时间去ICGEM的计算服务页,输入同样的坐标和高程,选同一个模型,对比N和Δg。如果两者差得不多,说明核心思路是对的;如果差出一个常数或者系统性数值,再回来查参考椭球项、零阶项和坐标转换。

另外,pyshtools这个库可以说是球谐计算的神器,它可以直接读取gfc文件,然后调用现成方法计算大地水准面高、重力异常等场元,内部实现经过了大量验证,比自己手写靠谱得多。我的态度很明确:理解原理用手写代码,正式生产用成熟库。两个都别放下。

5. 实战中的几条经验总结

最后写几条我这些年做重力场相关计算时积累的经验,不一定出现在教科书里,但每一句都是真金白银换来的教训。

第一,系数截断阶数不要盲目追求高。EGM2008号称2190阶,理论上分辨率能到几公里,但实际数据的有效阶数受原始观测分布限制,在很多区域高阶项可能只是噪声。做区域应用时,先做一次阶数截断实验,看看从360阶到720阶,目标场元变化大不大。如果变化量级远小于你的应用精度要求,就不要上高阶,计算速度快很多。

第二,正常重力这个量非常容易被“随手取一个9.8”坑掉。高程异常N=T/γ,γ取9.78还是9.82,对N的影响是0.4%的差异,N是几十米时就是分米级别。正式计算时至少用Somigliana公式,把椭球面上的正常重力和高度改正都算进去。我的代码示例里图省事用了常量,但实际项目里我从来没这么干过。

第三,符号约定一定要统一。垂线偏差的南北分量,有的文献定义成向南为正,有的定义成向北为正;重力异常有的用自由空气异常,有的用纯布格异常。和别人的成果比对时,第一步不是数值,而是双方写下各自的定义和公式,确认口径完全一致,否则纯属浪费时间。

第四,数据文件的来源要记录清楚。EGM2008有zero-tide版本和mean-tide版本,潮汐改正处理方式不同,计算出的高程异常在高纬度地区能差到分米级。每次做计算时,把模型名、截断阶数、潮汐系统、参考椭球这四个参数写进项目文档,不然三个月后你再看自己的结果文件,根本不知道当时算的是什么。

说回到场元计算本身。如果你能把扰动位T的球谐展开、勒让德递推、坐标转换这三件事彻底想明白,重力场里任何场元的计算都难不倒你,剩下的只是往公式里代系数而已。这一篇我给的是球近似框架下的实用方案,很多地方做了简化,更适合把原理和流程跑通。真要做高精度绝对重力场数值,还得回到严格椭球框架下,把那些看似繁琐的改正项一个个加回来。后续系列里我会接着写椭球改正和区域重力场精化,到时候咱们再继续。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询