Python中调用SAC

简单示例

下面的脚本展示了如何在 Python 中调用 SAC。

下载地址: 0.simple-script.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os
import subprocess

os.putenv("SAC_DISPLAY_COPYRIGHT", '0')

s = "fg seismo \n"
s += "lh evla kstnm \n"
s += "q \n"
subprocess.Popen(['sac'], stdin=subprocess.PIPE).communicate(s.encode())

Python 中使用 subprocess 模块的 Popen 方法调用 SAC,通过 p.communicate() 将命令 s.encode() 传递给 SAC。

数据转换

首先要将 SEED 格式的数据转换成 SAC 格式。

  • rdseed 一次只能处理一个 SEED 数据

  • rdseed-pdf 选项会提取出 SAC 波形数据和 PZ 格式的仪器响应文件

下载地址: 1.rdseed.py

警告

IRIS 自 2020 年 1 月开始不再提供 SEED 格式的数据下载支持,详情可阅读 IRIS 数据服务通讯。IRIS 也已不再维护 rdseed 软件

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import os
import sys
import glob
import subprocess

if len(sys.argv) != 2:
    sys.exit("Usage: python {} dirname".format(sys.argv[0]))

dirname = sys.argv[1]

os.chdir(dirname)  # cd 进数据所在目录,以避免处理路径

for seed in glob.glob("*.seed"):
    subprocess.call(['rdseed', '-pdf', seed])

os.chdir("..")

文件合并

SEED 文件的波形数据可能会因为多种原因而出现间断,导致同一个通道会解压出来多个 SAC 文件,因而需要将属于同一个通道的 SAC 数据合并起来。

  • 此处使用了新版 merge 命令的语法,要求 SAC 版本大于 v101.6

  • merge 命令还有更多选项可以控制数据合并的细节,见命令的语法介绍

  • 合并后的数据,以最早的数据段的文件名命名

  • 多余的数据段均被删除,以保证每个通道只有一个 SAC 文件

  • 由于脚本运行速度比 SAC 运行速度快,因而应先退出 SAC 再删除多余的数据段

下载地址: 2.merge.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import os
import sys
import glob
import subprocess

os.putenv("SAC_DISPLAY_COPYRIGHT", '0')

if len(sys.argv) != 2:
    sys.exit("python {} dirname".format(sys.argv[0]))

dirname = sys.argv[1]

os.chdir(dirname)

# 利用 dict 的 key 的不可重复性构建集合:
#     dict 的 key 定义为 NET.STA.LOC.CHN
#     dict 的 value 是文件名与 key 匹配的 SAC 文件数目
sets = {}
for fname in glob.glob("*.SAC"):
    key = '.'.join(fname.split('.')[6:10])
    if key not in sets:
        sets[key] = 1
    else:
        sets[key] += 1

# prepare sac commands
s = "wild echo off \n"
to_del = []
for key, value in sets.items():
    # 仅当 value 大于1时才需要 merge
    if value == 1:
        continue

    print("merge {}: {} traces".format(key, value))
    # Python 的 glob 返回值是乱序的,因而必须 sort
    traces = sorted(glob.glob('.'.join(['*', key, '?', 'SAC'])))

    # 在 SAC 中使用通配符而不是使用 @traces 以避免命令行过长的问题
    # merge 不支持通配符
    s += "r *.{}.?.SAC \n".format(key)    # SAC v101.6 or later
    # 缺数据则补0,重叠则做平均
    s += "merge gap zero overlap average \n"
    s += "w {} \n".format(traces[0])      # 以最早数据段的文件名保存

    to_del.extend(traces[1:])

s += "q \n"
subprocess.Popen(['sac'], stdin=subprocess.PIPE).communicate(s.encode())

# 删除多余的数据段
for file in to_del:
    os.unlink(file)

os.chdir("..")

文件重命名

从 SEED 解压出的 SAC 文件名较长,因而对其重命名以简化。

  • SEED 解压出的默认文件名格式为 yyyy.ddd.hh.mm.ss.ffff.NN.SSSSS.LL.CCC.Q.SAC

  • 重命名后的文件名为 NET.STA.LOC.CHN.SAC

下载地址: 3.rename.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import os
import sys
import glob

os.putenv("SAC_DISPLAY_COPYRIGHT", '0')

if len(sys.argv) != 2:
    sys.exit("Usage: python {} dirname\n".format(sys.argv[0]))

dirname = sys.argv[1]

os.chdir(dirname)
for fname in glob.glob("*.SAC"):
    net, sta, loc, chn = fname.split('.')[6:10]
    os.rename(fname, "{}.{}.{}.{}.SAC".format(net, sta, loc, chn))

os.chdir("..")

添加事件信息

