尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

地震震级计算:基于ObsPy的ML/MS/MW自动解算与可视化

地震震级计算:基于ObsPy的ML/MS/MW自动解算与可视化 1. 项目背景与整体设计思路1.1 先搞清楚地震台网里说的“magnitude”到底指什么很多刚接触地震数据的人一上来就问我“震级是不是就是地震大小”。这句话对但远远不够。震级并不是一个像“重量”那样可以直接量出来的物理量它更像是我们用仪器记录到的某种振幅经过一大堆经验公式校正之后得到的一个“约定俗成的量”。更麻烦的是同一个地震不同的测定方法会给出不同数值里氏震级、面波震级、矩震级之间可能差出 0.5 甚至更多而这在行业里是正常的。我刚开始做这个项目的时候一位老前辈跟我说了一句很实在的话“你要做的东西不是去发明一种新的震级而是把几种主流震级在同一套流程里算得又快又稳让人能对照、能追溯、能纠错。”这句话后来成了整个项目的指导思想。我做这个名为 magnitude 的内部工具就是为了解决一个具体问题当台网记录到大量波形事件时如何自动化地从原始数据出发经过预处理、震相识别、振幅测量、公式计算最终输出多类型震级结果和可视化报告把原本需要人力逐个处理的工作变成一套可复用的流水线。到现在这套工具已经在多个小震群里实测过稳定性比预期好这里把我的设计思路、实现细节和踩过的坑整理出来供同行参考。1.2 项目设计的三个核心目标自动化为先人工只需要输入事件时间和大概震中位置剩下的波形读取、滤波、震相识别、振幅提取、震级计算全部自动完成。多震级可对照一次计算同时输出 ML近震震级、MS面波震级和 MW矩震级并保留每一步的中间量方便人工核验。结果可解释工具不能只输出一个数字必须能画出波形图、标注震相位置、显示振幅测量窗口否则出了问题根本没法排查。这三个目标决定了后面的技术选型。我见过不少工具只追求“一键出结果”结果一旦结果偏差用户根本不知道是哪个环节坏了。震级这种东西宁可慢一点也要每一步都透明。1.3 技术选型与模块划分技术栈选择上我几乎没有犹豫就定了 Python。原因很简单地震数据处理这个领域ObsPy 已经是事实上的标准库支持从 SEED、MiniSEED、SAC 等多种格式读取波形数据内置了滤波、去仪器响应、震相拾取等常用功能社区活跃遇到问题基本都能搜到答案。整个项目按功能拆成了几个模块结构大致这样magnitude/ ├── config.yaml # 台站信息、事件参数、滤波频率等配置 ├── main.py # 主流程入口 ├── modules/ │ ├── preprocessing.py # 去均值、去趋势、滤波、去仪器响应 │ ├── phase_picking.py # P波、S波自动拾取 │ ├── amplitude.py # 振幅和周期测量 │ ├── magnitude_calc.py# 三种震级的计算核心 │ └── plotter.py # 波形图、震相标注、振幅窗口可视化 ├── reports/ # 输出的报告目录 └── tests/ # 单元测试和验证脚本模块化带来的最大好处是哪一步有问题直接去对应模块里找不用把整个流程读完。比如后来我发现 MS 计算总是不稳定定位到是 amplitude.py 里面波测量窗口取得太窄只改了这一个函数不影响其他部分。如果当初把所有代码写在一个大文件里调试成本会是现在的几倍。2. 三种震级的计算原理与实现细节2.1 近震震级 ML振幅加距离校正但别忽略仪器模拟ML也就是我们常说的里氏震级是 1935 年 Richter 提出的最初是针对南加州地区、用伍德-安德森扭力地震仪记录到的地震。关键在于这种地震仪的放大倍数、自然周期和阻尼都是固定的所以用现代数字宽频地震仪记录的数据算 ML 时不能直接拿原始波形上的最大振幅去套公式得先把数字波形模拟成伍德-安德森仪器的响应。这一步是很多新手踩坑的重灾区。如果直接拿宽频仪器的波形振幅算 ML结果会和正式地震目录差不少。在 ObsPy 里可以通过simulate_response把仪器响应模拟到指定的传感器参数或者用另一个更简单的办法利用贝尼奥夫Benioff等研究者给出的响应差异校正关系。我在项目里选择直接模拟伍德-安德森响应虽然计算量稍大但物理过程更透明也更适合作为教学和科研的基准。ML 的通用计算公式可以写成ML log10(A_WA) q(Δ)其中 A_WA 是模拟伍德-安德森响应后测得的最大振幅单位微米μmq(Δ) 是距离校正量规函数Δ 是震中距。不同地区的台网会给出本地化的量规函数所以我的工具里把 q(Δ) 做成可配置的支持读入外部校正表。举个例子震中距约 50 公里、模拟后最大振幅为 10 μm如果该距离处校正量为 2.4那么ML log10(10) 2.4 1 2.4 3.4这个地震差不多就是 3.4 级。注意振幅本身必须是在正确的时间窗内量取的一般是 S 波到达后的一段时间窗口而不是全波形任意找最大值。2.2 面波震级 MS对远震和大震更稳定面波震级 MS 是针对浅源远震提出的尤其是震中距在 1000 公里以上的记录。它利用的是周期 20 秒左右的面波主要是瑞利波测量的物理量是水平向最大振幅除以对应的周期。标准公式在很多教材里写成MS log10(A/T) 1.66 * log10(Δ) 3.3A 是面波最大振幅微米T 是周期秒Δ 是震中距度。这个经验公式里 1.66 和 3.3 是标准条件下的常数实际用起来要检查当地台网是否给出过更适用的系数。实现 MS 有个细节面波不是一到就完它是一串比较低频的波动需要在记录上定位合适的测量窗口。我采用的是先做一个 10 到 25 秒带通滤波再在预定义速度窗口内扫描最大振幅峰-峰值的一半作为 A并同步读取对应的周期 T。这个窗口通常用群速度来定义比如瑞利波从震源到台站的群速度大约在 3.0 到 4.0 km/s 之间可以根据震中距换算成时间窗。用曲线图来看面波在波形图上通常是一串明显比体波“胖”的低频振荡肉眼能认出来但程序判断要更仔细。窗口太长有可能混入后续的噪声或其他震相窗口太短又会截断真正的面波导致振幅偏小。经过几轮测试我把窗口设成从“理论面波到时提前 10 秒”到“理论到时之后 60 秒”这对大多数区域台网数据都适用。2.3 矩震级 MW不饱和但需要估算地震矩矩震级 MW 是 1977 年 Kanamori 提出的它的核心是地震矩 M0一个反映断层滑动总量与破裂面积的物理量。公式为MW (2/3) * log10(M0) - 6.07这里 M0 的单位是牛顿·米N·m。MW 的最大优点是物理意义清楚而且不会像 ML 或 MS 那样在大震时“饱和”——也就是震级到了某个值以后振幅很难再明显增大导致计算值偏低。但现实问题是M0 不像振幅那样能直接从波形上量出来。它需要通过对震源谱进行拟合或者用波形反演来获得。我在这套工具里用的是相对简单的震源谱拟合法先取 S 波到达后的一个时间窗做频谱分析得到位移谱然后基于 Brune 模型拟合出低频平台和拐角频率。低频平台的数值和地震矩成正比。这种方法比简单量振幅复杂需要调的参数也多但好处是每个步骤都有物理意义。如果台站三分量数据质量不错拟合出的 M0 和 GCMT 目录的差距能控制在合理范围内。如果只有单分量短周期记录MW 就别硬算工具会自动标记为“低可信度”避免给用户一个误导性的数字。2.4 为什么算出来的 ML、MS、MW 会不一致只要是处理过真实地震数据的人都会遇到同一个困惑同一个地震ML 是 4.2MS 却只有 3.8MW 又是 4.0到底该信哪个其实三个都对只是它们测量的物理对象和频率范围不同。ML 基于 1 秒左右周期的体波最大振幅适合地方震和近震。MS 基于 20 秒周期的瑞利面波适合远震和浅源大震。MW 基于整个震源谱的低频水平不受饱和影响物理上最稳定。打个比方这就好比同一个病人体温计量出来 38.5 摄氏度血常规指标也偏了影像检查显示有炎症。三个测量方法侧重点不同但都在反映同一个“病灶”。在实际报告中通常会优先采用 MW其次参考 MSML 作为近震的快速测定值。我在工具里专门生成一张对比表让用户看到不同方法的差别而不是傻傻只给一个数。3. 实操过程从波形文件到震级报告3.1 数据准备与波形读取我用的数据是台网导出的 MiniSEED 格式文件文件名一般携带台站代码和通道信息比如20240101_0000.IX.SCB..BHZ.mseed。事件触发信息从一个文本文件读入包含发震时刻、震中经度纬度和深度。from obspy import read from obspy import UTCDateTime # 读取单个台站的波形文件 st read(data/20240101_0000.IX.SCB..BHZ.mseed) # 根据事件触发时间裁剪数据事件前30秒到后300秒 t0 UTCDateTime(2024-01-01T00:00:00.5Z) st.trim(t0 - 30, t0 300)读取这里要注意采样率。有些台站是 100 Hz有些是 40 Hz后续处理窗口长度是按秒来的但换算成采样点时要根据实际采样率动态计算不能在代码里写死一个点数。3.2 预处理流水线干净数据是一切的前提波形数据直接拿来算振幅基本都会出问题必须经过完整的预处理。我整理了一套标准流水线顺序不能乱from obspy.core.trace import Trace def preprocess(st): # 1. 去均值 st.detrend(demean) # 2. 去趋势 st.detrend(linear) # 3. 去尖峰做一次简单的中值滤波或台账跳跃修正 # 4. 带通滤波频段按震相类型选 st.filter(bandpass, freqmin0.5, freqmax20.0) # 5. 如果后续要算MW这里必须保留原始仪器响应信息不能滤掉 return st这里有个容易忽略的点如果后续要计算 ML我们需要的是模拟伍德-安德森仪器响应后的波形如果要算 MW我们需要的是真实地面运动位移而不是滤波后的原始计数。所以在流程设计上预处理会同时保留两路数据一路用于震相检测的滤波数据一路用于震级计算的全频段数据。3.3 P 波、S 波自动拾取STA/LTA 算法够用震相拾取的质量直接决定了后面振幅测量的时间窗。我用的是经典的 STA/LTA 算法短时窗均值除以长时窗均值比值超过阈值就认为有震相到达。def pick_p_s(st, config): trace st.select(componentZ)[0] df trace.stats.sampling_rate data trace.data.astype(float) sta_len int(config[sta_seconds] * df) # 通常 1 秒 lta_len int(config[lta_seconds] * df) # 通常 10 秒 # 用递归方式计算STA/LTA避免循环太慢 sta np.convolve(np.abs(data), np.ones(sta_len)/sta_len, modesame) lta np.convolve(np.abs(data), np.ones(lta_len)/lta_len, modesame) ratio np.divide(sta, lta, outnp.zeros_like(sta), wherelta!0) # 找超过阈值的最早点 idx np.where(ratio config[trigger_threshold])[0] p_time trace.times(matplotlib)[idx[0]] if len(idx) 0 else None return p_timeSTA/LTA 虽然原始但非常稳定尤其在信噪比一般的台站记录上比很多花哨的 AI 拾取更可靠。S 波拾取稍微复杂一些需要在水平分量上联合判断或者用 P 波到时加一个固定的 vp/vs 比值估算。我在工具里默认用后者做兜底再让算法在 S 波窗口内搜索最大振幅位置来微调。3.4 振幅提取与震级计算的完整代码这是整个工具的核心环节。下面这段代码演示了如何从预处理后的波形中提取最大振幅并计算 MLimport numpy as np def calc_ml_from_trace(trace_wa, delta_km): trace_wa: 已模拟伍德-安德森响应的波形 delta_km: 震中距单位 km # 取S波到达后的时窗这里是简化的做法 start int(trace_wa.stats.sampling_rate * 5) window trace_wa.data[start:] # 最大峰-峰值 amp_peak np.max(window) - np.min(window) amp_um amp_peak / 2.0 # 半振幅 if amp_um 0: return None # 距离校正量规函数不同地区可替换成自己的值 q_delta 1.1 np.log10(delta_km) * 0.7 ml np.log10(amp_um) q_delta return ml这个代码是简化版本真实的校正函数要复杂得多不同距离段的 q(Δ) 会做成查表或者分段函数。不过核心思想就是这样先模拟正确的仪器响应再在正确的时窗内测到振幅最后加上距离校正。3.5 多台综合与报告输出单个台站的震级误差较大实际使用中至少要 3 个以上台站给出结果再平均。直接平均不够严谨我采用信噪比加权平均信噪比高的台站权重更大这样可以减少个别噪声台站对整体结果的拖累。def combine_magnitudes(ml_values, weights): w np.array(weights) vals np.array([v for v in ml_values if v is not None]) if len(vals) 0: return None return np.sum(vals * w[:len(vals)]) / np.sum(w[:len(vals)])报告输出我用了两部分一份 CSV 文件记录每个台站的原始测量数据方便回溯一份 PDF 图形报告把每个台站的波形图、震相标记、振幅测量窗口画出来。日常速报看 PDF 就够但要写论文或归档CSV 更实用。4. 可视化与震级统计应用4.1 单事件波形图让每个测量都有据可查只给一个最终震级用户是不放心的。magnitude 工具在每次计算后都会自动生成一张三分量波形图图上标注了 P 波、S 波拾取结果以及振幅测量窗口。这样一旦数值异常直接看图就能判断是拾取错了还是窗口选错了。比如有一次计算某个台站的 ML 时结果总是比邻近台站低 1 个量级。打开波形图一看S 波拾取窗口刚好落在噪声上最大振幅被压低了。调整拾取参数后恢复正常。如果没有可视化这一步光看数字根本没法定位问题。4.2 多台站震级对比图检测台站异常把同一事件下所有台站计算出的震级画在横轴为震中距的散点图上能发现一些有趣的现象。正常情况下震级不应该随距离明显变化如果某台站的震级系统性偏大或偏小大概率是台站响应标定出了问题或者仪器状态异常。这个图我在一次批量处理中还真发现了问题某个台站的数据连续多天偏高查下来是传感器底座松动。这种问题靠人工一份份看报告很难发现但震级对比图一眼就能看出异常台站是“离群点”。4.3 G-R 关系与 b 值量级统计的地震学意义震级计算只是手段震级分布本身才是研究区域地震活动性的重要输入。最经典的就是古登堡-里克特关系log10(N) a - b * MN 是大于等于震级 M 的地震数a 反映区域地震活动水平b 值反映大小地震的比例。稳定地区的 b 值通常在 1 左右b 值变小往往意味着较大地震占比增加可能指示应力状态变化。magnitude 工具内置了一个统计模块输入一批事件的震级列表直接拟合 G-R 关系并生成频度-震级图。虽然这个功能不算复杂但把“算震级”和“用震级”串在了一起整个工具有了更深的价值。def fit_gr(magnitudes, bin_size0.1): min_m np.floor(np.min(magnitudes) / bin_size) * bin_size max_m np.ceil(np.max(magnitudes) / bin_size) * bin_size bins np.arange(min_m, max_m bin_size, bin_size) counts, edges np.histogram(magnitudes, bins) centers (edges[:-1] edges[1:]) / 2 # 只取非零计数部分拟合 valid counts 0 x centers[valid] y np.log10(counts[valid]) coeffs np.polyfit(x, y, 1) # b值就是斜率的负值 b_value -coeffs[0] return b_valueb 值估计算法看起来简单实际要小心最小震级完整性问题。台网对小地震的记录是不完整的如果直接用所有数据拟合b 值会偏低。更严谨的做法是先通过拟合度分析确定最小完整震级 Mc只统计 Mc 以上的地震。这个细节我在工具文档里写了很长的说明提醒使用者不要盲目套用。5. 踩坑记录与参数调试指南5.1 滤波频率为什么总是定不准我在早期版本里对所有台站用同一套滤波参数结果在低频台站上计算 MS 时结果跳动很大。后来才意识到不同台站的背景噪声特征不一样台基是岩石还是沉积层高频噪声水平天差地别。现在的方案是让滤波参数支持按台站配置并且在处理前做一个简单的噪声功率谱分析自动判断该台站适合的频段。这个功能虽然增加了一点计算时间但换来的是震级计算的稳定性。5.2 去仪器响应时最容易出的错去仪器响应是把数字计数counts转换成真实物理量速度或位移。很多人以为用 ObsPy 的remove_response就万事大吉了实际上这里至少有三个坑必须提供正确的仪器响应文件PZ 或 RESP有些旧台站的响应文件已经失效但数据文件里还保留着过期信息。去响应时要选择合适的输出单位。算震级通常需要位移单位微米但如果你用的是速度波形必须先积分或让remove_response直接输出位移。频率范围要指定合理。如果频率范围定得太宽会把仪器响应没有记录到的频段放大成虚假信号太窄又会滤掉有效信号。我测试过一个台站用默认参数去除响应后噪声放大了几十倍振幅窗口直接被噪声淹没。后来把 lower/frequency 从 0.001 Hz 改成 0.03 Hz 才恢复正常。这种问题只能靠对台站数据本身的了解来调没有万能参数。5.3 ML 和 MW 差很大的排查思路如果算出来的 ML 和 MW 相差超过 0.5先别急着怀疑公式按这个顺序排查检查震中距是否准确。震中距错 10 公里ML 可能就差 0.2。检查振幅单位。模拟伍德-安德森响应后振幅是否已经是微米。检查 MW 拟合的震源谱低频平台是否平直。如果拟合窗口里有明显噪声尖峰MW 很容易偏大。确认 ML 是否已经饱和。如果是 6 级以上的地震ML 偏低是正常的物理现象。我整理了一个排查速查表贴在工具仓库的 README 里每次有同事问类似问题直接甩链接。现象可能原因检查方法所有台站 ML 都偏低未模拟WA响应/单位错误查看振幅测量日志MS 结果跳动大面波测量窗口不稳定重绘窗口波形图确认MW 异常大震源谱拟合窗口含噪声检查频谱图换时间窗单台站震级总偏差台站响应标定过期核实PZ文件时效性5.4 参数调试的几条实操建议不要追求一次调对参数先用 10 个事件跑一遍画出每个事件的波形图和测量窗口肉眼检查一遍再批量处理。每次调整参数保留前后两次的输出报告方便对比到底哪个参数产生了影响。配置项集中在 config.yaml 里不要散落在代码中。我用 YAML 而不是 JSON因为 YAML 支持注释可以在文件里写清楚每个参数的经验取值范围。6. 项目扩展方向6.1 从离线处理到实时流接入现在的版本是批处理模式读文件、算震级、出报告。但地震监测的很多场景需要实时或者准实时处理波形数据源源不断进来每个事件都要在几十秒内产出速报结果。我预留了接口可以把预处理和震级计算函数直接嵌套进流式处理框架里。改造的关键在于波形缓存和事件触发。如果用的是数据流式传输每来一个数据包就喂给 STA/LTA 检测一次触发后自动截取一段时间窗进行计算。这里需要特别注意的是事件重复触发的问题同一个地震可能被多个台站在不同时刻触发需要一个简单的窗口抑制机制避免对同一事件重复计算。6.2 接入机器学习震相拾取STA/LTA 在信噪比高的时候没问题但信号弱或者台站噪声大时拾取精度会明显下降。现在深度学习的震相拾取模型已经比较成熟比如用 PhaseNet 或类似架构对低信噪比数据的效果比传统算法好不少。我考虑过在工具中接入这类模型但目前还没采用。原因不是效果问题而是部署复杂度。深度学习模型需要额外的依赖框架和权重文件会让整个工具变得臃肿。我的计划是保持现有 STA/LTA 作为默认拾取器同时留出phase_picking模块的接口让有需要的人可以切换成模型推理的方式。6.3 从单一震级到震源机制解震级是震源参数的一部分但不是全部。如果已经计算出矩震级 MW其实已经获得了地震矩 M0再往下走一步通过 P 波初动方向或波形拟合可以反演震源机制解也就是断层破裂的模式。这是一个更大的工程超出了 magnitude 工具当前的范围但底层的地震矩估算部分是可以复用的。我对这个工具最满意的地方不是某个算法多么先进而是它把那些看起来基础、但实际执行起来琐碎易错的工作变成了一套规范化的流程。震级计算本身不神秘难的是在数以千计的波形数据面前保证每一次计算都可重复、可追溯、有据可查。7. 一些个人体会项目做到后面我对“magnitude”这个词有了新的感受。在物理课上老师会告诉你 magnitude 就是大小、量级在地震学里它是衡量地下能量释放的标尺但做一个计算工具之后我更愿意把这个词理解为“对同一个事物的不同衡量尺度”。同一份波形数据在 1 秒周期的视角下在 20 秒周期的视角下在整个震源谱的视角下会给出不同的震级数值。这提醒我任何测量结果都依赖于你所选的尺子和视角单一数字永远只能描述事物的一个侧面。如果你也要做类似的震级计算工具我的建议是先把一个台站的一整套流程跑通跑准再扩展到批量处理。不要一上来就把代码写得面面俱到一定要先保留可视化能力让每个环节的结果可以被看到、被检查。这个习惯在后来无数次调试中帮我省下了大量时间。最后再分享一个小技巧处理波形数据时边算边把中间产物滤波后的波形、震相拾取点位、振幅测量窗口保存成轻量级文件。哪怕最后结果完全正确这些中间文件也不要删。等哪一天报告被人质疑的时候你会庆幸当初保留了完整的现场记录。
返回列表