若 SEED 中不包含事件信息,则解压得到的 SAC 文件中也不会包含事件信息。因而需要用户手动添加事件的发震时刻、经纬度、深度和震级信息。

  • 输入参数包括:目录名、发震时刻、经度、纬度、深度、震级

  • 发震时刻的格式为 yyyy-mm-ddThh:mm:ss.xxx,其中 T 用于分隔日期和时间

下载地址: 4.eventinfo.py

#!/usr/bin/env python
# -*- coding: utf8 -*-

import os
import sys
import datetime
import subprocess

os.putenv("SAC_DISPLAY_COPYRIGHT", '0')

if len(sys.argv) != 7:
    sys.exit("Usage: python {} dirname yyyy-mm-ddThh:mm:ss.xxx "
             "evlo evla evdp mag".format(sys.argv[0]))

dirname, origin, evlo, evla, evdp, mag = sys.argv[1:]

o = datetime.datetime.strptime(origin, '%Y-%m-%dT%H:%M:%S.%f')
# 计算发震日期是一年中的第几天
jday = o.strftime("%j")
# 提取毫秒值
msec = int(o.microsecond / 1000 + 0.5)

os.chdir(dirname)

s = "wild echo off \n"
s += "r *.SAC \n"
s += "synchronize \n"
s += "ch o gmt {} {} {} {} {} {}\n".format(o.year, jday, o.hour,
                                           o.minute, o.second, msec)
s += "ch allt (0 - &1,o&) iztype IO \n"
s += "ch evlo {} evla {} evdp {} mag {} \n".format(evlo, evla, evdp, mag)
s += "wh \n"
s += "q \n"
subprocess.Popen(['sac'], stdin=subprocess.PIPE).communicate(s.encode())

os.chdir("..")

去仪器响应

使用 PZ 文件去仪器响应。若数据的时间跨度太长,在该时间跨度内可能仪器响应会发生变化,因而会存在一个通道有多个 PZ 文件的情况。目前该脚本在遇到这种情况时会自动退出。

下载地址: 5.transfer.py

#!/usr/bin/env python
# -*- coding: utf8 -*-
import os
import sys
import glob
import subprocess

os.putenv("SAC_DISPLAY_COPYRIGHT", "0")

if len(sys.argv) != 2:
    sys.exit("Usage: python {} dirname\n".format(sys.argv[0]))

dirname = sys.argv[1]

os.chdir(dirname)

s = ""
for sacfile in glob.glob("*.SAC"):
    net, sta, loc, chn = sacfile.split('.')[0:4]
    pz = glob.glob("SAC_PZs_{}_{}_{}_{}_*_*".format(net, sta, chn, loc))
    # 暂不考虑多个PZ文件的情况
    if len(pz) != 1:
        sys.exit("PZ file error for {}".format(sacfile))

    s += "r {} \n".format(sacfile)
    s += "rmean; rtr; taper \n"
    s += "trans from pol s {} to none freq 0.01 0.02 5 10\n".format(pz[0])
    s += "mul 1.0e9 \n"
    s += "w over \n"

s += "q \n"
subprocess.Popen(['sac'], stdin=subprocess.PIPE).communicate(s.encode())

os.chdir("..")

分量旋转

将成对的水平分量旋转到大圆路径。

  • 检查三分量是否缺失

  • 检查 delta 是否相等

  • 取三分量中的最大 b 和最小 e 值作为数据窗口,此操作要求三分量的 kzdatekztime 必须相同,这一点在添加事件信息时使用 synchronize 已经实现

  • 检查两个水平分量是否正交,若不正交,则无法旋转

下载地址: 6.rotate.py

#!/usr/bin/env python
# -*- coding: utf8 -*-

import os
import sys
import glob
import subprocess

os.putenv("SAC_DISPLAY_COPYRIGHT", '0')

if len(sys.argv) != 2:
    sys.exit("Usage: python %s dirname" % sys.argv[0])

dir = sys.argv[1]
os.chdir(dir)

# 构建 NET.STA.LOC.CH 的集合
sets = set()
for fname in glob.glob("*.SAC"):
    net, sta, loc, chn = fname.split('.')[0:4]
    key = '.'.join([net, sta, loc, chn[0:2]])
    sets.add(key)

p = subprocess.Popen(['sac'], stdin=subprocess.PIPE)
s = ""
for key in sets:
    # 检测Z分量是否存在
    Z = key + "Z.SAC"
    if not os.path.exists(Z):
        print("%s: Vertical component missing!" % key)
        continue

    # 检测水平分量是否存在
    if os.path.exists(key + "E.SAC") and os.path.exists(key + "N.SAC"):
        E = key + "E.SAC"
        N = key + "N.SAC"
    elif os.path.exists(key + "1.SAC") and os.path.exists(key + "2.SAC"):
        E = key + "1.SAC"
        N = key + "2.SAC"
    else:
        print("%s: Horizontal components missings!" % key)
        continue

    # 检查水平分量是否正交
    cmd = 'saclst cmpaz f {}'.format(E).split()
    Ecmpaz = subprocess.check_output(cmd).decode().split()[1]
    cmd = 'saclst cmpaz f {}'.format(N).split()
    Ncmpaz = subprocess.check_output(cmd).decode().split()[1]
    cmpaz_delta = abs(float(Ecmpaz) - float(Ncmpaz))
    if not (abs(cmpaz_delta-90) <= 0.01 or abs(cmpaz_delta-270) <= 0.01):
        print("{}: cmpaz1={}, cmpaz2={} "
              "are not orthogonal!".format(key, Ecmpaz, Ncmpaz))
        continue

    # 检查B, E, DELTA
    cmd = 'saclst b e delta f {}'.format(Z).split()
    Zb, Ze, Zdelta = subprocess.check_output(cmd).decode().split()[1:]
    cmd = 'saclst b e delta f {}'.format(E).split()
    Eb, Ee, Edelta = subprocess.check_output(cmd).decode().split()[1:]
    cmd = 'saclst b e delta f {}'.format(N).split()
    Nb, Ne, Ndelta = subprocess.check_output(cmd).decode().split()[1:]

    if not (float(Zdelta) == float(Edelta) and float(Zdelta) == float(Ndelta)):
        print("%s: delta not equal!" % key)
        continue

    # 获取三分量的最大B和最小E值作为数据窗
    begin = max(float(Zb), float(Eb), float(Nb))
    end = min(float(Ze), float(Ee), float(Ne))

    # 输出文件名为 NET.STA.LOC.[RTZ]
    prefix = key[:-2]
    R, T, Z0 = prefix + '.R', prefix + '.T', prefix + '.Z'

    s += "cut %f %f \n" % (begin, end)
    s += "r %s %s \n" % (E, N)
    s += "rotate to gcp \n"
    s += "w %s %s \n" % (R, T)
    s += "r %s \n" % Z
    s += "w %s \n" % Z0
s += "q \n"
p.communicate(s.encode())

# 删除原文件
for fname in glob.glob("*.SAC"):
    os.unlink(fname)

os.chdir("..")

数据重采样

通常有两种情况下需要对数据进行重采样:

  • 原始数据的采样率过高,而实际研究中不需要如此高的采样率,此时,对数据做减采样可以大大减少数据量;

  • 原始数据中,不同台站的采样率不同,可能会影响到后期的数据处理,因而需要让所有数据使用统一的采样率;

下面的Python脚本中使用 interpolate 命令将所有数据重采样到相同的采样周期。用户可以在命令行中直接指定要使用的重采样后的采样周期,若命令行中的采样周期指定为0,则以大多数数据所使用的采样周期作为重采样后的采样周期。

下载地址: 7.resample.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import os
import sys
import glob
import subprocess

os.putenv("SAC_DISPLAY_COPYRIGHT", "0")

if len(sys.argv) != 3:
    sys.exit("Usage: python {} dirname [delta | 0]".format(sys.argv[0]))
# 若第二个参数为0,则取数据中出现次数最多的周期为采样周期

dirname, delta = sys.argv[1], float(sys.argv[2])

os.chdir(dirname)

if delta == 0:
    # 假定所有数据已旋转到RTZ坐标系,文件名格式为NET.STA.LOC.[RTZ]
    # hash的键为出现了的采样周期,其值为出现的个数
    groups = {}
    for fname in glob.glob("*.*.*.[RTZ]"):
        cmd = 'saclst delta f {}'.format(fname).split()
        delta0 = subprocess.check_output(cmd).decode().split()[1]
        if delta0 not in groups:
            groups[delta0] = 1
        else:
            groups[delta0] += 1
    delta = float(max(groups, key=groups.get))

s = ""
for fname in glob.glob("*.*.*.[RTZ]"):
    cmd = 'saclst delta f {}'.format(fname).split()
    delta0 = float(subprocess.check_output(cmd).decode().split()[1])
    if delta == delta0:  # 不需要重采样
        continue

    s += "r {} \n".format(fname)
    # 用 interpolate 实现减采样或增采样
    # 若是减采样,则需要对数据做低通滤波以防止出现混淆效应
    # 低通滤波时或许需要加上 p 2 以避免滤波引起的相移
    if delta > delta0:
        s += "lp c {} \n".format(0.5/delta)
    s += "interpolate delta {} \n".format(delta)
    s += "w over \n"
s += "q \n"
subprocess.Popen(['sac'], stdin=subprocess.PIPE).communicate(s.encode())

os.chdir("..")