diff --git a/docs/source/Advanced/integ_converg/integ_converg.rst b/docs/source/Advanced/integ_converg/integ_converg.rst index 0f2a6b15..500dd032 100644 --- a/docs/source/Advanced/integ_converg/integ_converg.rst +++ b/docs/source/Advanced/integ_converg/integ_converg.rst @@ -29,7 +29,8 @@ :start-after: BEGIN DGRN :end-before: END DGRN - 输出的核函数文件会在自定义路径下。 + 输出的核函数文件会在 :rst:dir:`GRN_grtstats/milrow_{depsrc}_{deprcv}/` 路径下 + (与 C 一致,根目录名由 ``set_dynamic_grn_path`` 的输出目录决定)。 C和Python导出的核函数文件是一致的,底层调用的是相同的函数。文件名称格式为 ``K_{iw}_{freq}``,其中 ``{iw}`` 表示频率索引值, ``{freq}`` 表示对应频率(Hz)。文件为自定义的二进制文件, **强烈建议使用Python进行读取及后续处理**。这里还是给出两种读取方法。 diff --git a/docs/source/Advanced/integ_converg/ptam.rst b/docs/source/Advanced/integ_converg/ptam.rst index 1981b031..05959e2b 100644 --- a/docs/source/Advanced/integ_converg/ptam.rst +++ b/docs/source/Advanced/integ_converg/ptam.rst @@ -40,7 +40,8 @@ :start-after: BEGIN DEPSRC 0.0 DGRN :end-before: END DEPSRC 0.0 DGRN - 输出的核函数文件会在自定义路径下。 + 输出的核函数文件会在 :rst:dir:`GRN_grtstats/milrow_{depsrc}_{deprcv}/` 路径下 + (与 C 一致)。 在 ``K_{iw}_{freq}`` 文件同级目录下,程序把 **PTAM过程中的核函数以及积分峰谷位置分为两个文件** 保存在 ``PTAM_{ir}_{dist}/`` 目录下( ``{ir}`` 为震中距索引, ``{dist}`` 为震中距), diff --git a/docs/source/Advanced/integ_converg/run/run.py b/docs/source/Advanced/integ_converg/run/run.py index a3312316..85936911 100755 --- a/docs/source/Advanced/integ_converg/run/run.py +++ b/docs/source/Advanced/integ_converg/run/run.py @@ -2,19 +2,20 @@ # BEGIN DGRN import numpy as np import pygrt - -modarr = np.loadtxt("milrow") +from pygrt.cli import format_float depsrc = 2.0 deprcv = 0.0 -pymod = pygrt.PyModel1D(modarr, depsrc=depsrc, deprcv=deprcv) +pymod = pygrt.PyModel1D("milrow") +pymod.set_dynamic_grn_path("GRN") -# 通过statsfile参数自定义核函数文件的输出目录, statsidxs指定想输出的频率索引值 +# statsidxs 指定频率索引,核函数写入 GRN_grtstats/{model}_{depsrc}_{deprcv}/ distarr = [5,8,10] -stgrnLst = pymod.compute_grn( +pymod.compute_grn( + depsrc=depsrc, deprcv=deprcv, distarr=distarr, nt=500, dt=0.02, - statsfile=f"pygrtstats_{depsrc}_{deprcv}", statsidxs=[50,100] + statsidxs=[50,100], ) # END DGRN # ------------------------------------------------------------------- @@ -24,7 +25,8 @@ # BEGIN read statsfile # 可使用通配符简化输入,因为对应索引值下只会有一个文件 # 返回的是自定义类型的numpy数组 -statsdata = pygrt.utils.read_statsfile(f"pygrtstats_{depsrc}_{deprcv}/K_0050_*") +statsdir = f"GRN_grtstats/milrow_{format_float(depsrc)}_{format_float(deprcv)}" +statsdata = pygrt.utils.read_statsfile(f"{statsdir}/K_0050_*") print(statsdata.dtype) # [('k', ' stats +grt greenfn -Mmilrow -D0/0 -N500/0.02 -OGRN -R5,8,10 -Cp -K+k2+f+s1.2 -S50,100 # 绘制图像部分见Python # END DEPSRC 0.0 DGRN # ------------------------------------------------------------------- @@ -26,7 +25,7 @@ echo "..." >> ptam_stats_head # ------------------------------------------------------------------- # BEGIN SGRN # -S 表示输出核函数文件 -grt static greenfn -Mmilrow -D0.05/0 -X2/2/1 -Y2/2/1 -Cp -S -Ostgrn.nc +grt static greenfn -Mmilrow -D0.05/0 -X2/2/1 -Y2/2/1 -Cp -K+k3+f -S -Ostgrn.nc # grt.ker2asc 也可以读取静态解输出的核函数文件,格式一致 grt ker2asc stgrtstats/milrow_0.05_0/K > static_stats diff --git a/docs/source/Advanced/k_integ/drift/run/run.py b/docs/source/Advanced/k_integ/drift/run/run.py index c297eb2b..406bf0cb 100755 --- a/docs/source/Advanced/k_integ/drift/run/run.py +++ b/docs/source/Advanced/k_integ/drift/run/run.py @@ -1,15 +1,22 @@ # BEGIN 1 import numpy as np import pygrt +from obspy import read +from pygrt.cli import format_float -modarr = np.loadtxt("milrow") - -pymod = pygrt.PyModel1D(modarr, depsrc=10, deprcv=0.0) +pymod = pygrt.PyModel1D("milrow") +pymod.set_dynamic_grn_path("GRN") +depsrc = 10.0 +deprcv = 0.0 nt = 500 dt = 10 -st_grn = pymod.compute_grn(5000, nt=nt, dt=dt, keepAllFreq=True, statsfile="pygrtstats")[0] +pymod.compute_grn( + depsrc=depsrc, deprcv=deprcv, distarr=[5000], + nt=nt, dt=dt, keepAllFreq=True, statsidxs=[0, 1, 2, 3, 4, 5], +) +st_grn = read("GRN/*/*.sac") # END 1 # 仅绘制一个分量做示例 @@ -85,7 +92,8 @@ def plot_freqs(tr:Trace): # ================================================================= # 读入核函数 import glob -paths = glob.glob("pygrtstats/K_000[0-5]_*") +statsdir = f"GRN_grtstats/milrow_{format_float(depsrc)}_{format_float(deprcv)}" +paths = glob.glob(f"{statsdir}/K_000[0-5]_*") paths.sort() print(paths) @@ -110,7 +118,10 @@ def plot_freqs(tr:Trace): # ================================================================= # 跳过频段,重新计算 -st_grn3 = pymod.compute_grn(5000, nt=nt, dt=dt)[0] +pymod.compute_grn( + depsrc=depsrc, deprcv=deprcv, distarr=[5000], nt=nt, dt=dt, +) +st_grn3 = read("GRN/*/*.sac") srctypes = ['EX', 'VF', 'HF', 'DD', 'DS', 'SS'] chlst = ['Z', 'R', 'T'] @@ -137,3 +148,11 @@ def plot_all_waves(st_grn:Stream): fig = plot_all_waves(st_grn3.copy()) fig.savefig("grn3.svg", bbox_inches='tight') # ================================================================= + +# 删除中间计算结果,仅保留成图 +import shutil +from pathlib import Path +for name in ["GRN", f"GRN_grtstats"]: + p = Path(name) + if p.is_dir(): + shutil.rmtree(p, ignore_errors=True) diff --git a/docs/source/Advanced/k_integ/kmax.rst b/docs/source/Advanced/k_integ/kmax.rst index 24451147..bebb5709 100644 --- a/docs/source/Advanced/k_integ/kmax.rst +++ b/docs/source/Advanced/k_integ/kmax.rst @@ -72,8 +72,8 @@ 积分上限 :func:`compute_static_grn() ` 函数支持以下可选参数来控制波数积分, - 参数与上面对应,具体说明详见API。 + 具体说明详见API。静态解无 ``ampk`` 参数。 - + ``k0:float`` - + ``keps:float`` + + ``k0:float`` + + ``keps:float`` + ``use_kmax_ref:bool`` diff --git a/docs/source/Advanced/k_integ/safim/run/run.py b/docs/source/Advanced/k_integ/safim/run/run.py index d462c3db..135d68ac 100755 --- a/docs/source/Advanced/k_integ/safim/run/run.py +++ b/docs/source/Advanced/k_integ/safim/run/run.py @@ -1,16 +1,24 @@ import numpy as np import pygrt -modarr = np.loadtxt("milrow") +pymod = pygrt.PyModel1D("milrow") +pymod.set_dynamic_grn_path("GRN") -pymod = pygrt.PyModel1D(modarr, 5, 0) - -st_grn = pymod.compute_grn( - distarr=[2500], - nt=2000, - dt=1, - Length=20, +pymod.compute_grn( + depsrc=5.0, + deprcv=0.0, + distarr=[2500], + nt=2000, + dt=1, + Length=20, safilonTol=1e-2, # 自适应采样精度 filonCut=10, delayT0=100, -)[0] +) + +# 删除中间计算结果(成图由 plot.py 负责) +import shutil +from pathlib import Path +p = Path("GRN") +if p.is_dir(): + shutil.rmtree(p, ignore_errors=True) diff --git a/docs/source/Advanced/kernel_old/run/run.py b/docs/source/Advanced/kernel_old/run/run.py index 91f5a8cb..7501ec58 100755 --- a/docs/source/Advanced/kernel_old/run/run.py +++ b/docs/source/Advanced/kernel_old/run/run.py @@ -4,15 +4,22 @@ import matplotlib.pyplot as plt from typing import Union import pygrt +from pygrt.cli import format_float -modarr = np.loadtxt("mod1") +pymod = pygrt.PyModel1D("mod1") +pymod.set_dynamic_grn_path("KERNEL") -pymod = pygrt.PyModel1D(modarr, depsrc=0.03, deprcv=0.0) +depsrc = 0.03 +deprcv = 0.0 -# 不指定statsidx,默认输出全部频率点的积分过程文件 +# 不指定 statsidxs 索引时传空列表,输出全部频率点的积分过程文件 # vmin_ref 显式给定参考速度(用于定义波数积分上限),避免使用PTAM # Length 给定波数积分间隔dk -_ = pymod.compute_grn(distarr=[1], nt=500, dt=0.02, vmin_ref=0.1, Length=20, use_kmax_ref=True, converg_method='none', statsfile="pygrtstats") +pymod.compute_grn( + depsrc=depsrc, deprcv=deprcv, distarr=[1], nt=500, dt=0.02, + vmin_ref=0.1, Length=20, use_kmax_ref=True, converg_method='none', + statsidxs=[], +) # END GRN # ----------------------------------------------------------------- @@ -23,7 +30,8 @@ # 读取所有频率的核函数,并插值到vels # 不指定ktypes,默认返回全部核函数,均以2D数组的形式保存,shape=(nfreqs, nvels) -kerDct = pygrt.utils.read_kernels_freqs("pygrtstats", vels) +statsdir = f"KERNEL_grtstats/mod1_{format_float(depsrc)}_{format_float(deprcv)}" +kerDct = pygrt.utils.read_kernels_freqs(statsdir, vels) print(kerDct.keys()) # dict_keys(['_vels', '_freqs', 'EX_q', 'EX_w', 'VF_q', 'VF_w', 'HF_q', 'HF_w', 'HF_v', 'DD_q', 'DD_w', 'DS_q', 'DS_w', 'DS_v', 'SS_q', 'SS_w', 'SS_v']) # END read @@ -78,3 +86,11 @@ def plot_kernel(kerDct:dict, RorI:bool, out:Union[str,None]=None): plot_kernel(kerDct, True, "real.svg") # END plot # ----------------------------------------------------------------- + +# 删除中间计算结果,仅保留成图 +import shutil +from pathlib import Path +for name in ["KERNEL", "KERNEL_grtstats"]: + p = Path(name) + if p.is_dir(): + shutil.rmtree(p, ignore_errors=True) diff --git a/docs/source/Advanced/kernel_old/run/run.sh b/docs/source/Advanced/kernel_old/run/run.sh index 5542607e..27f1cc07 100755 --- a/docs/source/Advanced/kernel_old/run/run.sh +++ b/docs/source/Advanced/kernel_old/run/run.sh @@ -9,7 +9,7 @@ rm -rf GRN* pygrtstats* *.svg # -S 后不指定索引表示输出所有频率点的核函数 # -Cn 禁用收敛算法 # -L20 定义波数积分间隔dk -grt greenfn -Mmod1 -D0.03/0 -N500/0.02 -OGRN -R1 -K+v0.1 -S -L20 -Cn +grt greenfn -Mmod1 -D0.03/0 -N500/0.02 -OGRN -R1 -K+v0.1+f -S -L20 -Cn # END GRN # ----------------------------------------------------------------- diff --git a/docs/source/Gallery/ex01/ex01.rst b/docs/source/Gallery/ex01/ex01.rst index 86a0787e..7a6d238e 100644 --- a/docs/source/Gallery/ex01/ex01.rst +++ b/docs/source/Gallery/ex01/ex01.rst @@ -7,7 +7,7 @@ 下载示例: :download:`ex01.tar.gz` 在 MILROW 模型下,震源深度 5 km,震源为剪切源,走向、倾角、滑动角分别为 77°、88°、99°, -计算震中距为 100 km ,方位角为 39.2 ° 的地面台站记录到的三分量理论记录。 +计算震中距为 180 km ,方位角为 39.2 ° 的地面台站记录到的三分量理论记录。 此图也是 `PyGRT 代码主页 `_ 中显示的示例图。 diff --git a/docs/source/Gallery/ex01/run.py b/docs/source/Gallery/ex01/run.py index 121fd3cc..82b1eb55 100644 --- a/docs/source/Gallery/ex01/run.py +++ b/docs/source/Gallery/ex01/run.py @@ -4,10 +4,17 @@ from typing import Union import pygrt -modarr = np.loadtxt("milrow") -pymod = pygrt.PyModel1D(modarr, 5, 0) -stgrn = pymod.compute_grn(distarr=[180], nt=1400, dt=0.1)[0] -st = pygrt.utils.gen_syn_from_gf_DC(stgrn, M0=1e24, strike=77, dip=88, rake=99, az=39.2) +pymod = pygrt.PyModel1D("milrow") +pymod.set_dynamic_grn_path("GRN") +pymod.compute_grn(depsrc=5.0, deprcv=0.0, distarr=[180], nt=1400, dt=0.1) +# ?.sac 匹配位移三分量文件名(Z/R/T) +# integrate_order=1 对应 CLI -I1,得到阶跃型位移 +pymod.compute_syn( + dist=180.0, azimuth=39.2, scale=1e24, output_path="syn_dc", + source="DC", strike=77, dip=88, rake=99, + integrate_order=1, +) +st = read("syn_dc/?.sac") def plot_syn(stsyn:Stream, sigs:Union[np.ndarray,None]=None): figsize = (10, 5.5) @@ -31,7 +38,7 @@ def plot_syn(stsyn:Stream, sigs:Union[np.ndarray,None]=None): for i, comp in enumerate(['R', 'T', 'Z']): ax = axs[i] - tr = st.select(component=comp)[0] + tr = stsyn.select(channel=comp)[0] ax.plot(t, tr.data, c='k', lw=0.5, label=tr.stats.channel[-1]) ax.legend(loc='upper left') @@ -48,6 +55,15 @@ def plot_syn(stsyn:Stream, sigs:Union[np.ndarray,None]=None): return fig, axs -pygrt.utils.stream_integral(st) fig, axs = plot_syn(st) -fig.savefig('cover.svg', bbox_inches='tight') \ No newline at end of file +fig.savefig('cover.svg', bbox_inches='tight') + +# 删除中间计算结果,仅保留成图 +import shutil +from pathlib import Path +for name in ["GRN", "syn_dc"]: + p = Path(name) + if p.is_dir(): + shutil.rmtree(p, ignore_errors=True) + elif p.is_file(): + p.unlink(missing_ok=True) diff --git a/docs/source/Gallery/ex03/plot_cps_pygrt.py b/docs/source/Gallery/ex03/plot_cps_pygrt.py index 9b13f7bf..a52bd945 100644 --- a/docs/source/Gallery/ex03/plot_cps_pygrt.py +++ b/docs/source/Gallery/ex03/plot_cps_pygrt.py @@ -4,12 +4,10 @@ from obspy import * -# load model -modarr = np.loadtxt("./milrow") - depsrc = 2.0 deprcv = 0 -pymod = pygrt.PyModel1D(modarr, depsrc, deprcv) +pymod = pygrt.PyModel1D("./milrow") +pymod.set_dynamic_grn_path("GRN_pygrt") rs = np.array([10]) @@ -18,13 +16,17 @@ zeta = 0.8 # compute Green's Functions -st_grt = pymod.compute_grn( - distarr=rs, - nt=nt, - dt=dt, - zeta=zeta, +pymod.compute_grn( + depsrc=depsrc, + deprcv=deprcv, + distarr=rs, + nt=nt, + dt=dt, + zeta=zeta, Length=20, -)[0] +) +# 仅一个震中距,可用通配符读回 +st_grt = read("GRN_pygrt/*/*.sac") try: @@ -34,3 +36,10 @@ plot(st_grt, st_cps, "compare_cps_pygrt.svg") except Exception as e: print(str(e)) + +# 删除中间计算结果,仅保留成图 +import shutil +from pathlib import Path +p = Path("GRN_pygrt") +if p.is_dir(): + shutil.rmtree(p, ignore_errors=True) diff --git a/docs/source/Gallery/ex04/ex04.rst b/docs/source/Gallery/ex04/ex04.rst index 307f5a46..57f75ec3 100644 --- a/docs/source/Gallery/ex04/ex04.rst +++ b/docs/source/Gallery/ex04/ex04.rst @@ -7,7 +7,7 @@ 下载示例: :download:`ex04.tar.gz` 在 MILROW 模型下,震源深度 2 km,震源为剪切源,走向、倾角、滑动角分别为 77°、88°、99°, -计算震中距为 100 km ,方位角为 30 ° 的地面台站记录到的动态应力、应变、旋转张量。 +计算震中距为 10 km ,方位角为 30 ° 的地面台站记录到的动态应力、应变、旋转张量。 应变张量 ~~~~~~~~~~~~~~ diff --git a/docs/source/Gallery/ex05/run.py b/docs/source/Gallery/ex05/run.py index 9c66e944..377c2c32 100644 --- a/docs/source/Gallery/ex05/run.py +++ b/docs/source/Gallery/ex05/run.py @@ -13,23 +13,30 @@ modname="milrow" -modarr = np.loadtxt(modname) - -pymod = pygrt.PyModel1D(modarr, depsrc, deprcv) +pymod = pygrt.PyModel1D(modname) +pymod.set_dynamic_grn_path("GRN") # compute green functions -st_grn = pymod.compute_grn(dist, nt, dt)[0] +pymod.compute_grn( + depsrc=depsrc, deprcv=deprcv, distarr=[dist], nt=nt, dt=dt, +) def plot_syn(stsyn:Stream, out:Union[str,None]=None, sigs:Union[np.ndarray,None]=None): + traces = list(stsyn) + order = {ch: i for i, ch in enumerate("ZRTNE")} + traces.sort(key=lambda tr: order.get(tr.stats.channel, 99)) + figsize = (10, 4) - nrow = 3 + nrow = len(traces) if sigs is not None: nrow += 1 figsize = (10, 4.5) fig, axs = plt.subplots(nrow, 1, figsize=figsize, gridspec_kw=dict(hspace=0.0), sharex=True) - nt = stsyn[0].stats.npts - dt = stsyn[0].stats.delta + if nrow == 1: + axs = [axs] + nt = traces[0].stats.npts + dt = traces[0].stats.delta t = np.arange(nt)*dt if sigs is not None: @@ -37,10 +44,10 @@ def plot_syn(stsyn:Stream, out:Union[str,None]=None, sigs:Union[np.ndarray,None] ax.plot(t[:len(sigs)], sigs, 'k-', lw=0.5) axs = axs[1:] - travtP = stsyn[0].stats.sac['t0'] - travtS = stsyn[0].stats.sac['t1'] + travtP = traces[0].stats.sac['t0'] + travtS = traces[0].stats.sac['t1'] - for i, tr in enumerate(stsyn): + for i, tr in enumerate(traces): ax = axs[i] ax.plot(t, tr.data, c='k', lw=0.5, label=tr.stats.channel) ax.legend(loc='upper left') @@ -60,24 +67,51 @@ def plot_syn(stsyn:Stream, out:Union[str,None]=None, sigs:Union[np.ndarray,None] fig.savefig(out, bbox_inches='tight') # synthetic +# ?.sac 匹配位移三分量文件名(Z/R/T) +# time_function 对应 CLI -D,卷积用的时间函数保存在输出目录的 sig.sac S=1e24 az=39.2 -st = pygrt.utils.gen_syn_from_gf_EX(st_grn, S, az) -sigs = pygrt.sigs.gen_triangle_wave(0.4, dt) -pygrt.utils.stream_convolve(st, sigs) +pymod.compute_syn( + dist=dist, azimuth=az, scale=S, output_path="syn_ex", source="EX", + time_function="t/0.2/0.2/0.4", +) +st = read("syn_ex/?.sac") +sigs = read("syn_ex/sig.sac")[0].data plot_syn(st, "trig.svg", sigs) -st = pygrt.utils.gen_syn_from_gf_SF(st_grn, S, 2, -1, 4, az) -sigs = pygrt.sigs.gen_trap_wave(0.1, 0.3, 0.6, dt) -pygrt.utils.stream_convolve(st, sigs) +pymod.compute_syn( + dist=dist, azimuth=az, scale=S, output_path="syn_sf", source="SF", + force=(2, -1, 4), + time_function="t/0.1/0.3/0.6", +) +st = read("syn_sf/?.sac") +sigs = read("syn_sf/sig.sac")[0].data plot_syn(st, "trap.svg", sigs) -st = pygrt.utils.gen_syn_from_gf_DC(st_grn, S, 77, 88, 99, az) -sigs = pygrt.sigs.gen_parabola_wave(0.6, dt) -pygrt.utils.stream_convolve(st, sigs) +pymod.compute_syn( + dist=dist, azimuth=az, scale=S, output_path="syn_dc", source="DC", + strike=77, dip=88, rake=99, + time_function="p/0.6", +) +st = read("syn_dc/?.sac") +sigs = read("syn_dc/sig.sac")[0].data plot_syn(st, "para.svg", sigs) -st = pygrt.utils.gen_syn_from_gf_MT(st_grn, S, [1,-2,-5,0.5,3,1.2], az) -sigs = pygrt.sigs.gen_ricker_wave(3, dt) -pygrt.utils.stream_convolve(st, sigs) +pymod.compute_syn( + dist=dist, azimuth=az, scale=S, output_path="syn_mt", source="MT", + moment_tensor=(1, -2, -5, 0.5, 3, 1.2), + time_function="r/3", +) +st = read("syn_mt/?.sac") +sigs = read("syn_mt/sig.sac")[0].data plot_syn(st, "rick.svg", sigs) + +# 删除中间计算结果,仅保留成图 +import shutil +from pathlib import Path +for name in ["GRN", "syn_ex", "syn_sf", "syn_dc", "syn_mt"]: + p = Path(name) + if p.is_dir(): + shutil.rmtree(p, ignore_errors=True) + elif p.is_file(): + p.unlink(missing_ok=True) diff --git a/docs/source/Gallery/ex06/ex06.rst b/docs/source/Gallery/ex06/ex06.rst index 23126a89..91435425 100644 --- a/docs/source/Gallery/ex06/ex06.rst +++ b/docs/source/Gallery/ex06/ex06.rst @@ -6,7 +6,7 @@ 下载示例: :download:`ex06.tar.gz` -在 MILROW 模型下,震源深度 2 km,计算震中距为 1800 km 的地面台站的格林函数(卷积阶跃函数)。 +在 MILROW 模型下,震源深度 2 km,计算震中距为 1800 km 的地面台站的格林函数。 .. figure:: safim.svg :align: center diff --git a/docs/source/Gallery/ex14/plot.py b/docs/source/Gallery/ex14/plot.py index 76cf861d..e53d7ce8 100644 --- a/docs/source/Gallery/ex14/plot.py +++ b/docs/source/Gallery/ex14/plot.py @@ -1,6 +1,8 @@ import pygrt import numpy as np import matplotlib.pyplot as plt +from pathlib import Path +from obspy import read # 定义半无限空间模型 Vp = 8 # km/s @@ -28,11 +30,26 @@ t = np.arange(0, nt)*dt * Vs/r -pymod = pygrt.PyModel1D(modarr, depsrc, deprcv) # 整理好的模型对象 -# 计算格林函数 -st_none = pymod.compute_grn(distarr=rs, nt=nt, dt=dt, converg_method='none')[0] -st_dcm = pymod.compute_grn(distarr=rs, nt=nt, dt=dt, converg_method='DCM')[0] -st_ptam = pymod.compute_grn(distarr=rs, nt=nt, dt=dt, converg_method='PTAM')[0] +modfile = "_halfspace_mod" +np.savetxt(modfile, modarr) +pymod = pygrt.PyModel1D(modfile) +pymod.set_dynamic_grn_path("GRN") +# 计算格林函数(仅一个震中距,可用通配符读回) +pymod.compute_grn( + depsrc=depsrc, deprcv=deprcv, distarr=rs, nt=nt, dt=dt, + converg_method='none', +) +st_none = read("GRN/*/*.sac") +pymod.compute_grn( + depsrc=depsrc, deprcv=deprcv, distarr=rs, nt=nt, dt=dt, + converg_method='DCM', +) +st_dcm = read("GRN/*/*.sac") +pymod.compute_grn( + depsrc=depsrc, deprcv=deprcv, distarr=rs, nt=nt, dt=dt, + converg_method='PTAM', +) +st_ptam = read("GRN/*/*.sac") # 卷积阶跃函数 pygrt.utils.stream_integral(st_none) @@ -71,4 +88,13 @@ axs[0,1].set_title("Apply DCM") axs[0,2].set_title("Apply PTAM") -fig.savefig("lamb1_compare.svg", bbox_inches='tight') \ No newline at end of file +fig.savefig("lamb1_compare.svg", bbox_inches='tight') + +# 删除中间计算结果,仅保留成图 +import shutil +for name in ["GRN", "_halfspace_mod"]: + p = Path(name) + if p.is_dir(): + shutil.rmtree(p, ignore_errors=True) + elif p.is_file(): + p.unlink(missing_ok=True) diff --git a/docs/source/Gallery/ex15/plot.py b/docs/source/Gallery/ex15/plot.py index 4ae1ae3c..aa9609bf 100644 --- a/docs/source/Gallery/ex15/plot.py +++ b/docs/source/Gallery/ex15/plot.py @@ -2,21 +2,38 @@ import pygrt import matplotlib.pyplot as plt import sys +from pathlib import Path +from obspy import read modname = sys.argv[1] -modarr = np.loadtxt(modname) deprcv = 0.0 depsrc = 5.0 distarr = np.arange(0.1, 50, 1) -pymod = pygrt.PyModel1D(modarr, depsrc, deprcv) +pymod = pygrt.PyModel1D(modname) +pymod.set_dynamic_grn_path("GRN") +pymod.set_static_grn_path("stgrn.nc") -# 零频频谱, 此时 dt,zeta 变相用于控制虚频率的大小 -pygrnLst, _, _ = pymod._get_grn_spectra(distarr, nt=1, dt=100, zeta=1.0, keepAllFreq=True) +# 零频频谱: nt=1 时只算 ω=0 +# C 反变换写 SAC 时乘了 df=1/(nt*dt),故时域首点 * dt 即还原频域幅值 +dt = 100.0 +pymod.compute_grn( + depsrc=depsrc, deprcv=deprcv, distarr=distarr.tolist(), + nt=1, dt=dt, zeta=1.0, keepAllFreq=True, +) +# 多震中距:按子目录名末段解析 dist,再按 distarr 顺序读入 +dist2st = { + float(p.name.rsplit("_", 1)[-1]): read(str(p / "*.sac")) + for p in Path("GRN").iterdir() if p.is_dir() +} +stgrnLst = [dist2st[float(d)] for d in distarr] # 静态解 -static_grn = pymod.compute_static_grn(norths=[0.0], easts=distarr) +pymod.compute_static_grn( + depsrc=depsrc, deprcv=deprcv, distarr=distarr.tolist(), +) +static_grn = pygrt.utils.read_static_nc("stgrn.nc") # 绘制 coef = 1e-20 * 1e25 # 1e25 为地震矩 @@ -25,15 +42,16 @@ fig, ax = plt.subplots(figsize=(6,6)) dynamic_Z = np.zeros_like(distarr) dynamic_R = np.zeros_like(distarr) -for i in range(len(distarr)): - dynamic_Z[i] = np.real(pygrnLst[i][0][0].cmplx_grn[0]) * coef * (-1) - dynamic_R[i] = np.real(pygrnLst[i][0][1].cmplx_grn[0]) * coef +for i, st in enumerate(stgrnLst): + # SAC 中 Z 已取反为向上为正,无需再乘 -1 + dynamic_Z[i] = st.select(channel='EXZ')[0].data[0] * dt * coef + dynamic_R[i] = st.select(channel='EXR')[0].data[0] * dt * coef ax.plot(distarr, dynamic_Z, 'k', label='Dynamic Z') ax.plot(distarr, dynamic_R, 'k', label='Dynamic R') -ax.plot(distarr, static_grn['EXZ'][0] * coef, 'ro', ms=4, label='Static Z') -ax.plot(distarr, static_grn['EXR'][0] * coef, 'bo', ms=4, label='Static R') +ax.plot(distarr, static_grn['variables']['EXZ']['data'][0] * coef, 'ro', ms=4, label='Static Z') +ax.plot(distarr, static_grn['variables']['EXR']['data'][0] * coef, 'bo', ms=4, label='Static R') ax.set_xlim(0, 50) ax.set_xlabel('Distance (km)') @@ -43,4 +61,13 @@ ax.legend(loc='upper right') ax.grid() -fig.savefig(f'{modname}.svg', bbox_inches='tight') \ No newline at end of file +fig.savefig(f'{modname}.svg', bbox_inches='tight') + +# 删除中间计算结果,仅保留成图 +import shutil +for name in ["GRN", "stgrn.nc"]: + p = Path(name) + if p.is_dir(): + shutil.rmtree(p, ignore_errors=True) + elif p.is_file(): + p.unlink(missing_ok=True) diff --git a/docs/source/Gallery/ex15/plot_all.py b/docs/source/Gallery/ex15/plot_all.py index 4a990add..855967d9 100644 --- a/docs/source/Gallery/ex15/plot_all.py +++ b/docs/source/Gallery/ex15/plot_all.py @@ -2,27 +2,45 @@ import pygrt import matplotlib.pyplot as plt import sys +from pathlib import Path +from obspy import read modname = sys.argv[1] -modarr = np.loadtxt(modname) deprcv = 0.0 depsrc = 5.0 distarr = np.arange(0.1, 50, 0.5) -pymod = pygrt.PyModel1D(modarr, depsrc, deprcv) +pymod = pygrt.PyModel1D(modname) +pymod.set_dynamic_grn_path("GRN") +pymod.set_static_grn_path("stgrn.nc") -# 零频频谱, 此时 dt,zeta 变相用于控制虚频率的大小 -pygrnLst, _, _ = pymod._get_grn_spectra(distarr, nt=1, dt=500, zeta=1.0, keepAllFreq=True) +# 零频频谱: nt=1 时只算 ω=0 +# C 反变换写 SAC 时乘了 df=1/(nt*dt),故时域首点 * dt 即还原频域幅值 +dt = 500.0 +pymod.compute_grn( + depsrc=depsrc, deprcv=deprcv, distarr=distarr.tolist(), + nt=1, dt=dt, zeta=1.0, keepAllFreq=True, +) +# 多震中距:按子目录名末段解析 dist,再按 distarr 顺序读入 +dist2st = { + float(p.name.rsplit("_", 1)[-1]): read(str(p / "*.sac")) + for p in Path("GRN").iterdir() if p.is_dir() +} +stgrnLst = [dist2st[float(d)] for d in distarr] # 静态解 -static_grn = pymod.compute_static_grn(norths=[0.0], easts=distarr) +pymod.compute_static_grn( + depsrc=depsrc, deprcv=deprcv, distarr=distarr.tolist(), +) +static_grn = pygrt.utils.read_static_nc("stgrn.nc") # 绘制零频结果 fig, axs = plt.subplots(2, 3, figsize=(12, 8), gridspec_kw=dict(hspace=0.3, wspace=0.3)) # axs = axs.ravel() -for isrc, (src, src2) in enumerate(zip(['EX', 'VF', 'HF', 'DD', 'DS', 'SS'], +srctypes = ['EX', 'VF', 'HF', 'DD', 'DS', 'SS'] +for isrc, (src, src2) in enumerate(zip(srctypes, ['Explosion', 'Vertical Force', 'Horizontal Force', '45°-Dip Slip', '90°-Dip Slip', 'Strike Slip'])): ax = axs[isrc] @@ -34,20 +52,22 @@ dynamic_Z = np.zeros_like(distarr) dynamic_R = np.zeros_like(distarr) dynamic_T = np.zeros_like(distarr) - for i in range(len(distarr)): - dynamic_Z[i] = np.real(pygrnLst[i][isrc][0].cmplx_grn[0]) * coef * (-1) - dynamic_R[i] = np.real(pygrnLst[i][isrc][1].cmplx_grn[0]) * coef - dynamic_T[i] = np.real(pygrnLst[i][isrc][2].cmplx_grn[0]) * coef + for i, st in enumerate(stgrnLst): + # SAC 中 Z 已取反为向上为正,无需再乘 -1 + dynamic_Z[i] = st.select(channel=f'{src}Z')[0].data[0] * dt * coef + dynamic_R[i] = st.select(channel=f'{src}R')[0].data[0] * dt * coef + if src not in ['EX', 'VF', 'DD']: + dynamic_T[i] = st.select(channel=f'{src}T')[0].data[0] * dt * coef ms = 2 lw = 0.8 ax.plot(distarr, dynamic_Z, 'k', lw=lw, label='Dynamic Z') - ax.plot(distarr, static_grn[f'{src}Z'][0] * coef, 'ro', ms=ms, label='Static Z') + ax.plot(distarr, static_grn['variables'][f'{src}Z']['data'][0] * coef, 'ro', ms=ms, label='Static Z') ax.plot(distarr, dynamic_R, 'k', lw=lw, label='Dynamic R') - ax.plot(distarr, static_grn[f'{src}R'][0] * coef, 'bo', ms=ms, label='Static R') + ax.plot(distarr, static_grn['variables'][f'{src}R']['data'][0] * coef, 'bo', ms=ms, label='Static R') if src not in ['EX', 'VF', 'DD']: ax.plot(distarr, dynamic_T, 'k', lw=lw, label='Dynamic T') - ax.plot(distarr, static_grn[f'{src}T'][0] * coef, 'go', ms=ms, label='Static T') + ax.plot(distarr, static_grn['variables'][f'{src}T']['data'][0] * coef, 'go', ms=ms, label='Static T') ax.set_xlim(0, 50) ax.set_xlabel('Distance (km)') @@ -61,4 +81,13 @@ bbox_transform=fig.transFigure) -fig.savefig(f'{modname}_all.svg', bbox_inches='tight') \ No newline at end of file +fig.savefig(f'{modname}_all.svg', bbox_inches='tight') + +# 删除中间计算结果,仅保留成图 +import shutil +for name in ["GRN", "stgrn.nc"]: + p = Path(name) + if p.is_dir(): + shutil.rmtree(p, ignore_errors=True) + elif p.is_file(): + p.unlink(missing_ok=True) diff --git a/docs/source/Gallery/ex16/plot.py b/docs/source/Gallery/ex16/plot.py index 2c0aecf1..84c26fa1 100644 --- a/docs/source/Gallery/ex16/plot.py +++ b/docs/source/Gallery/ex16/plot.py @@ -3,8 +3,12 @@ import matplotlib.pyplot as plt from matplotlib.figure import Figure import sys +from pathlib import Path +from obspy import read easts = np.arange(0.01, 10.01, 0.01) +easts_rng = [0.01, 10.0, 0.01] +norths_rng = [0.0, 0.0, 1.0] rs = np.array([10]) # 震中距数组,km nt = 801 # 总点数,不要求2的幂次 dt = 0.01 # 采样时间间隔(s) @@ -29,10 +33,22 @@ [2.8, Vp+0.4, Vs+0.4, Rho+0.2], [0, Vp+0.6, Vs+0.6, Rho+0.3], ]) -pymod1 = pygrt.PyModel1D(modarr, depsrc, deprcv, topbound=bound1, botbound=bound2) -st1 = pymod1.compute_grn(distarr=rs, nt=nt, dt=dt, keepAllFreq=True)[0] +modfile1 = f"mod_{bound1}_{bound2}" +np.savetxt(modfile1, modarr) +pymod1 = pygrt.PyModel1D(modfile1, topbound=bound1, botbound=bound2) +pymod1.set_dynamic_grn_path("GRN1") +pymod1.set_static_grn_path("stgrn1.nc") +pymod1.compute_grn( + depsrc=depsrc, deprcv=deprcv, distarr=rs, nt=nt, dt=dt, + keepAllFreq=True, +) +st1 = read("GRN1/*/*.sac") pygrt.utils.stream_integral(st1) -static1 = pymod1.compute_static_grn(norths=[0.0], easts=easts) +pymod1.compute_static_grn( + depsrc=depsrc, deprcv=deprcv, + norths=norths_rng, easts=easts_rng, +) +static1 = pygrt.utils.read_static_nc("stgrn1.nc") # ============================================================= # 设置上下翻转模型 @@ -52,9 +68,21 @@ print(modarr2) print(modarr2.shape, depsrc2, deprcv2) -pymod2 = pygrt.PyModel1D(modarr2, depsrc2, deprcv2, topbound=bound2, botbound=bound1) # 整理好的模型对象 -st2 = pymod2.compute_grn(distarr=rs, nt=nt, dt=dt, keepAllFreq=True)[0] -static2 = pymod2.compute_static_grn(norths=[0.0], easts=easts) +modfile2 = f"mod_{bound2}_{bound1}" +np.savetxt(modfile2, modarr2) +pymod2 = pygrt.PyModel1D(modfile2, topbound=bound2, botbound=bound1) +pymod2.set_dynamic_grn_path("GRN2") +pymod2.set_static_grn_path("stgrn2.nc") +pymod2.compute_grn( + depsrc=depsrc2, deprcv=deprcv2, distarr=rs, nt=nt, dt=dt, + keepAllFreq=True, +) +st2 = read("GRN2/*/*.sac") +pymod2.compute_static_grn( + depsrc=depsrc2, deprcv=deprcv2, + norths=norths_rng, easts=easts_rng, +) +static2 = pygrt.utils.read_static_nc("stgrn2.nc") pygrt.utils.stream_integral(st2) @@ -87,8 +115,8 @@ ax.set_ymargin(0.3) ax = axs2[i] - ax.plot(easts, static1[chLst[i]][0], **prop1) - ax.plot(easts, static2[chLst[i]][0] * sgn, **prop2) + ax.plot(easts, static1['variables'][chLst[i]]['data'][0], **prop1) + ax.plot(easts, static2['variables'][chLst[i]]['data'][0] * sgn, **prop2) ax.text(0.96, 0.9, chLst[i], transform=ax.transAxes, ha='right', va='top', bbox=dict(fc='w')) ax.ticklabel_format(axis='y', style='sci', scilimits=(0,0)) @@ -114,4 +142,12 @@ # fig.savefig(f"test_flip1.svg", bbox_inches='tight') fig.savefig(f"{bound1}_{bound2}.svg", bbox_inches='tight') +# 删除中间计算结果,仅保留成图 +import shutil +for name in [modfile1, modfile2, "GRN1", "GRN2", "stgrn1.nc", "stgrn2.nc"]: + p = Path(name) + if p.is_dir(): + shutil.rmtree(p, ignore_errors=True) + elif p.is_file(): + p.unlink(missing_ok=True) diff --git a/docs/source/Lamb_problem/run/lamb1_plot_freq_time.py b/docs/source/Lamb_problem/run/lamb1_plot_freq_time.py index 7ef37410..cc47c59f 100644 --- a/docs/source/Lamb_problem/run/lamb1_plot_freq_time.py +++ b/docs/source/Lamb_problem/run/lamb1_plot_freq_time.py @@ -1,6 +1,8 @@ import pygrt import numpy as np import matplotlib.pyplot as plt +from pathlib import Path +from obspy import read plt.rcParams.update({ "font.sans-serif": "Times New Roman", @@ -36,14 +38,15 @@ t = np.arange(0, nt)*dt * Vs/r - -pymod = pygrt.PyModel1D(modarr, depsrc, deprcv) # 整理好的模型对象 -# 计算格林函数 -st = pymod.compute_grn( - distarr=rs, - nt=nt, - dt=dt, -)[0] +modfile = "_halfspace_mod" +np.savetxt(modfile, modarr) +pymod = pygrt.PyModel1D(modfile) +pymod.set_dynamic_grn_path("GRN") +# 计算格林函数(仅一个震中距,可用通配符读回) +pymod.compute_grn( + depsrc=depsrc, deprcv=deprcv, distarr=rs, nt=nt, dt=dt, +) +st = read("GRN/*/*.sac") # 卷积阶跃函数 pygrt.utils.stream_integral(st) @@ -83,4 +86,13 @@ axs[0,0].set_title("From Time-Domain") axs[0,1].set_title("From Frequency-Domain") -fig.savefig("lamb1_compare_freq_time.svg", bbox_inches='tight') \ No newline at end of file +fig.savefig("lamb1_compare_freq_time.svg", bbox_inches='tight') + +# 删除中间计算结果,仅保留成图 +import shutil +for name in ["GRN", "_halfspace_mod"]: + p = Path(name) + if p.is_dir(): + shutil.rmtree(p, ignore_errors=True) + elif p.is_file(): + p.unlink(missing_ok=True) diff --git a/docs/source/Lamb_problem/run/run.sh b/docs/source/Lamb_problem/run/run.sh index aa4fdbf0..bca17b86 100755 --- a/docs/source/Lamb_problem/run/run.sh +++ b/docs/source/Lamb_problem/run/run.sh @@ -2,7 +2,7 @@ set -euo pipefail -rm -f *.svg +rm -rf *.svg # ----------------------------------------------------------------------------------- # BEGIN LAMB1 diff --git a/docs/source/Tutorial/dynamic/gfunc.rst b/docs/source/Tutorial/dynamic/gfunc.rst index 78982f66..cb30131a 100644 --- a/docs/source/Tutorial/dynamic/gfunc.rst +++ b/docs/source/Tutorial/dynamic/gfunc.rst @@ -4,7 +4,7 @@ 计算动态格林函数 ================= -Python中计算动态格林函数的主函数为 :func:`compute_grn() ` ,C模块为 :doc:`/Module/greenfn`。 +Python中计算动态格林函数的主函数为 :meth:`compute_grn() ` ,C模块为 :doc:`/Module/greenfn`。 核心计算逻辑来自 |yao2026p| ,具体代码可见与C API中对应同名 :file:`*.c` 文件,其中计算格林函数频谱的主函数为 :file:`grn.c` 里的 :c:func:`grt_integ_grn_spec`。输出结果的坐标系见下图。 @@ -69,7 +69,9 @@ Python中计算动态格林函数的主函数为 :func:`compute_grn() ` + 指定的根目录。需要读回时使用 ObsPy 的 ``read`` 。 :class:`Trace.stats.sac` + 中保存了 SAC 头段变量,与 C 程序输出保持一致。 当时窗长度 nt\*dt 太小“包不住”有效信号,或时窗长度足够但时延不合适,输出的波形会发生混叠, 此时需调整相关参数。 diff --git a/docs/source/Tutorial/dynamic/index.rst b/docs/source/Tutorial/dynamic/index.rst index 4e392fc6..539563c3 100644 --- a/docs/source/Tutorial/dynamic/index.rst +++ b/docs/source/Tutorial/dynamic/index.rst @@ -13,10 +13,10 @@ flowchart TB GG(["compute_grn()"]) - SS(["gen_syn_from_gf_*()"]) - EE(["compute_strain()"]) - RR(["compute_rotation()"]) - TT(["compute_stress()"]) + SS(["compute_syn()"]) + EE(["utils.compute_strain()"]) + RR(["utils.compute_rotation()"]) + TT(["utils.compute_stress()"]) G["Compute Green's Functions (and its Spatial Derivatives)"] diff --git a/docs/source/Tutorial/dynamic/run/run.py b/docs/source/Tutorial/dynamic/run/run.py index 6ad3d1ea..563b35e6 100755 --- a/docs/source/Tutorial/dynamic/run/run.py +++ b/docs/source/Tutorial/dynamic/run/run.py @@ -1,23 +1,11 @@ # ----------------------------------------------------------------------------------- # START BUILD MODEL import numpy as np -import pygrt - -# option 1: -# modarr = np.loadtxt("milrow") - -# option 2 -modarr = np.array([ - [0.2, 3.4, 1.7, 2.3, 9e10, 9e10], - [0.6, 3.7, 1.9, 2.4, 9e10, 9e10], - [0.5, 4.2, 2.1, 2.4, 9e10, 9e10], - [0.5, 4.6, 2.3, 2.5, 9e10, 9e10], - [0.7, 4.9, 2.8, 2.6, 9e10, 9e10], - [0.5, 5.1, 2.9, 2.7, 9e10, 9e10], - [6.0, 5.9, 3.3, 2.7, 9e10, 9e10], - [28., 6.9, 4.0, 2.8, 9e10, 9e10], - [0., 8.2, 4.7, 3.2, 9e10, 9e10], -]) +import pygrt + +# 直接使用模型文件路径 +pymod = pygrt.PyModel1D("milrow") + # END BUILD MODEL # ----------------------------------------------------------------------------------- @@ -25,17 +13,23 @@ # ----------------------------------------------------------------------------------- # BEGIN GRN -modarr = np.loadtxt("milrow") - -pymod = pygrt.PyModel1D(modarr, depsrc=2.0, deprcv=0.0) - -# 多个震中距的格林函数以列表形式返回,其中每个元素为 |Stream| 类。 -stgrnLst = pymod.compute_grn( - distarr=[5,8,10], - nt=500, dt=0.02 +from obspy import read + +pymod = pygrt.PyModel1D("milrow") +pymod.set_dynamic_grn_path("GRN") + +# 结果写入 GRN/milrow_{depsrc}_{deprcv}_{dist}/ +pymod.compute_grn( + depsrc=2.0, + deprcv=0.0, + distarr=[5, 8, 10], + nt=500, + dt=0.02, ) +# 需要时再显式读回;多震中距时需指定子目录,单震中距可用 GRN/*/*.sac +stgrn = read("GRN/milrow_2_0_5/*.sac") -print(stgrnLst[0]) +print(stgrn) # 15 Trace(s) in Stream: # .SYN..EXZ | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples # .SYN..VFZ | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples @@ -51,15 +45,21 @@ from typing import Union def plot_syn(stsyn:Stream, out:Union[str,None]=None, sigs:Union[np.ndarray,None]=None): + traces = list(stsyn) + order = {ch: i for i, ch in enumerate("ZRTNE")} + traces.sort(key=lambda tr: order.get(tr.stats.channel, 99)) + figsize = (10, 4) - nrow = 3 + nrow = len(traces) if sigs is not None: nrow += 1 figsize = (10, 4.5) fig, axs = plt.subplots(nrow, 1, figsize=figsize, gridspec_kw=dict(hspace=0.0), sharex=True) - nt = stsyn[0].stats.npts - dt = stsyn[0].stats.delta + if nrow == 1: + axs = [axs] + nt = traces[0].stats.npts + dt = traces[0].stats.delta t = np.arange(nt)*dt if sigs is not None: @@ -67,10 +67,10 @@ def plot_syn(stsyn:Stream, out:Union[str,None]=None, sigs:Union[np.ndarray,None] ax.plot(t[:len(sigs)], sigs, 'k-', lw=0.5) axs = axs[1:] - travtP = stsyn[0].stats.sac['t0'] - travtS = stsyn[0].stats.sac['t1'] + travtP = traces[0].stats.sac['t0'] + travtS = traces[0].stats.sac['t1'] - for i, tr in enumerate(stsyn): + for i, tr in enumerate(traces): ax = axs[i] ax.plot(t, tr.data, c='k', lw=0.5, label=tr.stats.channel) ax.legend(loc='upper left') @@ -88,7 +88,7 @@ def plot_syn(stsyn:Stream, out:Union[str,None]=None, sigs:Union[np.ndarray,None] if out is not None: fig.savefig(out, bbox_inches='tight') -def plot_int_dif(stsyn:Stream, stsyn_int:Stream, stsyn_dif:Stream, comp:str, out:Union[str,None]=None): +def plot_int_dif(stsyn:Stream, stsyn_int:Stream, stsyn_dif:Stream, chnl:str, out:Union[str,None]=None): nt = stsyn[0].stats.npts dt = stsyn[0].stats.delta t = np.arange(nt)*dt @@ -98,7 +98,7 @@ def plot_int_dif(stsyn:Stream, stsyn_int:Stream, stsyn_dif:Stream, comp:str, out fig, axs = plt.subplots(3, 1, figsize=(10, 4), gridspec_kw=dict(hspace=0.0), sharex=True) for i, (st, suffix) in enumerate(zip([stsyn, stsyn_int, stsyn_dif], ["", "_int", "_dif"])): - tr = st.select(component=comp)[0] + tr = st.select(channel=chnl)[0] ax = axs[i] ax.plot(t, tr.data, c='k', lw=0.5, label=f"{tr.stats.channel}{suffix}") @@ -123,11 +123,16 @@ def plot_int_dif(stsyn:Stream, stsyn_int:Stream, stsyn_dif:Stream, comp:str, out # ----------------------------------------------------------------------------------- # BEGIN SYN EX -# 接之前的代码 -idx = 2 -stgrn = stgrnLst[idx] # 选择格林函数 - -stsyn = pygrt.utils.gen_syn_from_gf_EX(stgrn, M0=1e24, az=30) +# 接之前的代码,使用震中距 10 km 的格林函数 +# ?.sac 匹配位移三分量文件名(Z/R/T 或 Z/N/E) +pymod.compute_syn( + dist=10.0, + azimuth=30.0, + scale=1e24, + output_path="syn_ex", + source="EX", +) +stsyn = read("syn_ex/?.sac") print(stsyn) # 3 Trace(s) in Stream: # .SYN..Z | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples @@ -140,10 +145,15 @@ def plot_int_dif(stsyn:Stream, stsyn_int:Stream, stsyn_dif:Stream, comp:str, out # ----------------------------------------------------------------------------------- # BEGIN SYN SF # 接之前的代码 -idx = 2 -stgrn = stgrnLst[idx] # 选择格林函数 - -stsyn = pygrt.utils.gen_syn_from_gf_SF(stgrn, S=1e16, fN=1, fE=-0.5, fZ=2, az=30) +pymod.compute_syn( + dist=10.0, + azimuth=30.0, + scale=1e16, + output_path="syn_sf", + source="SF", + force=(1, -0.5, 2), +) +stsyn = read("syn_sf/?.sac") print(stsyn) # 3 Trace(s) in Stream: # .SYN..Z | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples @@ -157,10 +167,17 @@ def plot_int_dif(stsyn:Stream, stsyn_int:Stream, stsyn_dif:Stream, comp:str, out # ----------------------------------------------------------------------------------- # BEGIN SYN DC # 接之前的代码 -idx = 2 -stgrn = stgrnLst[idx] # 选择格林函数 - -stsyn = pygrt.utils.gen_syn_from_gf_DC(stgrn, M0=1e24, strike=33, dip=50, rake=120, az=30) +pymod.compute_syn( + dist=10.0, + azimuth=30.0, + scale=1e24, + output_path="syn_dc", + source="DC", + strike=33, + dip=50, + rake=120, +) +stsyn = read("syn_dc/?.sac") print(stsyn) # 3 Trace(s) in Stream: # .SYN..Z | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples @@ -174,10 +191,16 @@ def plot_int_dif(stsyn:Stream, stsyn_int:Stream, stsyn_dif:Stream, comp:str, out # ----------------------------------------------------------------------------------- # BEGIN SYN TS # 接之前的代码 -idx = 2 -stgrn = stgrnLst[idx] # 选择格林函数 - -stsyn = pygrt.utils.gen_syn_from_gf_TS(stgrn, M0=1e24, strike=33, dip=50, az=30) +pymod.compute_syn( + dist=10.0, + azimuth=30.0, + scale=1e24, + output_path="syn_ts", + source="TS", + strike=33, + dip=50, +) +stsyn = read("syn_ts/?.sac") print(stsyn) # 3 Trace(s) in Stream: # .SYN..Z | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples @@ -190,10 +213,15 @@ def plot_int_dif(stsyn:Stream, stsyn_int:Stream, stsyn_dif:Stream, comp:str, out # ----------------------------------------------------------------------------------- # BEGIN SYN MT -idx = 2 -stgrn = stgrnLst[idx] # 选择格林函数 - -stsyn = pygrt.utils.gen_syn_from_gf_MT(stgrn, M0=1e24, MT=[0.1,-0.2,1.0,0.3,-0.5,-2.0], az=30) +pymod.compute_syn( + dist=10.0, + azimuth=30.0, + scale=1e24, + output_path="syn_mt", + source="MT", + moment_tensor=(0.1, -0.2, 1.0, 0.3, -0.5, -2.0), +) +stsyn = read("syn_mt/?.sac") print(stsyn) # 3 Trace(s) in Stream: # .SYN..Z | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples @@ -207,11 +235,19 @@ def plot_int_dif(stsyn:Stream, stsyn_int:Stream, stsyn_dif:Stream, comp:str, out # ----------------------------------------------------------------------------------- # BEGIN ZNE # 接之前的代码 -idx = 2 -stgrn = stgrnLst[idx] # 选择格林函数 - -# 设置ZNE=True可返回ZNE分量 -stsyn = pygrt.utils.gen_syn_from_gf_DC(stgrn, M0=1e24, strike=33, dip=50, rake=120, az=30, ZNE=True) +# 设置 zne=True 可返回 ZNE 分量 +pymod.compute_syn( + dist=10.0, + azimuth=30.0, + scale=1e24, + output_path="syn_dc_zne", + source="DC", + strike=33, + dip=50, + rake=120, + zne=True, +) +stsyn = read("syn_dc_zne/?.sac") print(stsyn) # 3 Trace(s) in Stream: # .SYN..Z | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples @@ -225,14 +261,19 @@ def plot_int_dif(stsyn:Stream, stsyn_int:Stream, stsyn_dif:Stream, comp:str, out # ----------------------------------------------------------------------------------- # BEGIN TIME FUNC -idx = 2 -stgrn = stgrnLst[idx] # 选择格林函数 - -stsyn = pygrt.utils.gen_syn_from_gf_SF(stgrn, S=1e16, fN=1, fE=-0.5, fZ=2, az=30) -# 生成时间函数 -trig = pygrt.sigs.gen_triangle_wave(0.6, 0.02) -# 卷积,原地修改 -pygrt.utils.stream_convolve(stsyn, trig) +# time_function 对应 CLI -D;t1=t2 时梯形波退化为三角波 +# 卷积用的时间函数会以 sig.sac 保存在输出目录 +pymod.compute_syn( + dist=10.0, + azimuth=30.0, + scale=1e16, + output_path="syn_sf_trig", + source="SF", + force=(1, -0.5, 2), + time_function="t/0.3/0.3/0.6", +) +stsyn = read("syn_sf_trig/?.sac") +trig = read("syn_sf_trig/sig.sac")[0].data plot_syn(stsyn, "syn_sf_trig.svg", trig) # END TIME FUNC # ----------------------------------------------------------------------------------- @@ -241,12 +282,17 @@ def plot_int_dif(stsyn:Stream, stsyn_int:Stream, stsyn_dif:Stream, comp:str, out # ----------------------------------------------------------------------------------- # BEGIN INT DIF -idx = 2 -stgrn = stgrnLst[idx] # 选择格林函数 - -stsyn = pygrt.utils.gen_syn_from_gf_MT(stgrn, M0=1e24, MT=[0.1,-0.2,1.0,0.3,-0.5,-2.0], az=30) +pymod.compute_syn( + dist=10.0, + azimuth=30.0, + scale=1e24, + output_path="syn_mt_intdif", + source="MT", + moment_tensor=(0.1, -0.2, 1.0, 0.3, -0.5, -2.0), +) +stsyn = read("syn_mt_intdif/?.sac") -# 使用inplace=False,防止原地修改 +# 使用 inplace=False,防止原地修改 stsyn_int = pygrt.utils.stream_integral(stsyn, inplace=False) stsyn_dif = pygrt.utils.stream_diff(stsyn, inplace=False) @@ -254,3 +300,17 @@ def plot_int_dif(stsyn:Stream, stsyn_int:Stream, stsyn_dif:Stream, comp:str, out plot_int_dif(stsyn, stsyn_int, stsyn_dif, ch, f"syn_mt_intdif_{ch}.svg") # END INT DIF # ----------------------------------------------------------------------------------- + +# 删除中间计算结果,仅保留成图 +import shutil +from pathlib import Path +for name in [ + "GRN", + "syn_ex", "syn_sf", "syn_dc", "syn_ts", "syn_mt", + "syn_dc_zne", "syn_sf_trig", "syn_mt_intdif", +]: + p = Path(name) + if p.is_dir(): + shutil.rmtree(p, ignore_errors=True) + elif p.is_file(): + p.unlink(missing_ok=True) diff --git a/docs/source/Tutorial/dynamic/run/run.sh b/docs/source/Tutorial/dynamic/run/run.sh index d9418148..e6ad6adc 100755 --- a/docs/source/Tutorial/dynamic/run/run.sh +++ b/docs/source/Tutorial/dynamic/run/run.sh @@ -28,10 +28,10 @@ head -n 10 HFZ > HFZ_head echo "..." >> HFZ_head # ----------------------------------------------------------------------------------- -# BEGIN SYN EXP +# BEGIN SYN EX # 合成结果在 syn_ex/ 目录下,以SAC格式保存。 grt syn -GGRN/milrow_2_0_10 -S1e24 -A30 -Osyn_ex -# END SYN EXP +# END SYN EX # ----------------------------------------------------------------------------------- # ----------------------------------------------------------------------------------- @@ -89,6 +89,8 @@ grt syn -GGRN/milrow_2_0_10 -S1e24 -A30 -T0.1/-0.2/1.0/0.3/-0.5/-2.0 -J1 -Osyn_m # END INT DIF # ----------------------------------------------------------------------------------- +# C 示例与 Python 示例共用目录名,跑 Python 前清掉残留,避免读回混入旧 SAC +rm -rf GRN* syn* python run.py find . \( -name "syn*" -o -name "GRN*" \) -type d -exec rm -rf {} \+ \ No newline at end of file diff --git a/docs/source/Tutorial/dynamic/run_upar/run.py b/docs/source/Tutorial/dynamic/run_upar/run.py index 3eec8bd4..8beb4e48 100755 --- a/docs/source/Tutorial/dynamic/run_upar/run.py +++ b/docs/source/Tutorial/dynamic/run_upar/run.py @@ -1,61 +1,28 @@ # -------------------------------------------------------------------------------------- # BEGIN GRN import numpy as np -import pygrt - -modarr = np.loadtxt("milrow") - -pymod = pygrt.PyModel1D(modarr, depsrc=2.0, deprcv=0.0) - -# 传入calc_upar=True计算空间导数 -stgrn = pymod.compute_grn(distarr=[10], nt=500, dt=0.02, calc_upar=True)[0] +import pygrt +from obspy import read + +pymod = pygrt.PyModel1D("milrow") +pymod.set_dynamic_grn_path("GRN") + +# 传入 calc_upar=True 计算空间导数 +pymod.compute_grn( + depsrc=2.0, + deprcv=0.0, + distarr=[10], + nt=500, + dt=0.02, + calc_upar=True, +) +stgrn = read("GRN/*/*.sac") print(stgrn.__str__(extended=True)) # 45 Trace(s) in Stream: # .SYN..EXZ | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..VFZ | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..DDZ | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..HFZ | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..DSZ | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..SSZ | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..zEXZ | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..rEXZ | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..zVFZ | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..rVFZ | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..zDDZ | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..rDDZ | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..zHFZ | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..rHFZ | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..zDSZ | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..rDSZ | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..zSSZ | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..rSSZ | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..EXR | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..VFR | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..DDR | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..HFR | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..DSR | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..SSR | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..zEXR | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..rEXR | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..zVFR | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..rVFR | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..zDDR | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..rDDR | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..zHFR | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..rHFR | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..zDSR | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..rDSR | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..zSSR | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..rSSR | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..HFT | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..DST | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..SST | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..zHFT | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..rHFT | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..zDST | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..rDST | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..zSST | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..rSST | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples +# ... +# .SYN..zEXZ | ... +# .SYN..rEXZ | ... # END GRN # -------------------------------------------------------------------------------------- @@ -63,44 +30,53 @@ # -------------------------------------------------------------------------------------- # BEGIN SYN DC -# 传入calc_upar=True计算空间导数 -stsyn = pygrt.utils.gen_syn_from_gf_DC(stgrn, M0=1e24, strike=33, dip=50, rake=120, az=30, calc_upar=True) +# 传入 calc_upar=True 计算空间导数 +# ?.sac 为位移,[zrt]?.sac 为空间导数 +pymod.compute_syn( + dist=10.0, + azimuth=30.0, + scale=1e24, + output_path="syn_dc", + source="DC", + strike=33, + dip=50, + rake=120, + calc_upar=True, +) +stsyn = read("syn_dc/?.sac") + read("syn_dc/[zrt]?.sac") print(stsyn) # 12 Trace(s) in Stream: -# .SYN..Z | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..R | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..T | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..zZ | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..zR | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..zT | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..rZ | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..rR | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..rT | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..tZ | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..tR | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..tT | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples +# .SYN..Z | ... +# .SYN..zZ | ... +# .SYN..rZ | ... +# .SYN..tZ | ... # END SYN DC # -------------------------------------------------------------------------------------- # -------------------------------------------------------------------------------------- # BEGIN ZNE -# 传入ZNE=True可返回ZNE分量 -stsyn = pygrt.utils.gen_syn_from_gf_DC(stgrn, M0=1e24, strike=33, dip=50, rake=120, az=30, calc_upar=True, ZNE=True) +# 传入 zne=True 可返回 ZNE 分量 +# ?.sac 为位移,[zne]?.sac 为空间导数 +pymod.compute_syn( + dist=10.0, + azimuth=30.0, + scale=1e24, + output_path="syn_dc_zne", + source="DC", + strike=33, + dip=50, + rake=120, + calc_upar=True, + zne=True, +) +stsyn = read("syn_dc_zne/?.sac") + read("syn_dc_zne/[zne]?.sac") print(stsyn) # 12 Trace(s) in Stream: -# .SYN..Z | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..N | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..E | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..zZ | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..zN | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..zE | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..nZ | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..nN | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..nE | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..eZ | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..eN | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..eE | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples +# .SYN..Z | ... +# .SYN..zZ | ... +# .SYN..nZ | ... +# .SYN..eZ | ... # END ZNE # -------------------------------------------------------------------------------------- @@ -122,8 +98,8 @@ def plot6(st6:Stream, title:str, out:str|None=None): if MIN > np.min(d): MIN = np.min(d) - travtP = stsyn[0].stats.sac['t0'] - travtS = stsyn[0].stats.sac['t1'] + travtP = st6[0].stats.sac['t0'] + travtS = st6[0].stats.sac['t1'] fig, axs = plt.subplots(len(st6), 1, figsize=(10, 1.2*len(st6)), gridspec_kw=dict(hspace=0.0), sharex=True) for i in range(len(st6)): @@ -133,11 +109,13 @@ def plot6(st6:Stream, title:str, out:str|None=None): ax.plot(t, tr.data, c='k', lw=0.5, label=tr.stats.channel) ax.legend(loc='upper left') + # 相对全局最大值过小时,固定到同样数量级下的坐标轴便于展示近零分量 + m = np.max(np.abs(tr.data)) ylims = ax.get_ylim() - if np.max(np.abs(np.array(ylims)))/MAX < 1e-5: - ylims = [-1, 1] + if m / MAX < 1e-5: + ylims = np.array([-1, 1]) * MAX ax.set_ylim(ylims) - + # 绘制到时 ax.vlines(travtP, *ylims, colors='b') ax.text(travtP, ylims[1], "P", ha='left', va='top', color='b') @@ -153,32 +131,33 @@ def plot6(st6:Stream, title:str, out:str|None=None): fig.savefig(out, bbox_inches='tight') # END plot func # -------------------------------------------------------------------------------------- - + # -------------------------------------------------------------------------------------- # BEGIN STRAIN -st_strain = pygrt.utils.compute_strain(stsyn) +# return_result=True 时按文件名前缀读回 strain_*.sac,避免与位移等混在一起 +st_strain = pygrt.utils.compute_strain("syn_dc_zne", return_result=True) print(st_strain) # 6 Trace(s) in Stream: -# .SYN..ZZ | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..ZN | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..ZE | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..NN | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..NE | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..EE | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples +# .SYN..ZZ | ... +# .SYN..ZN | ... +# .SYN..ZE | ... +# .SYN..NN | ... +# .SYN..NE | ... +# .SYN..EE | ... plot6(st_strain, "Strain", "strain.svg") # END STRAIN # -------------------------------------------------------------------------------------- # -------------------------------------------------------------------------------------- # BEGIN ROTATION -st_rotation = pygrt.utils.compute_rotation(stsyn) +st_rotation = pygrt.utils.compute_rotation("syn_dc_zne", return_result=True) print(st_rotation) -# 6 Trace(s) in Stream: -# .SYN..ZN | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..ZE | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..NE | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples +# 3 Trace(s) in Stream: +# .SYN..ZN | ... +# .SYN..ZE | ... +# .SYN..NE | ... plot6(st_rotation, "Rotation", "rotation.svg") # END ROTATION # -------------------------------------------------------------------------------------- @@ -186,15 +165,23 @@ def plot6(st6:Stream, title:str, out:str|None=None): # BEGIN STRESS -st_stress = pygrt.utils.compute_stress(stsyn) +st_stress = pygrt.utils.compute_stress("syn_dc_zne", return_result=True) print(st_stress) # 6 Trace(s) in Stream: -# .SYN..ZZ | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..ZN | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..ZE | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..NN | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..NE | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples -# .SYN..EE | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:09.980000Z | 50.0 Hz, 500 samples +# .SYN..ZZ | ... +# .SYN..ZN | ... +# .SYN..ZE | ... +# .SYN..NN | ... +# .SYN..NE | ... +# .SYN..EE | ... plot6(st_stress, "Stress", "stress.svg") # END STRESS # -------------------------------------------------------------------------------------- + +# 删除中间计算结果,仅保留成图 +import shutil +from pathlib import Path +for name in ["GRN", "syn_dc", "syn_dc_zne"]: + p = Path(name) + if p.is_dir(): + shutil.rmtree(p, ignore_errors=True) diff --git a/docs/source/Tutorial/dynamic/run_upar/run.sh b/docs/source/Tutorial/dynamic/run_upar/run.sh index 64273b37..b7402a1d 100755 --- a/docs/source/Tutorial/dynamic/run_upar/run.sh +++ b/docs/source/Tutorial/dynamic/run_upar/run.sh @@ -47,6 +47,8 @@ grt stress syn_dc_zne # END STRESS # -------------------------------------------------------------------------------------- +# C 示例与 Python 示例共用目录名,跑 Python 前清掉残留,避免读回混入旧 SAC +rm -rf GRN* syn* python run.py find . \( -name "syn*" -o -name "GRN*" \) -type d -exec rm -rf {} \+ diff --git a/docs/source/Tutorial/dynamic/strain_stress.rst b/docs/source/Tutorial/dynamic/strain_stress.rst index 368fc024..07bd7c74 100644 --- a/docs/source/Tutorial/dynamic/strain_stress.rst +++ b/docs/source/Tutorial/dynamic/strain_stress.rst @@ -144,7 +144,10 @@ :start-after: BEGIN STRAIN :end-before: END STRAIN - 返回的 |Stream| 通道名即为分量名,如ZZ、ZN等。 + 张量结果写回原合成目录中的 :file:`strain_??.sac`, + 通道名即为分量名,如 ZZ、ZN 等。 + 动态结果只能靠文件名前缀区分应变/旋转/应力; + ``return_result=True`` 时按 :file:`strain_*.sac` 读回,避免与位移等混在一起。 .. figure:: run_upar/strain.svg :align: center @@ -176,7 +179,9 @@ :start-after: BEGIN ROTATION :end-before: END ROTATION - 返回的 |Stream| 通道名即为分量名,如ZN,ZE,NE。 + 张量结果写回原合成目录中的 :file:`rotation_??.sac`, + 通道名即为分量名,如 ZN、ZE、NE。 + ``return_result=True`` 时按 :file:`rotation_*.sac` 读回。 .. figure:: run_upar/rotation.svg :align: center @@ -210,7 +215,9 @@ :start-after: BEGIN STRESS :end-before: END STRESS - 返回的 |Stream| 通道名即为分量名,如ZZ、ZN等。 + 张量结果写回原合成目录中的 :file:`stress_??.sac`, + 通道名即为分量名,如 ZZ、ZN 等。 + ``return_result=True`` 时按 :file:`stress_*.sac` 读回。 .. figure:: run_upar/stress.svg :align: center diff --git a/docs/source/Tutorial/dynamic/syn.rst b/docs/source/Tutorial/dynamic/syn.rst index 0c1f833c..c15622f5 100644 --- a/docs/source/Tutorial/dynamic/syn.rst +++ b/docs/source/Tutorial/dynamic/syn.rst @@ -9,7 +9,7 @@ **震源机制参数中(如单力源、矩张量源)Z轴取向下为正。** -Python中合成动态位移的主函数为 :func:`gen_syn_from_gf_*() ` (\*表示对不同震源) ,C模块为 :doc:`/Module/syn`。 +Python中合成动态位移的主函数为 :meth:`compute_syn() ` ,C模块为 :doc:`/Module/syn`。 使用上节计算的格林函数,合成动态位移(理论地震图)。方便起见,这里统一使用milrow模型,震源深度2km,场点位于地表,震中距10km的格林函数,方位角30°。 @@ -225,7 +225,9 @@ Python中合成动态位移的主函数为 :func:`gen_syn_from_gf_*() ` + 的 ``time_function`` 参数。 .. figure:: run/syn_sf_trig.svg :align: center @@ -253,6 +255,10 @@ Python中合成动态位移的主函数为 :func:`gen_syn_from_gf_*() ` / + :func:`stream_diff() ` 做积分与微分。 + 若希望在合成阶段完成,也可传入 ``integrate_order`` / ``differentiate_order`` + (分别对应 CLI 的 ``-I`` / ``-J``)。 .. figure:: run/syn_mt_intdif_Z.svg :align: center diff --git a/docs/source/Tutorial/modal/run/plot_dispersion.sh b/docs/source/Tutorial/modal/run/plot_dispersion.sh index 678cab57..5c6fd188 100644 --- a/docs/source/Tutorial/modal/run/plot_dispersion.sh +++ b/docs/source/Tutorial/modal/run/plot_dispersion.sh @@ -20,7 +20,7 @@ function _plot(){ c1=$(echo "scale=1; $minc - ($maxc - $minc) * 0.05" | bc) c2=$(echo "scale=1; $maxc + ($maxc - $minc) * 0.05" | bc) - rm $JUNK + rm -rf $JUNK gmt basemap -R0/$maxfreq/$c1/$c2 -JX10c/7c -Bxa1f+l"Frequency (Hz)" -Bya0.2f+l"Phase velocity (km/s)" -BWSen+t"$title" $args for i in $(seq 0 $maxmode); do @@ -38,4 +38,4 @@ gmt begin $filename pdf gmt end pdf2svg $filename.pdf $filename.svg -rm $filename.pdf \ No newline at end of file +rm -rf $filename.pdf \ No newline at end of file diff --git a/docs/source/Tutorial/modal/run/plot_dispersion_group.sh b/docs/source/Tutorial/modal/run/plot_dispersion_group.sh index f6c26ce8..0e9361ec 100644 --- a/docs/source/Tutorial/modal/run/plot_dispersion_group.sh +++ b/docs/source/Tutorial/modal/run/plot_dispersion_group.sh @@ -20,7 +20,7 @@ function _plot(){ c1=$(echo "scale=1; $minc - ($maxc - $minc) * 0.05" | bc) c2=$(echo "scale=1; $maxc + ($maxc - $minc) * 0.05" | bc) - rm $JUNK + rm -rf $JUNK gmt basemap -R0/$maxfreq/$c1/$c2 -JX10c/7c -Bxa1f+l"Frequency (Hz)" -Bya0.2f+l"Group velocity (km/s)" -BWSen+t"$title" $args for i in $(seq 0 $maxmode); do @@ -38,4 +38,4 @@ gmt begin $filename pdf gmt end pdf2svg $filename.pdf $filename.svg -rm $filename.pdf \ No newline at end of file +rm -rf $filename.pdf \ No newline at end of file diff --git a/docs/source/Tutorial/modal/run/run.sh b/docs/source/Tutorial/modal/run/run.sh index 73d1d122..5a6c5c8f 100644 --- a/docs/source/Tutorial/modal/run/run.sh +++ b/docs/source/Tutorial/modal/run/run.sh @@ -49,4 +49,4 @@ grt eigenfn -Cphase_L.nc -F0/0.5/0.1 -N0 -K+ccsens_L.nc+uusens_L.nc+z0.2 python plot_sensitivity.py -rm *.nc secfunc_* \ No newline at end of file +rm -rf *.nc secfunc_* \ No newline at end of file diff --git a/docs/source/Tutorial/prepare.rst b/docs/source/Tutorial/prepare.rst index fdf8cdb4..f97625ec 100644 --- a/docs/source/Tutorial/prepare.rst +++ b/docs/source/Tutorial/prepare.rst @@ -4,10 +4,11 @@ 准备工作 ============= -**PyGRT** 程序包由C和Python两个编程语言的代码组成,目的是兼并高效性和便捷性。 -底层复杂运算由C语言编写,编译链接成动态库 ``libgrt.so`` 供Python调用。Python通过 ``ctypes`` 库导入动态库以使用外部函数,以此兼并了C语言的高效和Python语言的便捷。 +**PyGRT** 程序包由 C 和 Python 两个编程语言的代码组成,目的是兼并高效性和便捷性。 +底层复杂运算由 C 语言编写,并以可执行文件 :command:`grt` 的形式提供。 +Python 接口主要负责组织模型与输出路径,并调用 :command:`grt` 完成计算与文件读写。 -除了Python脚本式运行, **PyGRT** 保留传统命令行式运行C程序 :command:`grt` 。 +除了 Python 脚本式运行,也可直接在终端运行 :command:`grt` 。 受 |GMT| 的启发, :command:`grt` 程序对多个计算功能使用模块化管理, 可使用类似于以下格式来运行程序。每个模块可使用 ``-h`` 查看帮助。 @@ -15,7 +16,7 @@ grt [] -**C程序的运行独立于Python,不需要Python环境,从而满足了更多计算场景。** +**C 程序的运行独立于 Python,不需要 Python 环境,从而满足了更多计算场景。** 建立模型文件 @@ -49,7 +50,7 @@ .. tab:: Python - 模型格式与C一致,在Python中可以使用 :code:`np.loadtxt()` 导入文本文件,或者手动定义数组 + 模型格式与 C 一致,Python 端直接传入模型文件路径。 .. literalinclude:: dynamic/run/run.py :language: python diff --git a/docs/source/Tutorial/static/index.rst b/docs/source/Tutorial/static/index.rst index 1e9e9197..6d3da2af 100644 --- a/docs/source/Tutorial/static/index.rst +++ b/docs/source/Tutorial/static/index.rst @@ -12,11 +12,11 @@ flowchart TB - GG(["static_compute_grn()"]) - SS(["gen_syn_from_gf_*()"]) - EE(["compute_strain()"]) - RR(["compute_rotation()"]) - TT(["compute_stress()"]) + GG(["compute_static_grn()"]) + SS(["compute_static_syn()"]) + EE(["utils.compute_strain()"]) + RR(["utils.compute_rotation()"]) + TT(["utils.compute_stress()"]) G["Compute Green's Functions (and its Spatial Derivatives)"] diff --git a/docs/source/Tutorial/static/run/run.py b/docs/source/Tutorial/static/run/run.py index 33922045..30e6ed17 100755 --- a/docs/source/Tutorial/static/run/run.py +++ b/docs/source/Tutorial/static/run/run.py @@ -1,20 +1,30 @@ # --------------------------------------------------------------------------------- # BEGIN GRN import numpy as np -import pygrt +import pygrt -modarr = np.loadtxt("milrow") +pymod = pygrt.PyModel1D("milrow") +pymod.set_static_grn_path("stgrn.nc") -pymod = pygrt.PyModel1D(modarr, depsrc=2.0, deprcv=0.0) - -norths = np.linspace(-3, 3, 41) -easts = np.linspace(-2.5, 2.5, 33) +# norths/easts 各为三个元素: start/stop/step (km) +norths = [-3.0, 3.0, 0.15] +easts = [-2.5, 2.5, 0.15] # 可以设置 distarr 来指定震中距序列 -# static_grn = pymod.compute_static_grn(distarr=np.arange(0,10+1e-8,0.1)) +# pymod.compute_static_grn( +# depsrc=2.0, deprcv=0.0, distarr=np.arange(0, 10+1e-8, 0.1), +# ) # 也可以设置 norths 和 easts 来指定 north/east 网格 -static_grn = pymod.compute_static_grn(norths=norths, easts=easts) +pymod.compute_static_grn( + depsrc=2.0, + deprcv=0.0, + norths=norths, + easts=easts, +) +static_grn = pygrt.utils.read_static_nc("stgrn.nc") print(static_grn.keys()) -# dict_keys(['_norths', '_easts', '_src_va', '_src_vb', '_src_rho', '_rcv_va', '_rcv_vb', '_rcv_rho', 'EXZ', 'VFZ', 'DDZ', 'HFZ', 'DSZ', 'SSZ', 'EXR', 'VFR', 'DDR', 'HFR', 'DSR', 'SSR', 'HFT', 'DST', 'SST']) +# dict_keys(['dimensions', 'variables', 'attributes']) +print(list(static_grn["variables"].keys())) +# ['north', 'east', 'EXZ', 'EXR', ...] # END GRN # --------------------------------------------------------------------------------- @@ -24,14 +34,20 @@ from typing import Union def plot_static(static_syn:dict, out:Union[str,None]=None): - fig, ax = plt.subplots(1, 1, figsize=(10,8)) + vars_ = static_syn["variables"] + north = vars_["north"]["data"] + east = vars_["east"]["data"] + Z = vars_["Z"]["data"] + N = vars_["N"]["data"] + E = vars_["E"]["data"] + + fig, ax = plt.subplots(1, 1, figsize=(10, 8)) # 设计对称色标 - m = np.max(np.abs(static_syn[f'Z'])) * 1.2 - pcm = ax.pcolormesh(easts, norths, static_syn[f'Z'], cmap='bwr', vmin=-m, vmax=m) - ax.quiver(easts, norths, static_syn[f'E'], static_syn[f'N'], - angles='uv', pivot='mid') - ax.set_ylim([norths[0], norths[-1]]) - ax.set_xlim([easts[0], easts[-1]]) + m = np.max(np.abs(Z)) * 1.2 + pcm = ax.pcolormesh(east, north, Z, cmap='bwr', vmin=-m, vmax=m) + ax.quiver(east, north, E, N, angles='uv', pivot='mid') + ax.set_ylim([north[0], north[-1]]) + ax.set_xlim([east[0], east[-1]]) ax.set_aspect('equal') cbar = fig.colorbar(pcm, ax=ax, label='Z(cm)') cbar.formatter.set_powerlimits((0, 0)) @@ -44,9 +60,15 @@ def plot_static(static_syn:dict, out:Union[str,None]=None): # --------------------------------------------------------------------------------- # BEGIN SYN EX -static_syn = pygrt.utils.gen_syn_from_gf_EX(static_grn, M0=1e24, ZNE=True) -print(static_syn.keys()) -# dict_keys(['_norths', '_easts', '_src_va', '_src_vb', '_src_rho', '_rcv_va', '_rcv_vb', '_rcv_rho', 'Z', 'N', 'E']) +static_syn = pymod.compute_static_syn( + scale=1e24, + output_path="stsyn_ex.nc", + source="EX", + zne=True, + return_result=True, +) +print(list(static_syn["variables"].keys())) +# ['north', 'east', 'Z', 'N', 'E'] plot_static(static_syn, "syn_ex.svg") # END SYN EX # --------------------------------------------------------------------------------- @@ -54,9 +76,16 @@ def plot_static(static_syn:dict, out:Union[str,None]=None): # --------------------------------------------------------------------------------- # BEGIN SYN SF -static_syn = pygrt.utils.gen_syn_from_gf_SF(static_grn, S=1e16, fN=1, fE=-0.5, fZ=2, ZNE=True) -print(static_syn.keys()) -# dict_keys(['_norths', '_easts', '_src_va', '_src_vb', '_src_rho', '_rcv_va', '_rcv_vb', '_rcv_rho', 'Z', 'N', 'E']) +static_syn = pymod.compute_static_syn( + scale=1e16, + output_path="stsyn_sf.nc", + source="SF", + force=(1, -0.5, 2), + zne=True, + return_result=True, +) +print(list(static_syn["variables"].keys())) +# ['north', 'east', 'Z', 'N', 'E'] plot_static(static_syn, "syn_sf.svg") # END SYN SF # --------------------------------------------------------------------------------- @@ -64,27 +93,53 @@ def plot_static(static_syn:dict, out:Union[str,None]=None): # --------------------------------------------------------------------------------- # BEGIN SYN DC -static_syn = pygrt.utils.gen_syn_from_gf_DC(static_grn, M0=1e24, strike=33, dip=50, rake=120, ZNE=True) -print(static_syn.keys()) -# dict_keys(['_norths', '_easts', '_src_va', '_src_vb', '_src_rho', '_rcv_va', '_rcv_vb', '_rcv_rho', 'Z', 'N', 'E']) +static_syn = pymod.compute_static_syn( + scale=1e24, + output_path="stsyn_dc.nc", + source="DC", + strike=33, + dip=50, + rake=120, + zne=True, + return_result=True, +) +print(list(static_syn["variables"].keys())) +# ['north', 'east', 'Z', 'N', 'E'] plot_static(static_syn, "syn_dc.svg") # END SYN DC # --------------------------------------------------------------------------------- # --------------------------------------------------------------------------------- # BEGIN SYN DC2 -static_syn = pygrt.utils.gen_syn_from_gf_DC(static_grn, M0=1e24, strike=33, dip=90, rake=0, ZNE=True) -print(static_syn.keys()) -# dict_keys(['_norths', '_easts', '_src_va', '_src_vb', '_src_rho', '_rcv_va', '_rcv_vb', '_rcv_rho', 'Z', 'N', 'E']) +static_syn = pymod.compute_static_syn( + scale=1e24, + output_path="stsyn_dc2.nc", + source="DC", + strike=33, + dip=90, + rake=0, + zne=True, + return_result=True, +) +print(list(static_syn["variables"].keys())) +# ['north', 'east', 'Z', 'N', 'E'] plot_static(static_syn, "syn_dc2.svg") # END SYN DC2 # --------------------------------------------------------------------------------- # --------------------------------------------------------------------------------- # BEGIN SYN TS -static_syn = pygrt.utils.gen_syn_from_gf_TS(static_grn, M0=1e24, strike=33, dip=50, ZNE=True) -print(static_syn.keys()) -# dict_keys(['_norths', '_easts', '_src_va', '_src_vb', '_src_rho', '_rcv_va', '_rcv_vb', '_rcv_rho', 'Z', 'N', 'E']) +static_syn = pymod.compute_static_syn( + scale=1e24, + output_path="stsyn_ts.nc", + source="TS", + strike=33, + dip=50, + zne=True, + return_result=True, +) +print(list(static_syn["variables"].keys())) +# ['north', 'east', 'Z', 'N', 'E'] plot_static(static_syn, "syn_ts.svg") # END SYN TS # --------------------------------------------------------------------------------- @@ -92,9 +147,17 @@ def plot_static(static_syn:dict, out:Union[str,None]=None): # --------------------------------------------------------------------------------- # BEGIN SYN TS2 -static_syn = pygrt.utils.gen_syn_from_gf_TS(static_grn, M0=1e24, strike=33, dip=90, ZNE=True) -print(static_syn.keys()) -# dict_keys(['_norths', '_easts', '_src_va', '_src_vb', '_src_rho', '_rcv_va', '_rcv_vb', '_rcv_rho', 'Z', 'N', 'E']) +static_syn = pymod.compute_static_syn( + scale=1e24, + output_path="stsyn_ts2.nc", + source="TS", + strike=33, + dip=90, + zne=True, + return_result=True, +) +print(list(static_syn["variables"].keys())) +# ['north', 'east', 'Z', 'N', 'E'] plot_static(static_syn, "syn_ts2.svg") # END SYN TS2 # --------------------------------------------------------------------------------- @@ -102,18 +165,32 @@ def plot_static(static_syn:dict, out:Union[str,None]=None): # --------------------------------------------------------------------------------- # BEGIN SYN MT -static_syn = pygrt.utils.gen_syn_from_gf_MT(static_grn, M0=1e24, MT=[0.1,-0.2,1.0,0.3,-0.5,-2.0], ZNE=True) -print(static_syn.keys()) -# dict_keys(['_norths', '_easts', '_src_va', '_src_vb', '_src_rho', '_rcv_va', '_rcv_vb', '_rcv_rho', 'Z', 'N', 'E']) +static_syn = pymod.compute_static_syn( + scale=1e24, + output_path="stsyn_mt.nc", + source="MT", + moment_tensor=(0.1, -0.2, 1.0, 0.3, -0.5, -2.0), + zne=True, + return_result=True, +) +print(list(static_syn["variables"].keys())) +# ['north', 'east', 'Z', 'N', 'E'] plot_static(static_syn, "syn_mt.svg") # END SYN MT # --------------------------------------------------------------------------------- # --------------------------------------------------------------------------------- # BEGIN SYN MT2 -static_syn = pygrt.utils.gen_syn_from_gf_MT(static_grn, M0=1e24, MT=[0,-0.2,0,0,0,0], ZNE=True) -print(static_syn.keys()) -# dict_keys(['_norths', '_easts', '_src_va', '_src_vb', '_src_rho', '_rcv_va', '_rcv_vb', '_rcv_rho', 'Z', 'N', 'E']) +static_syn = pymod.compute_static_syn( + scale=1e24, + output_path="stsyn_mt2.nc", + source="MT", + moment_tensor=(0, -0.2, 0, 0, 0, 0), + zne=True, + return_result=True, +) +print(list(static_syn["variables"].keys())) +# ['north', 'east', 'Z', 'N', 'E'] plot_static(static_syn, "syn_mt2.svg") # END SYN MT2 # --------------------------------------------------------------------------------- @@ -121,11 +198,33 @@ def plot_static(static_syn:dict, out:Union[str,None]=None): # --------------------------------------------------------------------------------- # BEGIN NEW XY -norths2 = np.arange(-3, 3+1e-8, 0.2) -easts2 = np.arange(-2.5, 2.5+1e-8, 0.25) -static_syn = pygrt.utils.gen_syn_from_gf_DC(static_grn, M0=1e24, strike=33, dip=90, rake=0, ZNE=True, norths=norths2, easts=easts2) -print(static_syn.keys()) -# dict_keys(['_norths', '_easts', '_src_va', '_src_vb', '_src_rho', '_rcv_va', '_rcv_vb', '_rcv_rho', 'Z', 'N', 'E']) +static_syn = pymod.compute_static_syn( + scale=1e24, + output_path="stsynXY_dc2.nc", + source="DC", + strike=33, + dip=90, + rake=0, + zne=True, + norths=[-3.0, 3.0, 0.2], + easts=[-2.5, 2.5, 0.25], + return_result=True, +) +print(list(static_syn["variables"].keys())) +# ['north', 'east', 'Z', 'N', 'E'] plot_static(static_syn, "synXY_dc2.svg") # END NEW XY # --------------------------------------------------------------------------------- + +# 删除中间计算结果,仅保留成图 +import shutil +from pathlib import Path +for name in [ + "stgrn.nc", + "stsyn_ex.nc", "stsyn_sf.nc", "stsyn_dc.nc", "stsyn_dc2.nc", + "stsyn_ts.nc", "stsyn_ts2.nc", "stsyn_mt.nc", "stsyn_mt2.nc", + "stsynXY_dc2.nc", +]: + p = Path(name) + if p.is_file(): + p.unlink(missing_ok=True) diff --git a/docs/source/Tutorial/static/run/run.sh b/docs/source/Tutorial/static/run/run.sh index 7b902643..4dff5230 100755 --- a/docs/source/Tutorial/static/run/run.sh +++ b/docs/source/Tutorial/static/run/run.sh @@ -52,10 +52,10 @@ ncdump -h stgrn.nc > grn_head # --------------------------------------------------------------------------------- -# BEGIN SYN EXP +# BEGIN SYN EX # 从网格文件中读取格林函数,再将合成结果写入新网格 grt static syn -S1e24 -N -Gstgrn.nc -Ostsyn_ex.nc -# END SYN EXP +# END SYN EX # --------------------------------------------------------------------------------- gmt begin syn_ex pdf @@ -194,7 +194,7 @@ gmt end for pdfname in $(ls *.pdf); do name=$(basename $pdfname .pdf) pdf2svg $pdfname ${name}.svg - rm -r $pdfname + rm -rf $pdfname done rm -rf stgrn* stsyn* \ No newline at end of file diff --git a/docs/source/Tutorial/static/run_upar/run.py b/docs/source/Tutorial/static/run_upar/run.py index 296e969a..668238c1 100755 --- a/docs/source/Tutorial/static/run_upar/run.py +++ b/docs/source/Tutorial/static/run_upar/run.py @@ -1,31 +1,35 @@ import matplotlib.pyplot as plt import numpy as np -import pygrt +import pygrt def plot6(data:dict, title:str, out:str|None=None): - chs = [k for k in data.keys() if k[0]!='_'] - chs.sort(reverse=True) - norths = data['_norths'] - easts = data['_easts'] + vars_ = data["variables"] + norths = vars_["north"]["data"] + easts = vars_["east"]["data"] + chs = sorted( + [k for k in vars_ if k.startswith(title.lower() + "_")], + reverse=True, + ) fig, axs = plt.subplots(len(chs)//3, 3, figsize=(10, len(chs))) axs = axs.ravel() MAX = 0 for i in range(len(chs)): ch = chs[i] - m = np.max(np.abs(data[ch])) + m = np.max(np.abs(vars_[ch]["data"])) if m > MAX: MAX = m for i in range(len(chs)): ax = axs[i] ch = chs[i] + arr = vars_[ch]["data"] vmin = vmax = None - if np.max(np.abs(data[ch]))/MAX < 1e-5: + if np.max(np.abs(arr))/MAX < 1e-5: vmin = -1 vmax = 1 - pcm = ax.pcolormesh(easts, norths, data[ch], shading='nearest', vmin=vmin, vmax=vmax, rasterized=True) + pcm = ax.pcolormesh(easts, norths, arr, shading='nearest', vmin=vmin, vmax=vmax, rasterized=True) ax.set_aspect('equal') ax.set_title(ch) cbar = fig.colorbar(pcm, ax=ax) @@ -38,28 +42,45 @@ def plot6(data:dict, title:str, out:str|None=None): fig.savefig(out, bbox_inches='tight') -modarr = np.loadtxt("milrow") +pymod = pygrt.PyModel1D("milrow") +pymod.set_static_grn_path("stgrn.nc") -pymod = pygrt.PyModel1D(modarr, depsrc=2.0, deprcv=0.0) +# norths/easts 各为三个元素: start/stop/step (km) +# 传入 calc_upar=True 可计算空间导数 +pymod.compute_static_grn( + depsrc=2.0, + deprcv=0.0, + norths=[-3.0, 3.0, 0.15], + easts=[-2.5, 2.5, 0.15], + calc_upar=True, +) -norths = np.linspace(-3, 3, 41) -easts = np.linspace(-2.5, 2.5, 33) -# 传入calc_upar=True可计算空间导数 -static_grn = pymod.compute_static_grn(norths, easts, calc_upar=True) +# 传入 calc_upar=True 可计算空间导数 +# 传入 zne=True 返回 ZNE 分量 +pymod.compute_static_syn( + scale=1e24, + output_path="stsyn_dc_zne.nc", + source="DC", + strike=33, + dip=50, + rake=120, + zne=True, + calc_upar=True, +) -# 传入calc_upar=True可计算空间导数 -# 传入ZNE=True返回ZNE分量 -static_syn = pygrt.utils.gen_syn_from_gf_DC(static_grn, M0=1e24, strike=33, dip=50, rake=120, ZNE=True, calc_upar=True) - -# 计算应变 -static_strain = pygrt.utils.compute_strain(static_syn) - -# 计算旋转 -static_rotation = pygrt.utils.compute_rotation(static_syn) - -# 计算应力 -static_stress = pygrt.utils.compute_stress(static_syn) +# 计算应变 / 旋转 / 应力,结果写回同一 nc 文件 +static_strain = pygrt.utils.compute_strain("stsyn_dc_zne.nc", return_result=True) +static_rotation = pygrt.utils.compute_rotation("stsyn_dc_zne.nc", return_result=True) +static_stress = pygrt.utils.compute_stress("stsyn_dc_zne.nc", return_result=True) plot6(static_strain, "Strain", 'static_strain.svg') plot6(static_rotation, "Rotation", 'static_rotation.svg') -plot6(static_stress, "Stress", 'static_stress.svg') \ No newline at end of file +plot6(static_stress, "Stress", 'static_stress.svg') + +# 删除中间计算结果,仅保留成图 +import shutil +from pathlib import Path +for name in ["stgrn.nc", "stsyn_dc_zne.nc"]: + p = Path(name) + if p.is_file(): + p.unlink(missing_ok=True) diff --git a/docs/source/Tutorial/static/run_upar/run.sh b/docs/source/Tutorial/static/run_upar/run.sh index 01e53d05..17b2e4d7 100755 --- a/docs/source/Tutorial/static/run_upar/run.sh +++ b/docs/source/Tutorial/static/run_upar/run.sh @@ -31,6 +31,8 @@ grt static rotation stsyn_dc_zne.nc grt static stress stsyn_dc_zne.nc # END +# C 示例与 Python 示例共用文件名,跑 Python 前清掉残留 +rm -rf stgrn* stsyn* python run.py rm -rf stgrn* stsyn* \ No newline at end of file diff --git a/docs/source/Tutorial/static/static_gfunc.rst b/docs/source/Tutorial/static/static_gfunc.rst index 02970771..bdae056a 100644 --- a/docs/source/Tutorial/static/static_gfunc.rst +++ b/docs/source/Tutorial/static/static_gfunc.rst @@ -4,7 +4,7 @@ 计算静态格林函数 ================= -Python中计算静态格林函数的主函数为 :func:`compute_static_grn() ` ,C模块为 :doc:`/Module/static_greenfn`。 +Python中计算静态格林函数的主函数为 :meth:`compute_static_grn() ` ,C模块为 :doc:`/Module/static_greenfn`。 建议先阅读完 :doc:`/Tutorial/dynamic/gfunc` 部分。静态情况与动态情况采取的计算方法一致,只是推导细节会有不同,详见 |yao2026p| 。 @@ -15,8 +15,8 @@ Python中计算静态格林函数的主函数为 :func:`compute_static_grn() ` 指定的 +NetCDF 文件。在 :doc:`static_syn` 阶段,可以指定新的 XY 网格,此时每个节点的格林函数会近似为最近震中距的格林函数。 示例程序 ----------- @@ -46,4 +46,7 @@ Python中计算静态格林函数的主函数为 :func:`compute_static_grn() ` + 指定的 NetCDF 文件。需要读回时调用 + :func:`pygrt.utils.read_static_nc`,返回字典包含 + ``dimensions``、 ``variables`` 与 ``attributes``。 \ No newline at end of file diff --git a/docs/source/Tutorial/static/static_strain_stress.rst b/docs/source/Tutorial/static/static_strain_stress.rst index 2e1d10d5..0c38a59a 100644 --- a/docs/source/Tutorial/static/static_strain_stress.rst +++ b/docs/source/Tutorial/static/static_strain_stress.rst @@ -4,7 +4,8 @@ 计算静态应变、旋转、应力张量 ================================= -除了使用不同的函数名/程序名,输出文件不同之外,流程基本和 :doc:`/Tutorial/dynamic/strain_stress` 类似。这里直接给出脚本。 +除了使用不同的程序名和输出文件之外,流程基本和 :doc:`/Tutorial/dynamic/strain_stress` 类似。 +Python 接口接收静态合成 NetCDF 路径,并由 C CLI 原地写入张量变量。 .. tabs:: diff --git a/docs/source/Tutorial/static/static_syn.rst b/docs/source/Tutorial/static/static_syn.rst index ce8cbf8e..8c5a09ff 100644 --- a/docs/source/Tutorial/static/static_syn.rst +++ b/docs/source/Tutorial/static/static_syn.rst @@ -4,7 +4,8 @@ 合成静态位移 ================= -Python中合成静态位移的主函数为 :func:`gen_syn_from_gf_*() ` (\*表示对不同震源)(与合成动态位移的函数共用) ,C模块为 :doc:`/Module/static_syn`。 +Python中合成静态位移的主函数为 :meth:`compute_static_syn() ` , +C模块为 :doc:`/Module/static_syn`。 使用上节计算的格林函数,合成静态位移。为方便画图,以下结果都使用ZNE分量。 diff --git a/docs/source/install.rst b/docs/source/install.rst index 4455c136..85ae5424 100755 --- a/docs/source/install.rst +++ b/docs/source/install.rst @@ -14,41 +14,46 @@ + |FFTW| *(其静态库已链接到预构建版本)* + |NetCDF| *(其静态库已链接到预构建版本)* + `Seismic Analysis Code (SAC) `_ ,需在对应网址申请下载。 - 用于用户进一步处理 SAC 格式的输出波形。 + 用于用户进一步处理 SAC 格式的输出波形(可选)。 -如果你想以Python脚本形式使用,依赖已在 :file:`setup.py` 中写好,直接使用 :command:`pip` 安装即可。 +以 Python 脚本使用时,其余依赖已在 :file:`setup.py` 中写好,直接 :command:`pip` 安装即可。 +Python 接口通过调用包内的 :command:`grt` 完成主计算;预构建安装包已按平台内置该可执行文件, +安装后即可使用,**无需再配置** :envvar:`PATH` 。 安装预构建版本 -------------------- -目前 **PyGRT** 已在 |gr| 中分发内置预构建二进制文件的安装包,用户仅需运行以下命令即可(建议使用 `conda `_ 虚拟环境) +目前 **PyGRT** 已在 |gr| 中分发不同平台的预构建安装包(内置对应平台的 :command:`grt` 与库文件)。 +用户仅需运行以下命令即可(建议使用 `conda `_ 虚拟环境) .. code-block:: bash pip install pygrt-kit -或者从 |gr| 中下载最新版本的程序压缩包(符合自己的操作系统),解压,在根目录下运行以下命令即可 +或者从 |gr| 中下载符合自己操作系统的程序压缩包,解压后在根目录运行 .. code-block:: bash pip install . -如果你不想使用Python,只想使用传统的命令行形式运行C程序,也可从 |gr| 中下载最新版本的 ``*.tar.gz`` 程序压缩包(选择自己的操作系统), -其中对于 Mac 用户,Apple 芯片下载 ``macosx_11_0_arm64`` 版本,Intel 芯片下载 ``macosx_10_9_x86_64`` 版本。 -下载解压后,其中 :rst:dir:`pygrt/C_extension/bin` 和 :rst:dir:`pygrt/C_extension/lib` 为预构建好的可执行文件目录和动态/静态库目录, -按自己习惯配置环境变量 :envvar:`PATH` 即可(详见下方)。 +安装完成后即可在 Python 中 ``import pygrt`` 使用。程序会自动定位安装目录内的 +:rst:dir:`pygrt/C_extension/bin/grt` ,不必额外配置环境变量。 +仅使用命令行 :command:`grt` +-------------------------------- +如果你不想使用 Python,只想在终端以命令行形式运行 C 程序,也可从 |gr| 下载对应平台的 +``*.tar.gz`` 压缩包(Mac 用户:Apple 芯片选 ``macosx_11_0_arm64`` ,Intel 芯片选 ``macosx_10_9_x86_64`` )。 +解压后,:rst:dir:`pygrt/C_extension/bin` 与 :rst:dir:`pygrt/C_extension/lib` 分别为预构建的可执行文件目录和库目录。 +此时需将 :rst:dir:`bin/` 加入环境变量 :envvar:`PATH` ,以便在终端直接调用 :command:`grt` 。 -环境变量配置 -------------- -如果你使用 :command:`pip` 安装后,想使用构建好的C程序 :command:`grt` ,需配置环境变量 :envvar:`PATH` 。运行以下命令 +使用 :command:`pip` 安装后若也希望在终端直接运行 :command:`grt` ,可先查看可执行文件路径: .. code-block:: bash python -m pygrt.print -输出 +输出形如 .. code-block:: text @@ -56,11 +61,8 @@ PyGRT executable file directory: PyGRT library directory: -将其中的 “PyGRT executable file directory” 路径添加到环境变量 :envvar:`PATH` 中即可。 - -如果是从 |gr| 上直接下载的压缩包,则只需将解压后的 :rst:dir:`bin/` 路径添加到环境变量 :envvar:`PATH` 中即可。 - -C程序 :command:`grt` 的运行独立于Python, :command:`grt` 的每个模块可使用 ``-h`` 查看帮助, 例如 :command:`grt greenfn -h` 。 +将其中的 “PyGRT executable file directory” 加入 :envvar:`PATH` 即可。 +各模块可用 ``-h`` 查看帮助,例如 :command:`grt greenfn -h` 。 从源码构建安装 @@ -107,7 +109,9 @@ C程序 :command:`grt` 的运行独立于Python, :command:`grt` 的每个模 make CC=gcc-14 - 成功后会在 :rst:dir:`bin/` 和 :rst:dir:`lib/` 路径下看到新构建出来的可执行文件和库文件。如果正确配置了 :envvar:`PATH` 可尝试运行 :command:`grt -h` 看能否正常打印帮助文档。再运行 + 成功后会在 :rst:dir:`bin/` 和 :rst:dir:`lib/` 路径下看到新构建出来的可执行文件和库文件。 + Python 侧会自动使用包内刚构建的 :command:`grt` ;若要在终端直接调用,将 :rst:dir:`bin/` 加入 :envvar:`PATH` , + 并运行 :command:`grt -h` 检查。然后可执行 .. code-block:: bash @@ -131,7 +135,3 @@ C程序 :command:`grt` 的运行独立于Python, :command:`grt` 的每个模 或者在运行 :command:`make` 命令时通过 ``CFLAGS2`` 临时增加 :command:`gcc` 的头文件搜索路径,例如:: make CFLAGS="-I/usr/local/include -I<其它路径> -I<其它路径>" - - - - diff --git a/docs/source/intro.rst b/docs/source/intro.rst index 3b4f7db7..e8ea401e 100644 --- a/docs/source/intro.rst +++ b/docs/source/intro.rst @@ -40,12 +40,11 @@ + **底层复杂运算完全由 C 语言重新实现**,并基于 `OpenMP `_ 进行并行优化,极大提升计算效率。 - + C 代码被编译链接成动态库 ``libgrt.so`` , **PyGRT** 再基于 Python 的 - `ctypes `_ 标准库实现对 C 库函数的调用。 - 再基于第三方库 `NumPy `_ 、 `SciPy `_ - 和 `ObsPy `_ ,用户可很方便地完成对 C 程序结果的数据整合、 - Fourier 变换、卷积、滤波、保存到 sac 文件等操作(例如 FFT 点数不再强制要求 2 次幂)。 - 借用 Python 语言的特点以及丰富成熟的第三方库,用户可灵活地实现后续的各种数据处理。 + + 计算主流程以可执行文件 **grt** 的形式提供。Python 接口负责组织模型与输出路径, + 并调用 **grt** 完成格林函数、合成与张量等计算;结果以 SAC / NetCDF 文件保存。 + 再基于第三方库 `NumPy `_ 、 `SciPy `_ + 和 `ObsPy `_ ,用户可方便地读回结果并做后续处理 + (例如卷积、滤波、积分微分等;FFT 点数不再强制要求 2 次幂)。 2. **C 程序 grt** @@ -82,7 +81,7 @@ - **含低速层模型的面波频散计算** : - + **自适应搜根策略** |ars2026| + + **自适应搜根策略** (|ars2026|) 6. **开源透明,持续维护** diff --git a/pygrt/C_extension/include/grt/common/travt.h b/pygrt/C_extension/include/grt/common/travt.h index 4a42f4e8..ac378930 100644 --- a/pygrt/C_extension/include/grt/common/travt.h +++ b/pygrt/C_extension/include/grt/common/travt.h @@ -25,4 +25,24 @@ */ real_t grt_compute_travt1d( const real_t *Thk, const real_t *Vel0, const size_t nlay, - const size_t isrc, const size_t ircv, const real_t dist); \ No newline at end of file + const size_t isrc, const size_t ircv, const real_t dist); + + +/** + * 从模型文件计算多个震中距的初至 P/S 走时 + * + * 返回长度为 2*nr 的数组,按 [Tp0, Ts0, Tp1, Ts1, ...] 排列 + * 调用方需用 grt_free1d 释放;失败返回 NULL + * + * @param[in] modelpath 一维分层模型文件路径 + * @param[in] depsrc 震源深度 (km) + * @param[in] deprcv 台站深度 (km) + * @param[in] rs 震中距数组 (km) + * @param[in] nr 震中距个数 + */ +real_t *grt_compute_travt1d_from_file( + const char *modelpath, + const real_t depsrc, + const real_t deprcv, + const real_t *rs, + const size_t nr); diff --git a/pygrt/C_extension/src/common/travt.c b/pygrt/C_extension/src/common/travt.c index 1595faa2..8d741cb1 100644 --- a/pygrt/C_extension/src/common/travt.c +++ b/pygrt/C_extension/src/common/travt.c @@ -8,7 +8,9 @@ */ #include +#include +#include "grt/common/model.h" #include "grt/common/travt.h" @@ -368,3 +370,38 @@ real_t grt_compute_travt1d( return travt; } + + +real_t *grt_compute_travt1d_from_file( + const char *modelpath, + const real_t depsrc, + const real_t deprcv, + const real_t *rs, + const size_t nr) +{ + if(modelpath == NULL || rs == NULL || nr == 0){ + return NULL; + } + + MODEL1D *mod1d = grt_read_mod1d_from_file(modelpath, depsrc, deprcv, true); + if(mod1d == NULL){ + return NULL; + } + + // 按 [Tp0, Ts0, Tp1, Ts1, ...] 交错排列 + real_t *out = (real_t*)malloc(sizeof(real_t) * nr * 2); + if(out == NULL){ + grt_free_mod1d(mod1d); + return NULL; + } + + for(size_t i = 0; i < nr; ++i){ + out[2*i] = grt_compute_travt1d( + mod1d->Thk, mod1d->Va, mod1d->n, mod1d->isrc, mod1d->ircv, rs[i]); + out[2*i + 1] = grt_compute_travt1d( + mod1d->Thk, mod1d->Vb, mod1d->n, mod1d->isrc, mod1d->ircv, rs[i]); + } + + grt_free_mod1d(mod1d); + return out; +} diff --git a/pygrt/__init__.py b/pygrt/__init__.py index 891023ef..c7013feb 100755 --- a/pygrt/__init__.py +++ b/pygrt/__init__.py @@ -2,18 +2,9 @@ # 版本号 from ._version import __version__ -# 和C库交互的结构体类型 -from .c_structures import * - -# 和C库交互的函数签名 -from .c_interfaces import * - # 1D水平分层模型类 from .pymod import * -# 格林函数类 -from .pygrn import * - from . import signals as sigs from . import utils diff --git a/pygrt/c_interfaces.py b/pygrt/c_interfaces.py index 9a5fce12..7ab5916a 100755 --- a/pygrt/c_interfaces.py +++ b/pygrt/c_interfaces.py @@ -1,239 +1,80 @@ """ - :file: c_interfaces.py - :author: Zhu Dengda (zhudengda@mail.iggcas.ac.cn) - :date: 2024-07-24 + :file: c_interfaces.py + :author: Zhu Dengda (zhudengda@mail.iggcas.ac.cn) + :date: 2024-07-24 - 该文件包括 C库的调用接口 + 该文件包括 C 库的调用接口 """ - import os -from ctypes import \ - c_double, c_float, c_int, c_size_t, c_bool, c_char_p, c_void_p,\ - POINTER, cdll +from ctypes import POINTER, c_char_p, c_double, c_float, c_int, c_size_t, c_void_p, cdll -from .c_structures import * FPOINTER = POINTER(c_float) IPOINTER = POINTER(c_int) +REAL = c_double +PREAL = POINTER(REAL) libgrt = cdll.LoadLibrary( os.path.join( - os.path.abspath(os.path.dirname(__file__)), - "C_extension/lib/libgrt.so")) -"""libgrt库""" - - -C_grt_prepare_grn_spec = libgrt.grt_prepare_grn_spec -"""动态格林函数:频谱积分前准备(Length/wI/freqs/KPROC/GRNSPEC 元数据)""" -C_grt_prepare_grn_spec.restype = None -C_grt_prepare_grn_spec.argtypes = [ - POINTER(c_MODEL1D), - c_size_t, PREAL, - c_size_t, REAL, REAL, c_bool, - REAL, REAL, - REAL, - REAL, REAL, REAL, - REAL, REAL, REAL, REAL, c_bool, - c_int, - REAL, REAL, c_bool, - c_bool, - POINTER(c_K_INTEG_PROCESS), - POINTER(c_GRNSPEC), -] - -C_grt_integ_grn_spec = libgrt.grt_integ_grn_spec -"""C库中计算格林函数的主函数 integ_grn_spec, 详见C API同名函数""" -C_grt_integ_grn_spec.argtypes = [POINTER(c_MODEL1D), POINTER(c_K_INTEG_PROCESS), POINTER(c_GRNSPEC), c_bool] - - -C_grt_prepare_static_grn = libgrt.grt_prepare_static_grn -"""静态格林函数:积分前准备(Length 默认 + KPROC)""" -C_grt_prepare_static_grn.restype = None -C_grt_prepare_static_grn.argtypes = [ - POINTER(c_MODEL1D), - c_size_t, PREAL, - REAL, - REAL, REAL, REAL, - REAL, REAL, c_bool, - c_int, - POINTER(c_K_INTEG_PROCESS), -] - -C_grt_integ_static_grn = libgrt.grt_integ_static_grn -"""计算静态格林函数""" -C_grt_integ_static_grn.restype = None -C_grt_integ_static_grn.argtypes = [ - POINTER(c_MODEL1D), c_size_t, PREAL, POINTER(c_K_INTEG_PROCESS), - c_bool, - POINTER((REAL*CHANNEL_NUM)*SRC_M_NUM), - POINTER((REAL*CHANNEL_NUM)*SRC_M_NUM), - POINTER((REAL*CHANNEL_NUM)*SRC_M_NUM), - c_char_p -] - - - -C_grt_static_syn_from_gf = libgrt.grt_static_syn_from_gf -"""由静态格林函数合成三分量位移场(及可选空间偏导),可按震中距插值到新的 north/east 接收点网格""" -C_grt_static_syn_from_gf.restype = None -C_grt_static_syn_from_gf.argtypes = [ - c_size_t, PREAL, c_size_t, PREAL, - c_size_t, PREAL, c_size_t, PREAL, - POINTER((REAL * CHANNEL_NUM) * SRC_M_NUM), - POINTER((REAL * CHANNEL_NUM) * SRC_M_NUM), - POINTER((REAL * CHANNEL_NUM) * SRC_M_NUM), - c_int, REAL, REAL, REAL * MECHANISM_NUM, - c_bool, c_bool, - POINTER(REAL * CHANNEL_NUM), POINTER((REAL * CHANNEL_NUM) * CHANNEL_NUM), -] - -C_grt_syn_from_gf = libgrt.grt_syn_from_gf -"""由动态格林函数合成三分量地震图(及可选空间偏导)""" -C_grt_syn_from_gf.restype = None -C_grt_syn_from_gf.argtypes = [ - c_size_t, c_float, - POINTER(FPOINTER * CHANNEL_NUM), - POINTER(FPOINTER * CHANNEL_NUM), - POINTER(FPOINTER * CHANNEL_NUM), - c_int, REAL, REAL, PREAL, REAL * MECHANISM_NUM, - c_bool, c_bool, - FPOINTER * CHANNEL_NUM, POINTER(FPOINTER * CHANNEL_NUM), -] - - -C_grt_compute_stress = libgrt.grt_compute_stress -"""由动态位移偏导合成应力张量""" -C_grt_compute_stress.restype = None -C_grt_compute_stress.argtypes = [ - c_size_t, c_float, c_float, c_float, c_float, c_float, c_float, c_float, - FPOINTER * CHANNEL_NUM, - POINTER(FPOINTER * CHANNEL_NUM), POINTER(FPOINTER * CHANNEL_NUM), - c_bool, -] - -C_grt_compute_strain = libgrt.grt_compute_strain -"""由动态位移偏导合成应变张量""" -C_grt_compute_strain.restype = None -C_grt_compute_strain.argtypes = [ - c_size_t, c_float, - FPOINTER * CHANNEL_NUM, - POINTER(FPOINTER * CHANNEL_NUM), POINTER(FPOINTER * CHANNEL_NUM), - c_bool, -] - -C_grt_compute_rotation = libgrt.grt_compute_rotation -"""由动态位移偏导合成旋转张量""" -C_grt_compute_rotation.restype = None -C_grt_compute_rotation.argtypes = [ - c_size_t, c_float, - FPOINTER * CHANNEL_NUM, - POINTER(FPOINTER * CHANNEL_NUM), POINTER(FPOINTER * CHANNEL_NUM), - c_bool, -] - -C_grt_static_compute_stress = libgrt.grt_static_compute_stress -"""由静态位移偏导合成应力张量""" -C_grt_static_compute_stress.restype = None -C_grt_static_compute_stress.argtypes = [ - c_size_t, c_size_t, PREAL, PREAL, - PREAL * CHANNEL_NUM, - POINTER(PREAL * CHANNEL_NUM), POINTER(PREAL * CHANNEL_NUM), - c_bool, REAL, REAL, -] + os.path.abspath(os.path.dirname(__file__)), + "C_extension/lib/libgrt.so", + ) +) +"""libgrt 库""" -C_grt_static_compute_strain = libgrt.grt_static_compute_strain -"""由静态位移偏导合成应变张量""" -C_grt_static_compute_strain.restype = None -C_grt_static_compute_strain.argtypes = [ - c_size_t, c_size_t, PREAL, PREAL, - PREAL * CHANNEL_NUM, - POINTER(PREAL * CHANNEL_NUM), POINTER(PREAL * CHANNEL_NUM), - c_bool, -] - -C_grt_static_compute_rotation = libgrt.grt_static_compute_rotation -"""由静态位移偏导合成旋转张量""" -C_grt_static_compute_rotation.restype = None -C_grt_static_compute_rotation.argtypes = [ - c_size_t, c_size_t, PREAL, PREAL, - PREAL * CHANNEL_NUM, - POINTER(PREAL * CHANNEL_NUM), POINTER(PREAL * CHANNEL_NUM), - c_bool, -] - - -C_grt_set_num_threads = libgrt.grt_set_num_threads -"""设置多线程数""" -C_grt_set_num_threads.restype = None -C_grt_set_num_threads.argtypes = [c_int] - - -def set_num_threads(n): - r''' - 定义计算使用的多线程数 - - :param n: 线程数 - ''' - C_grt_set_num_threads(n) - - -C_grt_compute_travt1d = libgrt.grt_compute_travt1d -"""计算1D层状半空间的初至波走时""" -C_grt_compute_travt1d.restype = REAL -C_grt_compute_travt1d.argtypes = [ - PREAL, PREAL, c_int, - c_int, c_int, REAL -] - - -C_grt_read_mod1d_from_file = libgrt.grt_read_mod1d_from_file -"""读取模型文件并进行预处理""" -C_grt_read_mod1d_from_file.restype = POINTER(c_MODEL1D) -C_grt_read_mod1d_from_file.argtypes = [c_char_p, c_double, c_double, c_bool] - -C_grt_set_mod1d_boundary = libgrt.grt_set_mod1d_boundary -"""设置模型边界条件并检查底界面""" -C_grt_set_mod1d_boundary.restype = None -C_grt_set_mod1d_boundary.argtypes = [POINTER(c_MODEL1D), c_int, c_int] -C_grt_free_mod1d = libgrt.grt_free_mod1d -"""释放C程序中申请的 GRT_MODEL1D 结构体内存""" -C_grt_free_mod1d.restype = None -C_grt_free_mod1d.argtypes = [POINTER(c_MODEL1D)] - -# ------------------------------------------------------------------- -# C函数定义的时间函数 -# ------------------------------------------------------------------- C_grt_free = libgrt.grt_free1d -"""释放在C中申请的内存""" +"""释放在 C 中申请的内存""" C_grt_free.restype = None C_grt_free.argtypes = [c_void_p] + C_grt_get_trap_wave = libgrt.grt_get_trap_wave """梯形波""" C_grt_get_trap_wave.restype = FPOINTER -C_grt_get_trap_wave.argtypes = [c_float, FPOINTER, FPOINTER, FPOINTER, IPOINTER] +C_grt_get_trap_wave.argtypes = [ + c_float, + FPOINTER, + FPOINTER, + FPOINTER, + IPOINTER, +] + C_grt_get_parabola_wave = libgrt.grt_get_parabola_wave """抛物波""" C_grt_get_parabola_wave.restype = FPOINTER C_grt_get_parabola_wave.argtypes = [c_float, FPOINTER, IPOINTER] + C_grt_get_ricker_wave = libgrt.grt_get_ricker_wave """雷克子波""" C_grt_get_ricker_wave.restype = FPOINTER C_grt_get_ricker_wave.argtypes = [c_float, c_float, IPOINTER] -# ------------------------------------------------------------------- -# 使用 C 函数求解 Lamb 问题 -# ------------------------------------------------------------------- C_grt_solve_lamb1 = libgrt.grt_solve_lamb1 """使用广义闭合解求解第一类 Lamb 问题""" C_grt_solve_lamb1.restype = None C_grt_solve_lamb1.argtypes = [ - REAL, PREAL, c_int, REAL, PREAL + REAL, + PREAL, + c_int, + REAL, + PREAL, +] + + +C_grt_compute_travt1d_from_file = libgrt.grt_compute_travt1d_from_file +"""从模型文件计算多个震中距的初至 P/S 走时""" +C_grt_compute_travt1d_from_file.restype = PREAL +C_grt_compute_travt1d_from_file.argtypes = [ + c_char_p, + REAL, + REAL, + PREAL, + c_size_t, ] diff --git a/pygrt/c_structures.py b/pygrt/c_structures.py deleted file mode 100755 index 23151c26..00000000 --- a/pygrt/c_structures.py +++ /dev/null @@ -1,164 +0,0 @@ -""" - :file: c_structures.py - :author: Zhu Dengda (zhudengda@mail.iggcas.ac.cn) - :date: 2024-07-24 - - 该文件包括 - 1、模型结构体的C接口 c_PyModel1D - 2、格林函数结构体的C接口 c_GRN - -""" - - -from ctypes import * - -__all__ = [ - "CHANNEL_NUM", - "QWV_NUM", - "INTEG_NUM", - "SRC_M_NUM", - "SRC_M_ORDERS", - "SRC_M_NAME_ABBR", - "ZRTchs", - "ZNEchs", - "qwvchs", - "MECHANISM_NUM", - - "NPCT_REAL_TYPE", - "NPCT_CMPLX_TYPE", - - "K_INTEG_CVGMET_DICT", - - "REAL", - "PREAL", - "CPLX", - "PCPLX", - - "c_MODEL1D", - "c_K_INTEG_PROCESS", - "c_GRNSPEC" -] - - -CHANNEL_NUM = 3 -QWV_NUM = 3 -INTEG_NUM = 4 -SRC_M_NUM = 6 -SRC_M_ORDERS = [0, 0, 1, 0, 1, 2] -SRC_M_NAME_ABBR = ["EX", "VF", "HF", "DD", "DS", "SS"] -ZRTchs = ['Z', 'R', 'T'] -ZNEchs = ['Z', 'N', 'E'] -qwvchs = ['q', 'w', 'v'] -MECHANISM_NUM = 6 - -NPCT_REAL_TYPE = 'f8' -NPCT_CMPLX_TYPE = 'c16' - -K_INTEG_CVGMET_DICT = { - 'AUTO': 0, - 'NONE': 1, - 'DCM': 2, - 'PTAM': 3 -} - - -class CPLX(Structure): - _fields_ = [ - ('real', c_double), - ('imag', c_double), - ] - - -REAL = c_double -PREAL = POINTER(REAL) -PCPLX = POINTER(CPLX) - - -class c_MODEL1D(Structure): - """ - 和C结构体 MODEL1D 作匹配 - """ - - _fields_ = [ - ('n', c_size_t), - ("depsrc", REAL), - ("deprcv", REAL), - ('isrc', c_size_t), - ('ircv', c_size_t), - ('ircvup', c_bool), - ('io_depth', c_bool), - ('srcrcv_isInserted', c_bool), - ('omgref', CPLX), - - ('Thk', PREAL), - ('Dep', PREAL), - ('Va', PREAL), - ('Vb', PREAL), - ('Rho', PREAL), - ('Qa', PREAL), - ('Qb', PREAL), - ('Qainv', PREAL), - ('Qbinv', PREAL), - ('isLiquid', POINTER(c_bool)), - - ('topbound', c_int), - ('botbound', c_int), - ] - - -class c_K_INTEG_PROCESS(Structure): - """ - 和C结构体 K_INTEG_PROCESS 作匹配 - - 波数积分参数。k0、ampk、vmin 共同确定搜索上界 kmax_ref; - 默认在 [dk, kmax_ref] 内基于核函数振幅搜索实际积分上限 kmax。 - """ - - _fields_ = [ - ('k0', REAL), - ('use_kmax_ref', c_bool), - ('ampk', REAL), - ('keps', REAL), - ('vmin', REAL), - - ('kcut', REAL), - ('kmax', REAL), - - ('dk', REAL), - - ('applyFIM', c_bool), - ('filondk', REAL), - - ('applySAFIM', c_bool), - ('sa_tol', REAL), - - ('cvgmet', c_int), - - ('fstats', c_void_p), - ('ptam_fstatsnr', c_void_p), - ] - -class c_GRNSPEC(Structure): - """ - 和 C 结构体 GRNSPEC 作匹配 - """ - - _fields_ = [ - ('nf', c_size_t), - ('freqs', PREAL), - ('nf1', c_size_t), - ('nf2', c_size_t), - ('nr', c_size_t), - ('rs', PREAL), - ('wI', REAL), - ('keepAllFreq', c_bool), - ('calc_upar', c_bool), - - ('u', POINTER((PCPLX*CHANNEL_NUM)*SRC_M_NUM)), - ('uiz', POINTER((PCPLX*CHANNEL_NUM)*SRC_M_NUM)), - ('uir', POINTER((PCPLX*CHANNEL_NUM)*SRC_M_NUM)), - - ('statsstr', c_char_p), - ('nstatsidxs', c_size_t), - ('statsidxs', POINTER(c_size_t)), - ] \ No newline at end of file diff --git a/pygrt/cli.py b/pygrt/cli.py new file mode 100644 index 00000000..eaba1589 --- /dev/null +++ b/pygrt/cli.py @@ -0,0 +1,92 @@ +""" + :file: cli.py + :author: Zhu Dengda (zhudengda@mail.iggcas.ac.cn) + :date: 2026-8-11 + + 调用 grt 命令行程序的工具 + +""" + +from __future__ import annotations + +import os +from pathlib import Path +import shutil +import subprocess +from typing import Iterable, Optional, Sequence, Union + + +def find_grt() -> str: + """Return the nearest available ``grt`` executable path.""" + bundled = Path(__file__).resolve().parent / "C_extension" / "bin" / "grt" + if bundled.is_file(): + return str(bundled) + + from_path = shutil.which("grt") + if from_path: + return from_path + + raise FileNotFoundError( + "The grt executable was not found. Build it with `make -C pygrt/C_extension CC=gcc-16 -j`." + ) + + +def run_grt( + args: Sequence[object], + *, + cwd: Optional[Union[str, os.PathLike]] = None, + print_log: bool = True, +) -> None: + """ + Run one ``grt`` module and raise a detailed error on failure. + + :param args: Arguments passed to the ``grt`` command. + :param cwd: Working directory used to run the command. + :param print_log: If true, stream ``grt`` stdout/stderr to the terminal. + If false, capture them and attach any output to the + raised error on failure. + """ + command = [find_grt(), *(str(arg) for arg in args)] + if print_log: + completed = subprocess.run(command, cwd=cwd, check=False) + if completed.returncode != 0: + raise RuntimeError(f"grt command failed with exit code {completed.returncode}: {' '.join(command)}") + return + + completed = subprocess.run(command, cwd=cwd, check=False, text=True, capture_output=True) + if completed.returncode != 0: + detail = completed.stderr.strip() or completed.stdout.strip() + message = f"grt command failed with exit code {completed.returncode}: {' '.join(command)}" + if detail: + message += f"\n{detail}" + raise RuntimeError(message) + + +def format_float(value: float) -> str: + """ + Format a numerical CLI value without unnecessary trailing zeros. + + :param value: Numerical value to format. + + :return: Formatted command-line value. + """ + return format(float(value), ".15g") + + +def format_range(values: Iterable[float], name: str) -> str: + """ + Format three values for a ``-X`` or ``-Y`` option. + + :param values: Three values corresponding to one CLI coordinate option. + :param name: Name used in the validation error message. + + :return: Slash-separated command-line value. + """ + try: + values = list(values) + except TypeError: + raise ValueError(f"{name} must contain exactly three values.") + if len(values) != 3: + raise ValueError(f"{name} must contain exactly three values.") + + return "/".join(format_float(value) for value in values) diff --git a/pygrt/pygrn.py b/pygrt/pygrn.py deleted file mode 100755 index 3dde0c91..00000000 --- a/pygrt/pygrn.py +++ /dev/null @@ -1,165 +0,0 @@ -""" - :file: pygrn.py - :author: Zhu Dengda (zhudengda@mail.iggcas.ac.cn) - :date: 2024-07-24 - - 该文件包括 Python端使用的格林函数 :class:`pygrt.pygrn.PyGreenFunction` - -""" - -from __future__ import annotations -import numpy as np -import matplotlib.pyplot as plt -import numpy.ctypeslib as npct -from obspy import read, Stream, Trace, UTCDateTime -from obspy.io.sac import SACTrace -from scipy.fft import irfft -from typing import List, Dict - - - -from ctypes import * -from .c_interfaces import * -from .c_structures import * - - -__all__ = [ - "PyGreenFunction", -] - - -class PyGreenFunction: - def __init__( - self, - name:str, - nt:int, - dt:float, - upsampling_n:int, - freqs:np.ndarray, - wI:float, - dist:float, - depsrc:float, - deprcv:float): - ''' - :param name: source-type (EX,VF,HF,DD,DS,SS) + component (Z,R,T) - :param nt: number of time points - :param dt: time interval (s) - :param upsampling_n: upsampling factor - :param freqs: frequency array (Hz) - :param wI: imaginary angular frequency `wI`,omega = w - j*wI - :param dist: epicentral distance (km) - :param depsrc: source depth (km) - :param deprcv: receiver depth (km) - ''' - - # 频率点 - self.freqs = freqs # 未copy,共享内存 - self.freqs.flags.writeable = False # 不允许修改内部值 - - self.name = name - self.nt = nt - self.dt = dt - self.upsampling_n = upsampling_n - self.wI = wI - self.dist = dist - self.depsrc = depsrc - self.deprcv = deprcv - - nf = len(self.freqs) - - # 频谱numpy数据 - self.cmplx_grn = np.zeros((nf,), dtype=NPCT_CMPLX_TYPE) - - # 虚频率 - self.wI = wI - - # 提前建立Trace时间序列 - self.SACTrace = SACTrace(npts=nt*upsampling_n, delta=dt/upsampling_n, iztype='io') - sac = self.SACTrace - sac.evdp = depsrc - sac.stel = (-1)*deprcv - sac.dist = dist - sac.user0 = wI # 记录虚频率 - sac.kstnm = 'SYN' - sac.kcmpnm = name - - - def plot_response(self): - ''' - plot the frequency response, including amplitude response and phase response - ''' - amp = np.abs(self.cmplx_grn) - phi = np.angle(self.cmplx_grn) - - freqs = self.freqs - - fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(8, 6), gridspec_kw=dict(hspace=0.5)) - ax1.plot(freqs, amp, 'k', lw=0.6) - ax1.set_xlabel("Frequency (Hz)") - ax1.set_ylabel("Amplitude") - ax1.grid() - - - ax2.plot(freqs, phi, 'k', lw=0.6) - ax2.set_xlabel("Frequency (Hz)") - ax2.set_ylabel("Phase") - ax2.set_yticks([-np.pi, 0, np.pi], ['$-\pi$', '$0$', '$\pi$']) - ax2.grid() - - return fig, (ax1, ax2) - - - def freq2time(self, T0:float, travtP:float, travtS:float, mult:float=1.0, skipImagComps:bool=False): - ''' - Convert the Green's function from the frequency domain to the time domain - and return it in the form of :class:`obspy.Trace` - - :param T0: The offset (secs) of the starttime w.r.t the event origin, - for example, T0=5 denotes recording began 5 seconds after the origin. - - :return: - - **tr**: :class:`obspy.Trace` Green's function - ''' - - self.cmplx_grn[:] *= mult - - freqs = self.freqs - - df = freqs[-1] - freqs[-2] - sac = self.SACTrace - nt = sac.npts # 可能考虑升采样的点数 - dt = sac.delta # 可能考虑升采样的采样间隔 - wI = sac.user0 - - T = nt*dt - if not np.isclose(T*df, 1.0): - raise ValueError(f"{sac.kcmpnm} length of window not match the freq interval.") - - omegas = 2*np.pi*freqs - - cmlx_grn = self.cmplx_grn * np.exp(1j*omegas*T0) # 时移 - - # 实序列的傅里叶变换 - data = irfft(cmlx_grn, nt, norm='backward') * (1/dt) # *(1/dt)和连续傅里叶变换幅值保持一致 - # 抵消虚频率的影响 - if not skipImagComps: - data *= np.exp((np.arange(0,nt)*dt + T0)*wI) - - # 保存sac头段变量 - sac.o = 0.0 - sac.b = T0 - # 记录走时 - sac.kt0 = 'P' - sac.t0 = travtP - sac.kt1 = 'S' - sac.t1 = travtS - # 记录时域数据 - tr = sac.to_obspy_trace() - tr.data = data - - - return tr - - - - \ No newline at end of file diff --git a/pygrt/pymod.py b/pygrt/pymod.py index c9080f75..d2ed159c 100755 --- a/pygrt/pymod.py +++ b/pygrt/pymod.py @@ -3,657 +3,801 @@ :author: Zhu Dengda (zhudengda@mail.iggcas.ac.cn) :date: 2024-07-24 - 该文件包括 Python端使用的模型 :class:`pygrt.c_structures.c_PyModel1D` + 该文件包括 Python 端使用的基于文件的模型 :class:`PyModel1D` """ - from __future__ import annotations -from multiprocessing import Value + +import os +from ctypes import c_size_t, cast, c_void_p +from pathlib import Path +from typing import Dict, Iterable, Optional, Sequence, Union + import numpy as np import numpy.ctypeslib as npct -from obspy import read, Stream, Trace, UTCDateTime -from scipy.fft import irfft, ifft -from obspy.core import AttribDict -from typing import List, Dict, Union, Literal -import tempfile -import os -import warnings +from obspy import read + +from .cli import format_float, format_range, run_grt +from .c_interfaces import C_grt_compute_travt1d_from_file, C_grt_free, PREAL +from .utils import read_static_nc + + +PathLike = Union[str, os.PathLike] + +__all__ = ["PyModel1D"] -from time import time -from copy import deepcopy -from ctypes import Array, pointer, c_char_p -from ctypes import _Pointer -from .c_interfaces import * -from .c_structures import * -from .pygrn import PyGreenFunction +def _normalize_distarr(distarr): + """ + 将 distarr 规范为一维 float64 数组 -__all__ = [ - "PyModel1D", -] + 仅接受标量浮点数或一维浮点序列;字符串等类型直接拒绝 + """ + if isinstance(distarr, (str, bytes)): + raise TypeError("distarr must be a float or a 1-D sequence of floats, not a string.") + arr = np.asarray(distarr, dtype=np.float64) + if arr.ndim == 0: + return True, np.ascontiguousarray([float(arr)], dtype=np.float64) + if arr.ndim == 1: + return False, np.ascontiguousarray(arr, dtype=np.float64) + raise ValueError("distarr must be a scalar or a 1-D sequence of floats.") class PyModel1D: - def __init__(self, modarr0:np.ndarray, depsrc:float, deprcv:float, allowLiquid:bool=True, - topbound:Literal['free', 'rigid', 'halfspace']='free', - botbound:Literal['free', 'rigid', 'halfspace']='halfspace'): - ''' - Create 1D model instance, and insert the imaginary layer of source and receiver. - - :param modarr0: model array, in the format of [thickness(km), Vp(km/s), Vs(km/s), Rho(g/cm^3), Qp, Qs] - :param depsrc: source depth (km) - :param deprcv: receiver depth (km) - :param allowLiquid: (deprecated) unused argument - :param topbound: boundary condition of the top layer - :param botbound: boundary condition of the bottom layer - - ''' - self.depsrc:float = depsrc - self.deprcv:float = deprcv - self.c_mod1d:c_MODEL1D - self.topbound:str = topbound - self.botbound:str = botbound - self.hasLiquid = False - - if depsrc < 0.0 or deprcv < 0.0: - raise ValueError("Negative source depth or receiver depth is not supported.") - - boundDct = { - 'free': 0, - 'rigid': 1, - 'halfspace': 2 - } + """ + File-based 1D layered model for GRT calculations. + + Typical workflow: + + 1. Create the model from a layered-model file. + 2. Call :meth:`set_dynamic_grn_path` or :meth:`set_static_grn_path`. + 3. Compute Green's functions with :meth:`compute_grn` or :meth:`compute_static_grn`. + 4. Synthesize waveforms or static fields with :meth:`compute_syn` or :meth:`compute_static_syn`. + """ - if topbound not in boundDct: + def __init__( + self, + modelpath: PathLike, + topbound: str = "free", + botbound: str = "halfspace", + ): + """ + Create a file-based 1D layered model. + + The model file is a plain text table. Each row is one layer in the form + ``thickness(km) Vp(km/s) Vs(km/s) Rho(g/cm^3) [Qp Qs]``. + A zero thickness marks a half-space bottom layer. + + :param modelpath: Path to the layered model file. + :param topbound: Top boundary condition. One of ``free``, ``rigid`` and ``halfspace``. + :param botbound: Bottom boundary condition. One of ``free``, ``rigid`` and ``halfspace``. + """ + self.modelpath = str(Path(modelpath)) + self.topbound = topbound + self.botbound = botbound + self.dynamic_grn_path: Optional[str] = None + self.static_grn_path: Optional[str] = None + + if not Path(self.modelpath).is_file(): + raise FileNotFoundError(f"Model file does not exist: {self.modelpath}") + if topbound not in {"free", "rigid", "halfspace"}: raise ValueError(f"Unsupported topbound={topbound}.") - if botbound not in boundDct: + if botbound not in {"free", "rigid", "halfspace"}: raise ValueError(f"Unsupported botbound={botbound}.") - # 将modarr写入临时数组 - with tempfile.NamedTemporaryFile(mode='w', delete=False) as tmpfile: - ncol = modarr0.shape[-1] - np.savetxt(tmpfile, modarr0.reshape(-1, ncol), "%.15e") - tmp_path = tmpfile.name # 获取临时文件路径 - - try: - c_mod1d_ptr = C_grt_read_mod1d_from_file(tmp_path.encode("utf-8"), depsrc, deprcv, True) - self.c_mod1d = c_mod1d_ptr.contents # 这部分内存在C中申请,需由C函数释放。占用不多,这里跳过 - C_grt_set_mod1d_boundary(self.c_mod1d, boundDct[topbound], boundDct[botbound]) - finally: - if os.path.exists(tmp_path): - os.unlink(tmp_path) - - # 设置边界条件 - self.c_mod1d.topbound = boundDct[topbound] - self.c_mod1d.botbound = boundDct[botbound] - - self.isrc = self.c_mod1d.isrc - self.ircv = self.c_mod1d.ircv - - va = npct.as_array(self.c_mod1d.Va, (self.c_mod1d.n,)) - vb = npct.as_array(self.c_mod1d.Vb, (self.c_mod1d.n,)) - if np.any(vb == 0.0): - self.hasLiquid = True - - self.vmin = min(np.min(va), np.min(vb)) - # 最小非零速度 - nonzero_vb = vb[vb > 0] - self.vmin = min(np.min(va), np.min(nonzero_vb)) if nonzero_vb.size else np.min(va) - self.vmax = max(np.max(va), np.max(vb)) - - - def compute_travt1d(self, dist:float): + def compute_travt1d( + self, + *, + depsrc: float, + deprcv: float, + distarr: Union[float, Sequence[float]], + ): r""" - Call the C function to calculate the travel time of the first P-wave and S-wave + Compute first-arrival P- and S-wave travel times. + + Calls the C routine ``grt_compute_travt1d_from_file``, which reads the + layered model from ``modelpath`` and evaluates travel times at the given + source/receiver depths and epicentral distances. All arguments must be + passed by keyword. - :param dist: epicentral distance (km) + :param depsrc: Source depth in km. + :param deprcv: Receiver depth in km. + :param distarr: Epicentral distance(s) in km. A scalar or a sequence of distances. - :return: - - **travtP** - first P-wave arrival (s) - - **travtS** - first S-wave arrival (s) + :return: ``(travtP, travtS)`` in s. For a scalar distance both are floats; + for multiple distances both are NumPy arrays of shape ``(n,)``. """ - travtP = C_grt_compute_travt1d( - self.c_mod1d.Thk, - self.c_mod1d.Va, - self.c_mod1d.n, - self.c_mod1d.isrc, - self.c_mod1d.ircv, - dist - ) - travtS = C_grt_compute_travt1d( - self.c_mod1d.Thk, - self.c_mod1d.Vb, - self.c_mod1d.n, - self.c_mod1d.isrc, - self.c_mod1d.ircv, - dist + if depsrc < 0 or deprcv < 0: + raise ValueError("Source and receiver depths must be nonnegative.") + + # 标量震中距返回 float,序列返回长度为 n 的数组 + single, distances = _normalize_distarr(distarr) + if distances.size == 0 or np.any(distances < 0): + raise ValueError("distarr must contain nonnegative distances.") + + carr = C_grt_compute_travt1d_from_file( + self.modelpath.encode("utf-8"), + float(depsrc), + float(deprcv), + distances.ctypes.data_as(PREAL), + c_size_t(distances.size), ) + if cast(carr, c_void_p).value is None: + raise RuntimeError(f"Failed to compute travel times for model {self.modelpath}.") - return travtP, travtS + arr = npct.as_array(carr, shape=(distances.size, 2)).copy() + C_grt_free(carr) + if single: + return float(arr[0, 0]), float(arr[0, 1]) + return arr[:, 0].copy(), arr[:, 1].copy() - def _init_grn( - self, - distarr:np.ndarray, - nt:int, dt:float, upsampling_n:int, freqs:np.ndarray, wI:float, prefix:str=''): - - ''' - 建立各个震源对应的格林函数类 - ''' - - depsrc = self.depsrc - deprcv = self.deprcv - nr = len(distarr) - - pygrnLst:List[List[List[PyGreenFunction]]] = [] - c_grnArr = (((PCPLX*CHANNEL_NUM)*SRC_M_NUM)*nr)() - - for ir in range(len(distarr)): - dist = distarr[ir] - pygrnLst.append([]) - for isrc in range(SRC_M_NUM): - pygrnLst[ir].append([]) - for ic, comp in enumerate(ZRTchs): - - pygrn = PyGreenFunction(f'{prefix}{SRC_M_NAME_ABBR[isrc]}{comp}', nt, dt, upsampling_n, freqs, wI, dist, depsrc, deprcv) - pygrnLst[ir][isrc].append(pygrn) - c_grnArr[ir][isrc][ic] = pygrn.cmplx_grn.ctypes.data_as(PCPLX) - - return pygrnLst, c_grnArr - - - def gen_gf_spectra(self, *args, **kwargs): - raise NameError("Function 'gen_gf_spectra()' has been removed, use 'compute_grn' instead.") - - def _get_grn_spectra( - self, - distarr:np.ndarray, - nt:int, - dt:float, - upsampling_n:int = 1, - freqband:Union[np.ndarray,List[float]]=[-1,-1], - zeta:float=0.8, - keepAllFreq:bool=False, - vmin_ref:float=0.0, - keps:float=-1.0, - ampk:float=2.0, - k0:float=50.0, - use_kmax_ref:bool=False, - Length:float=0.0, - filonLength:float=0.0, - safilonTol:float=0.0, - filonCut:float=0.0, - converg_method:Literal['AUTO', 'NONE', 'DCM', 'PTAM']='AUTO', - delayT0:float=0.0, - delayV0:float=0.0, - calc_upar:bool=False, - statsfile:Union[str,None]=None, - statsidxs:Union[np.ndarray,List[int],None]=None, - print_log:bool=True - ): - # 仅做最基本的正负号等检查;物理预处理交给 C grt_prepare_grn_spec - if np.any(distarr < 0): - raise ValueError(f"distarr < 0") - if nt < 0: - raise ValueError(f"nt ({nt}) < 0") - if dt < 0: - raise ValueError(f"dt ({dt}) < 0") - if zeta < 0: - raise ValueError(f"zeta ({zeta}) < 0") - if k0 < 0: - raise ValueError(f"k0 ({k0}) < 0") - if vmin_ref < 0: - raise ValueError(f"vmin_ref ({vmin_ref}) < 0") - - if Length < 0.0: - raise ValueError(f"Length ({Length}) < 0") - if filonLength < 0.0: - raise ValueError(f"filonLength ({filonLength}) < 0") - if filonCut < 0.0: - raise ValueError(f"filonCut ({filonCut}) < 0") - if safilonTol < 0.0: - raise ValueError(f"safilonTol ({safilonTol}) < 0") - - # 只能设置一种filon积分方法 - if safilonTol > 0.0 and filonLength > 0.0: - raise ValueError(f"You should only set one of filonLength and safilonTol.") - - f1, f2 = freqband - if f1 >= f2 and f1 >= 0 and f2 >= 0: - raise ValueError(f"freqband f1({f1}) >= f2({f2})") - - distarr = np.asarray(distarr, dtype=NPCT_REAL_TYPE) - nrs = len(distarr) - c_rs = npct.as_ctypes(distarr) - - KPROC = c_K_INTEG_PROCESS() - grn = c_GRNSPEC() - C_grt_prepare_grn_spec( - self.c_mod1d, - nrs, c_rs, - nt, dt, zeta, keepAllFreq, - float(f1), float(f2), - Length, - filonLength, safilonTol, filonCut, - k0, ampk, keps, vmin_ref, use_kmax_ref, - K_INTEG_CVGMET_DICT[converg_method.upper()], - delayT0, delayV0, False, - calc_upar, - pointer(KPROC), pointer(grn), - ) + def set_dynamic_grn_path(self, path: PathLike) -> str: + """ + Set and create the root directory for dynamic Green's functions. - try: - nf = grn.nf - freqs = npct.as_array(grn.freqs, shape=(nf,)).copy() - freqs.flags.writeable = False - wI = float(grn.wI) - - # 初始化格林函数(缓冲仍由 Python 持有) - pygrnLst, c_grnArr = self._init_grn(distarr, nt, dt, upsampling_n, freqs, wI, '') - - pygrnLst_uiz = [] - c_grnArr_uiz = None - pygrnLst_uir = [] - c_grnArr_uir = None - if calc_upar: - pygrnLst_uiz, c_grnArr_uiz = self._init_grn(distarr, nt, dt, upsampling_n, freqs, wI, 'z') - pygrnLst_uir, c_grnArr_uir = self._init_grn(distarr, nt, dt, upsampling_n, freqs, wI, 'r') - - c_statsfile = None - if statsfile is not None: - os.makedirs(statsfile, exist_ok=True) - c_statsfile = c_char_p(statsfile.encode('utf-8')) - - if statsidxs is None: - statsidxs = np.arange(nf) - - statsidxs = np.array(statsidxs) - if np.any(statsidxs < 0): - raise ValueError("negative value in statsidxs is not supported.") - - c_statsidxs = npct.as_ctypes(np.array(statsidxs).astype(np.uint64)) # size_t - nstatsidxs = len(statsidxs) - else: - c_statsidxs = None - nstatsidxs = 0 - - grn.u = c_grnArr - grn.uiz = c_grnArr_uiz - grn.uir = c_grnArr_uir - grn.statsstr = c_statsfile - grn.nstatsidxs = nstatsidxs - grn.statsidxs = c_statsidxs - - # 运行C库函数 - #///////////////////////////////////////////////////////////////////////////////// - # 计算得到的格林函数的单位: - # 单力源 HF[ZRT],VF[ZR] 1e-15 cm/dyne - # 爆炸源 EX[ZR] 1e-20 cm/(dyne*cm) - # 剪切源 DD[ZR],DS[ZRT],SS[ZRT] 1e-20 cm/(dyne*cm) - #================================================================================= - C_grt_integ_grn_spec(self.c_mod1d, pointer(KPROC), pointer(grn), print_log) - #================================================================================= - #///////////////////////////////////////////////////////////////////////////////// - finally: - if grn.freqs: - C_grt_free(grn.freqs) - grn.freqs = None - - return pygrnLst, pygrnLst_uiz, pygrnLst_uir - - def _get_stream_from_grn_spectra( - self, distarr, pygrnLst, pygrnLst_uiz, pygrnLst_uir, - delayT0:float=0.0, - delayV0:float=0.0, - skipImagComps:bool=False, - calc_upar:bool=False, - gf_source=['EX', 'VF', 'HF', 'DC'] - ): - depsrc = self.depsrc - deprcv = self.deprcv - - calc_EX:bool = 'EX' in gf_source - calc_VF:bool = 'VF' in gf_source - calc_HF:bool = 'HF' in gf_source - calc_DC:bool = 'DC' in gf_source - - # 震源和场点层的物性,写入sac头段变量 - rcv_va = self.c_mod1d.Va[self.ircv] - rcv_vb = self.c_mod1d.Vb[self.ircv] - rcv_rho = self.c_mod1d.Rho[self.ircv] - rcv_qainv = self.c_mod1d.Qainv[self.ircv] - rcv_qbinv = self.c_mod1d.Qbinv[self.ircv] - src_va = self.c_mod1d.Va[self.isrc] - src_vb = self.c_mod1d.Vb[self.isrc] - src_rho = self.c_mod1d.Rho[self.isrc] - - # 对应实际采集的地震信号,取向上为正(和理论推导使用的方向相反) - dataLst = [] - for ir in range(len(distarr)): - stream = Stream() - dist = distarr[ir] - - # 计算延迟 - delayT = delayT0 - if delayV0 > 0.0: - delayT += np.hypot(dist, deprcv-depsrc)/delayV0 - - # 计算走时 - travtP, travtS = self.compute_travt1d(dist) - - for im in range(SRC_M_NUM): - if(not calc_EX and im==0): - continue - if(not calc_VF and im==1): - continue - if(not calc_HF and im==2): - continue - if(not calc_DC and im>=3): - continue - - modr = SRC_M_ORDERS[im] - sgn = 1 - for c in range(CHANNEL_NUM): - if(modr==0 and ZRTchs[c]=='T'): - continue - - sgn = -1 if ZRTchs[c]=='Z'=='Z' else 1 - stream.append(pygrnLst[ir][im][c].freq2time(delayT, travtP, travtS, sgn, skipImagComps)) - if(calc_upar): - stream.append(pygrnLst_uiz[ir][im][c].freq2time(delayT, travtP, travtS, sgn*(-1), skipImagComps)) - stream.append(pygrnLst_uir[ir][im][c].freq2time(delayT, travtP, travtS, sgn , skipImagComps)) - - - # 在sac头段变量部分 - for tr in stream: - SAC = tr.stats.sac - SAC['user1'] = rcv_va - SAC['user2'] = rcv_vb - SAC['user3'] = rcv_rho - SAC['user4'] = rcv_qainv - SAC['user5'] = rcv_qbinv - SAC['user6'] = src_va - SAC['user7'] = src_vb - SAC['user8'] = src_rho - - dataLst.append(stream) - - return dataLst + Later calls to :meth:`compute_grn` write SAC files under this directory. + Subdirectories are named + ``{model}_{depsrc}_{deprcv}_{distance}``. + :param path: Root directory for dynamic Green's functions. - def compute_grn( - self, - distarr:Union[np.ndarray,List[float],float], - nt:int, - dt:float, - upsampling_n:int = 1, - freqband:Union[np.ndarray,List[float]]=[-1,-1], - zeta:float=0.8, - keepAllFreq:bool=False, - vmin_ref:float=0.0, - keps:float=-1.0, - ampk:float=2.0, - k0:float=50.0, - use_kmax_ref:bool=False, - Length:float=0.0, - filonLength:float=0.0, - safilonTol:float=0.0, - filonCut:float=0.0, - converg_method:Literal['AUTO', 'NONE', 'DCM', 'PTAM']='AUTO', - delayT0:float=0.0, - delayV0:float=0.0, - skipImagComps:bool=False, - calc_upar:bool=False, - gf_source=['EX', 'VF', 'HF', 'DC'], - statsfile:Union[str,None]=None, - statsidxs:Union[np.ndarray,List[int],None]=None, - print_log:bool=True): - - r''' - Call the C function to calculate the Green's functions at multiple distances and return them in a list, - where each element is in the form of :class: 'obspy.Stream' type. - - :param distarr: array of epicentral distances (km), or a single float - :param nt: number of time points. with the help of `SciPy`, nt no longer needs to be a power of 2 - :param dt: time interval (s) - :param upsampling_n: upsampling factor - :param freqband: frequency range (Hz) - :param zeta: zeta is used to define the imaginary angular frequency, - :math:`\tilde{\omega} = \omega - j*w_I, w_I = \zeta*\pi/T, T=nt*dt` . - see Bouchon (1981) and 张海明 (2021) for more details and tests. - :param keepAllFreq: calculate all frequency points, no matter how low the frequency is - :param vmin_ref: minimum reference velocity (km/s). - the default vmin=max(minimum velocity, 0.1), used to define kmax_ref - :param keps: automatic convergence condition, see Yao and Harkrider (1983) for more details. - negative value denotes not use. - :param ampk: amplification factor in kmax_ref, see below. - :param k0: coefficient in kmax_ref - :math:`k_{\text{max,ref}}=\sqrt{(k_{0}*\pi/hs)^2 + (ampk*\omega/vmin_{ref})^2}` , - hs=max(abs(depsrc-deprcv),0.1). - The actual kmax is searched in [dk, kmax_ref] based on kernel amplitude; - if the search reaches kmax_ref without convergence, - or source and receiver are at the same depth, DCM is applied in Auto mode. - :param use_kmax_ref: directly use kmax_ref as kmax, without amplitude search - :param Length: integration step `dk=2\pi / (L*rmax)`, see Bouchon (1981) and 张海明 (2021) for the criterion, default set automatically. - :param filonLength: integration step of Fixed-Interval Filon's Integration Method (large distance only; not for r=0) - :param safilonTol: precision of Self-Adaptive Filon's Integration Method (large distance only; not for r=0) - :param filonCut: The splitting point of DWM and (SA)FIM, k*=/rmax, default is 0 - :param converg_method: The method of explicit convergence, you can set "AUTO", "NONE", "DCM" or "PTAM". Default use "AUTO". - :param skipImagComps: skip the amplitude compensation from imaginary frequency. - :param calc_upar: whether calculate the spatial derivatives of displacements. - :param gf_source: The source type to be calculated - :param statsfile: directory path for saving the statsfile during k integral, used to debug or observe the variations of :math:`F(k,\omega)` and :math:`F(k,\omega)J_m(kr)k` - :param statsidxs: only output the statsfile at specific frequency indexes. It is recommended to specify the indexes; - otherwise, by default, statsfiles of all frequency will be output, which probably occupy a lot of disk space - :param print_log: whether print calculation logs. - - :return: - - **dataLst** - Green's Functions at multiple distances, in a list of :class:`obspy.Stream` - - ''' - - if isinstance(distarr, float) or isinstance(distarr, int): - distarr = np.array([distarr*1.0]) - - distarr = np.array(distarr) - distarr = distarr.copy().astype(NPCT_REAL_TYPE) - - pygrnLst, pygrnLst_uiz, pygrnLst_uir = self._get_grn_spectra( - distarr, nt, dt, upsampling_n, freqband, zeta, keepAllFreq, - vmin_ref, keps, ampk, k0, use_kmax_ref, Length, filonLength, safilonTol, filonCut, converg_method, - delayT0, delayV0, calc_upar, - statsfile, statsidxs, print_log - ) + :return: The configured dynamic Green's function directory. + """ + target = Path(path) + target.mkdir(parents=True, exist_ok=True) + self.dynamic_grn_path = str(target) + return self.dynamic_grn_path - dataLst = self._get_stream_from_grn_spectra( - distarr, pygrnLst, pygrnLst_uiz, pygrnLst_uir, - delayT0, delayV0, skipImagComps, calc_upar, gf_source - ) - - return dataLst + def set_static_grn_path(self, path: PathLike) -> str: + """ + Set the NetCDF file path for static Green's functions. - + Later calls to :meth:`compute_static_grn` write (and currently overwrite) + this file. Parent directories are created if needed. - def compute_static_grn( - self, - norths:Union[np.ndarray,List[float],float,None]=None, - easts:Union[np.ndarray,List[float],float,None]=None, - distarr:Union[np.ndarray,List[float],float,None]=None, - keps:float=-1.0, - k0:float=50.0, - use_kmax_ref:bool=False, - Length:float=15.0, - filonLength:float=0.0, - safilonTol:float=0.0, - filonCut:float=0.0, - converg_method:Literal['AUTO', 'NONE', 'DCM', 'PTAM']='AUTO', - calc_upar:bool=False, - statsfile:Union[str,None]=None, - xarr:Union[np.ndarray,List[float],float,None]=None, - yarr:Union[np.ndarray,List[float],float,None]=None): + :param path: NetCDF file path for static Green's functions. + :return: The configured static Green's function file path. + """ + target = Path(path) + target.parent.mkdir(parents=True, exist_ok=True) + self.static_grn_path = str(target) + return self.static_grn_path + + def compute_grn( + self, + *, + depsrc: float, + deprcv: float, + distarr: Union[float, Sequence[float]], + nt: int, + dt: float, + upsampling_n: int = 1, + freqband: Sequence[float] = (-1.0, -1.0), + zeta: float = 0.8, + keepAllFreq: bool = False, + vmin_ref: float = 0.0, + keps: float = -1.0, + ampk: float = 2.0, + k0: float = 50.0, + use_kmax_ref: bool = False, + Length: float = 0.0, + filonLength: float = 0.0, + safilonTol: float = 0.0, + filonCut: float = 0.0, + converg_method: str = "AUTO", + delayT0: float = 0.0, + delayV0: float = 0.0, + ref_first_p: bool = False, + skipImagComps: bool = False, + calc_upar: bool = False, + gf_source: Optional[Iterable[str]] = None, + statsidxs: Optional[Sequence[int]] = None, + print_log: bool = True, + ): r""" - Call the C function to calculate the static Green's functions and return them in a dict. - There're two ways to define the "epicentral distances": - 1. set both ``norths`` and ``easts`` to define a north/east grid in advance. - 2. simply set ``distarr``, which equals ``norths=[0.0], easts=distarr``. - - :param norths: coordinate array in the north direction (km), or a single float. - :param easts: coordinate array in the east direction (km), or a single float. - :param xarr: deprecated alias of ``norths`` - :param yarr: deprecated alias of ``easts`` - :param distarr: equal to "norths=[0.0], easts=distarr" - :param keps: automatic convergence condition, see (Yao and Harkrider (1983) for more details. - negative value denotes not use. - :param k0: coefficient in kmax_ref :math:`k_{\text{max,ref}}=k_{0}*\pi/hs`, - hs=max(abs(depsrc-deprcv),0.1). - The actual kmax is searched in [dk, kmax_ref] based on kernel amplitude; - if the search reaches kmax_ref without convergence, - or source and receiver are at the same depth, DCM is applied in Auto mode. - :param use_kmax_ref: directly use kmax_ref as kmax, without amplitude search - :param Length: integration step `dk=2\pi / (L*rmax)`, default L=15 - :param filonLength: integration step of Fixed-Interval Filon's Integration Method (large distance only; not for r=0) - :param safilonTol: precision of Self-Adaptive Filon's Integration Method (large distance only; not for r=0) - :param filonCut: The splitting point of DWM and (SA)FIM, k*=/rmax, default is 0 - :param converg_method: The method of explicit convergence, you can set "AUTO", "NONE", "DCM" or "PTAM". Default use "AUTO". - :param calc_upar: whether calculate the spatial derivatives of displacements. - :param statsfile: directory path for saving the statsfile during k integral, used to debug or observe the variations of :math:`F(k,\omega)` and :math:`F(k,\omega)J_m(kr)k` - - :return: - - **dataDct** - static Green's function in a dict + Compute dynamic Green's functions with the ``grt greenfn`` command. + + Call :meth:`set_dynamic_grn_path` first. Results are written as SAC files + under ``{dynamic_grn_path}/{model}_{depsrc}_{deprcv}_{distance}``. + All arguments must be passed by keyword. + + :param depsrc: Source depth in km. + :param deprcv: Receiver depth in km. + :param distarr: Array of epicentral distances in km, or a single distance. + :param nt: Number of time points. With the help of SciPy, + ``nt`` no longer needs to be a power of 2. + :param dt: Time interval in s. + :param upsampling_n: Upsampling factor applied after inverse FFT. + :param freqband: Frequency range ``(f1, f2)`` in Hz. Negative values mean + that the corresponding bound is determined automatically. + :param zeta: Coefficient defining the imaginary angular frequency, + :math:`\tilde{\omega} = \omega - j w_I`, + where :math:`w_I = \zeta\pi/T` and :math:`T=nt\,dt`. + :param keepAllFreq: Whether to calculate all frequency points, + regardless of how low the frequency is. + :param vmin_ref: Minimum reference velocity in km/s. ``0.0`` means the + minimum model velocity, limited to 0.1 km/s. + :param keps: Automatic convergence condition. See Yao and Harkrider + (1983) for more details. A negative value disables this condition. + :param ampk: Amplification factor in the reference maximum wavenumber. + :param k0: Coefficient in the reference maximum wavenumber, + :math:`k_{\mathrm{max,ref}} = + \sqrt{(k_0\pi/h_s)^2 + (ampk\,\omega/v_{\mathrm{min,ref}})^2}`. + Here :math:`h_s=\max(|depsrc-deprcv|,0.1)`. + :param use_kmax_ref: Whether to use the reference maximum wavenumber directly, + without amplitude searching. + :param Length: Integration step :math:`dk=2\pi/(Lr_{\max})`. + ``0.0`` means the value is selected automatically. + :param filonLength: Integration step of fixed-interval Filon integration at + large distances, but not at zero distance. ``0.0`` disables + this method. Do not set together with ``safilonTol``. + :param safilonTol: Precision of self-adaptive Filon integration at large + distances, but not at zero distance. ``0.0`` disables this + method. Do not set together with ``filonLength``. + :param filonCut: Splitting point between DWM and Filon integration, + :math:`k^*=\mathrm{filonCut}/r_{\max}`. + :param converg_method: Explicit convergence method. One of ``AUTO``, ``NONE``, + ``DCM`` and ``PTAM``. + :param delayT0: Time delay at zero distance in s. + :param delayV0: Reference velocity for the time delay in km/s. + Used only when ``ref_first_p`` is false. + :param ref_first_p: Whether to use the first P-wave arrival as the reference + for the time delay (CLI ``-Ep``). + :param skipImagComps: Whether to skip the amplitude compensation from the + imaginary frequency. + :param calc_upar: Whether to calculate spatial derivatives of displacement. + Required later if strain, stress or rotation will be computed. + :param gf_source: Source types to calculate. Choose from ``EX``, ``VF``, + ``HF`` and ``DC``. ``None`` means all available source types. + :param statsidxs: Frequency indexes for optional statistics output. + ``None`` means no statistics files. An empty list means + all frequency indexes (CLI bare ``-S``). + :param print_log: Whether to print calculation logs. + + :return: ``None``. Results are written to disk. """ + if self.dynamic_grn_path is None: + raise RuntimeError("Call set_dynamic_grn_path() before compute_grn().") + if nt <= 0 or dt <= 0: + raise ValueError("nt and dt must be positive.") + if depsrc < 0 or deprcv < 0: + raise ValueError("Source and receiver depths must be nonnegative.") - if self.hasLiquid: - raise NotImplementedError( - "The feature for calculating static displacements " - "in a model with liquid layers has not yet been implemented." - ) - - # 兼容旧公开 API:xarr/yarr - if xarr is not None or yarr is not None: - warnings.warn( - "Arguments 'xarr'/'yarr' are deprecated; prefer 'norths'/'easts'.", - FutureWarning, - stacklevel=2, - ) - if norths is not None or easts is not None: - raise ValueError("Use either norths/easts or xarr/yarr, not both.") - norths, easts = xarr, yarr - - if Length < 0.0: - raise ValueError(f"Length ({Length}) < 0") - if filonLength < 0.0: - raise ValueError(f"filonLength ({filonLength}) < 0") - if filonCut < 0.0: - raise ValueError(f"filonCut ({filonCut}) < 0") - if safilonTol < 0.0: - raise ValueError(f"filonCut ({safilonTol}) < 0") - - # 只能设置一种filon积分方法 - if safilonTol > 0.0 and filonLength > 0.0: - raise ValueError(f"You should only set one of filonLength and safilonTol.") + _, distances = _normalize_distarr(distarr) + if distances.size == 0 or np.any(distances < 0): + raise ValueError("distarr must contain nonnegative distances.") + try: + freq1, freq2 = freqband + except (TypeError, ValueError): + raise ValueError("freqband must contain exactly two values (f1, f2).") from None + + command = { + "subcommand": "greenfn", + "M": f"-M{self.modelpath}", + "D": f"-D{format_float(depsrc)}/{format_float(deprcv)}", + "N": f"-N{nt}/{format_float(dt)}+w{format_float(zeta)}+n{upsampling_n}", + "R": f"-R{','.join(format_float(distance) for distance in distances)}", + "O": f"-O{self.dynamic_grn_path}", + "B": f"-B{self._boundary_option()}", + } + + # Build the -N option. + if keepAllFreq: + command["N"] += "+a" + if skipImagComps: + command["N"] += "+f" + + # Build the -H option. + command["H"] = f"-H{format_float(freq1)}/{format_float(freq2)}" + + # Build the -L option. + option = format_float(Length) + if filonLength: + option += f"+l{format_float(filonLength)}" + if safilonTol: + option += f"+a{format_float(safilonTol)}" + if filonCut: + option += f"+o{format_float(filonCut)}" + command["L"] = f"-L{option}" + + # Build the -C option. + option = self._convergence_option(converg_method) + if option: + command["C"] = f"-C{option}" + + # Build the -K option. + options = [f"+k{format_float(k0)}"] + if use_kmax_ref: + options.append("+f") + options.extend([f"+s{format_float(ampk)}", f"+e{format_float(keps)}"]) + if vmin_ref: + options.append(f"+v{format_float(vmin_ref)}") + command["K"] = "-K" + "".join(options) + + # Build the -E option. + if ref_first_p: + command["E"] = f"-Ep{format_float(delayT0)}" + else: + command["E"] = f"-E{format_float(delayT0)}/{format_float(delayV0)}" + + # Build the -G option. + if gf_source is not None: + source_codes = {"EX": "e", "VF": "v", "HF": "h", "DC": "s"} + codes = [] + for name in gf_source: + key = str(name).upper() + if key not in source_codes: + raise ValueError(f"Unsupported gf_source={name!r}. Choose from EX, VF, HF and DC.") + codes.append(source_codes[key]) + command["G"] = "-G" + "".join(codes) + + # Build the -S option. + if statsidxs is not None: + command["S"] = "-S" + ",".join(str(index) for index in statsidxs) + + # Build the derivative and logging options. + if calc_upar: + command["e"] = "-e" + if not print_log: + command["s"] = "-s" + + run_grt(list(command.values()), print_log=print_log) + + def compute_static_grn( + self, + *, + depsrc: float, + deprcv: float, + norths: Optional[Sequence[float]] = None, + easts: Optional[Sequence[float]] = None, + distarr: Optional[Sequence[float]] = None, + keps: float = -1.0, + k0: float = 50.0, + use_kmax_ref: bool = False, + Length: float = 15.0, + filonLength: float = 0.0, + safilonTol: float = 0.0, + filonCut: float = 0.0, + converg_method: str = "AUTO", + calc_upar: bool = False, + stats: bool = False, + ): + r""" + Compute static Green's functions with the ``grt static greenfn`` command. + + Call :meth:`set_static_grn_path` first. Results are written to the + configured NetCDF file and currently overwrite any existing content. + All arguments must be passed by keyword. + + Receiver locations can be specified in either of two ways: + + 1. ``norths`` and ``easts``, each a three-value sequence + ``(start, stop, step)`` in km, mapped to CLI ``-X`` / ``-Y``. + 2. ``distarr``, a list of epicentral distances in km. This is equivalent + to placing receivers along the east axis with north = 0. + + :param depsrc: Source depth in km. + :param deprcv: Receiver depth in km. + :param norths: Three values defining the north-coordinate + option ``-Xstart/stop/step`` in km. + :param easts: Three values defining the east-coordinate + option ``-Ystart/stop/step`` in km. + :param distarr: Epicentral distances in km. Equivalent to + receivers with north = 0 and east = ``distarr``. + Mutually exclusive with ``norths`` / ``easts``. + :param keps: Automatic convergence condition. See Yao and + Harkrider (1983) for more details. A negative + value disables this condition. + :param k0: Coefficient in the reference maximum wavenumber, + where :math:`h_s=\max(|depsrc-deprcv|,0.1)`. + The actual maximum wavenumber is searched based + on the kernel amplitude. + :param use_kmax_ref: Whether to use the reference maximum wavenumber + directly, without amplitude searching. + :param Length: Integration step :math:`dk=2\pi/(Lr_{\max})`. + The default is 15. + :param filonLength: Step parameter for fixed-interval Filon + integration at large distances. ``0.0`` disables + this method. Do not set together with + ``safilonTol``. + :param safilonTol: Tolerance for self-adaptive Filon integration + at large distances. ``0.0`` disables this method. + Do not set together with ``filonLength``. + :param filonCut: Splitting point between DWM and Filon integration. + :param converg_method: Explicit convergence method. One of + ``AUTO``, ``NONE``, ``DCM`` and ``PTAM``. + :param calc_upar: Whether to calculate spatial derivatives of + displacement. Required later if strain, + stress or rotation will be computed. + :param stats: Whether to write integration statistics. + + :return: ``None``. Results are written to the configured NetCDF file. + """ + if self.static_grn_path is None: + raise RuntimeError("Call set_static_grn_path() before compute_static_grn().") + if depsrc < 0 or deprcv < 0: + raise ValueError("Source and receiver depths must be nonnegative.") if distarr is not None: - if isinstance(distarr, float) or isinstance(distarr, int): - distarr = np.array([distarr*1.0]) - distarr = np.array(distarr) - - norths = np.array([0.0]) - easts = distarr.copy() - if np.any(easts < 0.0): - raise ValueError("distances can't be negative.") - - if norths is None or easts is None: - raise ValueError("you need to set norths and easts or distarr.") - - if isinstance(norths, float) or isinstance(norths, int): - norths = np.array([norths*1.0]) - norths = np.array(norths) - - if isinstance(easts, float) or isinstance(easts, int): - easts = np.array([easts*1.0]) - easts = np.array(easts) - - nnorth = len(norths) - neast = len(easts) - nr = nnorth*neast - rs = np.zeros((nr,), dtype=NPCT_REAL_TYPE) - for ieast in range(neast): - for inorth in range(nnorth): - rs[inorth + ieast*nnorth] = np.hypot(norths[inorth], easts[ieast]) - c_rs = npct.as_ctypes(rs) - - # 积分状态文件 - c_statsfile = None - if statsfile is not None: - os.makedirs(statsfile, exist_ok=True) - c_statsfile = c_char_p(statsfile.encode('utf-8')) - - # 初始化格林函数 - pygrn = np.zeros((nr, SRC_M_NUM, CHANNEL_NUM), dtype=NPCT_REAL_TYPE, order='C'); c_pygrn = npct.as_ctypes(pygrn) - pygrn_uiz = np.zeros((nr, SRC_M_NUM, CHANNEL_NUM), dtype=NPCT_REAL_TYPE, order='C'); c_pygrn_uiz = npct.as_ctypes(pygrn_uiz) - pygrn_uir = np.zeros((nr, SRC_M_NUM, CHANNEL_NUM), dtype=NPCT_REAL_TYPE, order='C'); c_pygrn_uir = npct.as_ctypes(pygrn_uir) - - if not calc_upar: - c_pygrn_uiz = c_pygrn_uir = None - - # 仅做最基本的正负号等检查;物理预处理交给 C grt_prepare_static_grn - KPROC = c_K_INTEG_PROCESS() - C_grt_prepare_static_grn( - self.c_mod1d, - nr, c_rs, - Length, - filonLength, safilonTol, filonCut, - k0, keps, use_kmax_ref, - K_INTEG_CVGMET_DICT[converg_method.upper()], - pointer(KPROC), + if norths is not None or easts is not None: + raise ValueError("Use either distarr or norths/easts.") + _, distances = _normalize_distarr(distarr) + if distances.size == 0 or np.any(distances < 0.0): + raise ValueError("distarr must contain nonnegative distances.") + command_grid = { + "R": f"-R{','.join(format_float(value) for value in distances)}" + } + else: + if norths is None or easts is None: + raise ValueError("Set norths and easts, or set distarr.") + command_grid = { + "X": f"-X{format_range(norths, 'norths')}", + "Y": f"-Y{format_range(easts, 'easts')}", + } + + command = { + "module": "static", + "subcommand": "greenfn", + "M": f"-M{self.modelpath}", + "D": f"-D{format_float(depsrc)}/{format_float(deprcv)}", + "O": f"-O{self.static_grn_path}", + "B": f"-B{self._boundary_option()}", + } + command.update(command_grid) + + # Build the -L option. + option = format_float(Length) + if filonLength: + option += f"+l{format_float(filonLength)}" + if safilonTol: + option += f"+a{format_float(safilonTol)}" + if filonCut: + option += f"+o{format_float(filonCut)}" + command["L"] = f"-L{option}" + + # Build the -C option. + option = self._convergence_option(converg_method) + if option: + command["C"] = f"-C{option}" + + # Build the -K option. + options = [f"+k{format_float(k0)}"] + if use_kmax_ref: + options.append("+f") + options.append(f"+e{format_float(keps)}") + command["K"] = "-K" + "".join(options) + + # Build the statistics and derivative options. + if stats: + command["S"] = "-S" + if calc_upar: + command["e"] = "-e" + + run_grt(list(command.values())) + + def compute_syn( + self, + *, + dist: float, + azimuth: float, + scale: float, + output_path: PathLike, + source: str = "EX", + strike: Optional[float] = None, + dip: Optional[float] = None, + rake: Optional[float] = None, + force: Optional[Sequence[float]] = None, + moment_tensor: Optional[Sequence[float]] = None, + time_function: Optional[str] = None, + integrate_order: Optional[int] = None, + differentiate_order: Optional[int] = None, + scale_with_mu: bool = False, + zne: bool = False, + calc_upar: bool = False, + return_result: bool = False, + ): + r""" + Synthesize dynamic three-component displacement with ``grt syn``. + + Results are written as SAC files under ``output_path``. By default the + synthetics are impulse-like displacements in cm with ZRT components: + + * ``Z`` - vertical upward + * ``R`` - radial outward + * ``T`` - clockwise 90° from ``R`` + + Call :meth:`set_dynamic_grn_path` and :meth:`compute_grn` first. The + Green's function directory is located under ``dynamic_grn_path`` by + matching ``dist`` in the subdirectory name. All arguments must be + passed by keyword. + + Choose one source type with ``source``: + + * ``EX`` - explosion. Only ``scale`` is required. + * ``DC`` - double-couple / shear. Requires ``strike``, ``dip`` and ``rake``. + * ``TS`` - tensile crack. Requires ``strike`` and ``dip``. + * ``SF`` - single force. Requires ``force=(fN, fE, fZ)``. + * ``MT`` - moment tensor. Requires + ``moment_tensor=(Mxx, Mxy, Mxz, Myy, Myz, Mzz)``. + + :param dist: Epicentral distance in km. Used to locate the + Green's function directory under + ``dynamic_grn_path``. + :param azimuth: Azimuth from source to receiver in deg. + North is 0°, clockwise positive. + :param scale: Source scaling factor. For ``EX``, ``DC``, + ``TS`` and ``MT``, this is the scalar seismic + moment in dyne·cm. For ``SF``, the unit is dyne. + If ``scale_with_mu`` is true, ``scale`` is + treated as area × slip in cm³ and multiplied by + the source-layer shear modulus :math:`\mu`. + :param output_path: Output directory for SAC files + ``{output_path}/{ch}.sac``. + :param source: Source type. One of ``EX``, ``DC``, ``TS``, + ``SF`` and ``MT``. + :param strike: Fault strike in deg, in [0, 360]. North is 0°, + clockwise positive. Required for ``DC`` and + ``TS``. + :param dip: Fault dip in deg, in [0, 90]. Required for + ``DC`` and ``TS``. + :param rake: Slip rake in deg, in [-180, 180], + counterclockwise positive on the fault plane. + Required for ``DC``. + :param force: Single-force coefficients ``(fN, fE, fZ)`` for + ``SF``. Directions are north, east and downward. + Each coefficient is multiplied by ``scale``. + :param moment_tensor: Six independent moment-tensor coefficients + ``(Mxx, Mxy, Mxz, Myy, Myz, Mzz)`` for ``MT``. + Subscripts x/y/z denote north/east/down. + :param time_function: Time-function string passed to CLI ``-D``. + Supported forms include: + + * ``p/t0`` - parabola lasting ``t0`` s + * ``t/t1/t2/t3`` - trapezoid with rise, + plateau and fall cutoffs in s + * ``r/f0`` - Ricker wavelet with dominant + frequency ``f0`` in Hz + * ``0/file`` - custom one-column amplitude file + + The peak amplitude of the time function is 1. + Omit this argument for an impulse response. + :param integrate_order: Number of time integrations. For example, + ``1`` yields step-like displacement. + :param differentiate_order: Number of time differentiations. For example, + ``1`` yields velocity. + :param scale_with_mu: If true, multiply ``scale`` by the source-layer + shear modulus :math:`\mu` (CLI ``-Su``). + :param zne: If true, output ZNE instead of ZRT components. + :param calc_upar: If true, also synthesize spatial derivatives of + displacement. Derivative channel names are + prefixed with ``z``, ``r`` or ``t``. Set this + when strain, stress or rotation will be computed + later. + :param return_result: If true, read the generated SAC files into an + :class:`obspy.Stream`. + + :return: An ObsPy stream when ``return_result`` is true; otherwise ``None``. + """ + grn_path = self._dynamic_grn_dir(dist) + output = Path(output_path) + output.mkdir(parents=True, exist_ok=True) + + command = { + "subcommand": "syn", + "G": f"-G{grn_path}", + "A": f"-A{format_float(azimuth)}", + "S": f"-S{'u' if scale_with_mu else ''}{format_float(scale)}", + "O": f"-O{output}", + } + command.update( + self._source_options(source, strike, dip, rake, force, moment_tensor) ) - # 运行C库函数 - #///////////////////////////////////////////////////////////////////////////////// - # 计算得到的格林函数的单位: - # 单力源 HF[ZRT],VF[ZR] 1e-15 cm/dyne - # 爆炸源 EX[ZR] 1e-20 cm/(dyne*cm) - # 剪切源 DD[ZR],DS[ZRT],SS[ZRT] 1e-20 cm/(dyne*cm) - #================================================================================= - C_grt_integ_static_grn( - self.c_mod1d, nr, c_rs, pointer(KPROC), - calc_upar, c_pygrn, c_pygrn_uiz, c_pygrn_uir, - c_statsfile + # Build the time-function and operation-order options. + if time_function is not None: + command["D"] = f"-D{time_function}" + if integrate_order is not None: + command["I"] = f"-I{integrate_order}" + if differentiate_order is not None: + command["J"] = f"-J{differentiate_order}" + + # Build the component and derivative options. + if zne: + command["N"] = "-N" + if calc_upar: + command["e"] = "-e" + + run_grt(list(command.values())) + if return_result: + return read(str(output / "*.sac")) + return None + + def compute_static_syn( + self, + *, + scale: float, + output_path: PathLike, + source: str = "EX", + strike: Optional[float] = None, + dip: Optional[float] = None, + rake: Optional[float] = None, + force: Optional[Sequence[float]] = None, + moment_tensor: Optional[Sequence[float]] = None, + scale_with_mu: bool = False, + norths: Optional[Sequence[float]] = None, + easts: Optional[Sequence[float]] = None, + zne: bool = False, + calc_upar: bool = False, + return_result: bool = False, + ): + r""" + Synthesize static three-component displacement with ``grt static syn``. + + Results are written to the NetCDF file ``output_path``. Source-type and + component conventions match :meth:`compute_syn`. Call + :meth:`set_static_grn_path` and :meth:`compute_static_grn` first. + All arguments must be passed by keyword. + + By default the output grid inherits the north/east grid of the static + Green's function file. You may pass ``norths`` and ``easts`` to request a + new grid; each node then uses the nearest epicentral-distance Green's + function, which is an approximation that reuses an existing library. + + Choose one source type with ``source``: + + * ``EX`` - explosion. Only ``scale`` is required. + * ``DC`` - double-couple / shear. Requires ``strike``, ``dip`` and ``rake``. + * ``TS`` - tensile crack. Requires ``strike`` and ``dip``. + * ``SF`` - single force. Requires ``force=(fN, fE, fZ)``. + * ``MT`` - moment tensor. Requires + ``moment_tensor=(Mxx, Mxy, Mxz, Myy, Myz, Mzz)``. + + :param scale: Source scaling factor. For ``EX``, ``DC``, + ``TS`` and ``MT``, this is the scalar seismic + moment in dyne·cm. For ``SF``, the unit is dyne. + If ``scale_with_mu`` is true, ``scale`` is + treated as area × slip in cm³ and multiplied by + the source-layer shear modulus :math:`\mu`. + :param output_path: Output NetCDF file path. + :param source: Source type. One of ``EX``, ``DC``, ``TS``, + ``SF`` and ``MT``. + :param strike: Fault strike in deg, in [0, 360]. North is 0°, + clockwise positive. Required for ``DC`` and + ``TS``. + :param dip: Fault dip in deg, in [0, 90]. Required for + ``DC`` and ``TS``. + :param rake: Slip rake in deg, in [-180, 180], + counterclockwise positive on the fault plane. + Required for ``DC``. + :param force: Single-force coefficients ``(fN, fE, fZ)`` for + ``SF``. Directions are north, east and downward. + Each coefficient is multiplied by ``scale``. + :param moment_tensor: Six independent moment-tensor coefficients + ``(Mxx, Mxy, Mxz, Myy, Myz, Mzz)`` for ``MT``. + Subscripts x/y/z denote north/east/down. + :param scale_with_mu: If true, multiply ``scale`` by the source-layer + shear modulus :math:`\mu` (CLI ``-Su``). + :param norths: Optional new north grid as three values + ``(start, stop, step)`` in km. Must be set + together with ``easts``. + :param easts: Optional new east grid as three values + ``(start, stop, step)`` in km. Must be set + together with ``norths``. + :param zne: If true, output ZNE instead of ZRT components. + :param calc_upar: If true, also synthesize spatial derivatives of + displacement. Derivative variable names are + prefixed with ``z``, ``r`` or ``t``. Set this + when strain, stress or rotation will be computed + later. + :param return_result: If true, read the generated NetCDF file with + :func:`pygrt.utils.read_static_nc`. + + :return: The synthesized NetCDF data when ``return_result`` is true; + otherwise ``None``. + """ + if self.static_grn_path is None: + raise RuntimeError("Call set_static_grn_path() before compute_static_syn().") + output = Path(output_path) + output.parent.mkdir(parents=True, exist_ok=True) + command = { + "module": "static", + "subcommand": "syn", + "G": f"-G{self.static_grn_path}", + "S": f"-S{'u' if scale_with_mu else ''}{format_float(scale)}", + "O": f"-O{output}", + } + command.update( + self._source_options(source, strike, dip, rake, force, moment_tensor) ) - #================================================================================= - #///////////////////////////////////////////////////////////////////////////////// - - # 震源和场点层的物性 - rcv_va = self.c_mod1d.Va[self.ircv] - rcv_vb = self.c_mod1d.Vb[self.ircv] - rcv_rho = self.c_mod1d.Rho[self.ircv] - src_va = self.c_mod1d.Va[self.isrc] - src_vb = self.c_mod1d.Vb[self.isrc] - src_rho = self.c_mod1d.Rho[self.isrc] - - # 结果字典 - dataDct = {} - dataDct['_norths'] = norths.copy() - dataDct['_easts'] = easts.copy() - dataDct['_depsrc'] = self.depsrc - dataDct['_deprcv'] = self.deprcv - dataDct['_src_va'] = src_va - dataDct['_src_vb'] = src_vb - dataDct['_src_rho'] = src_rho - dataDct['_rcv_va'] = rcv_va - dataDct['_rcv_vb'] = rcv_vb - dataDct['_rcv_rho'] = rcv_rho - - # 整理结果,以 (nnorth, neast) 矩阵存储;物理量仍只依赖震中距 - for isrc in range(SRC_M_NUM): - src_name = SRC_M_NAME_ABBR[isrc] - for ic, comp in enumerate(ZRTchs): - sgn = -1 if comp=='Z' else 1 - dataDct[f'{src_name}{comp}'] = sgn * pygrn[:,isrc,ic].reshape((nnorth, neast), order='F') - if calc_upar: - dataDct[f'z{src_name}{comp}'] = sgn * pygrn_uiz[:,isrc,ic].reshape((nnorth, neast), order='F') * (-1) - dataDct[f'r{src_name}{comp}'] = sgn * pygrn_uir[:,isrc,ic].reshape((nnorth, neast), order='F') - - return dataDct + + # Build the coordinate options. + if norths is not None or easts is not None: + if norths is None or easts is None: + raise ValueError("norths and easts must be supplied together.") + command["X"] = f"-X{format_range(norths, 'norths')}" + command["Y"] = f"-Y{format_range(easts, 'easts')}" + + # Build the component and derivative options. + if zne: + command["N"] = "-N" + if calc_upar: + command["e"] = "-e" + + run_grt(list(command.values())) + if return_result: + return read_static_nc(output) + return None + + def _dynamic_grn_dir(self, dist: float) -> str: + """ + 在 dynamic_grn_path 下按震中距匹配格林函数子目录 + + 子目录命名为 ``{model}_{depsrc}_{deprcv}_{dist}`` + 当前假设仅有一套震源/台站深度,故只需匹配 dist + """ + if self.dynamic_grn_path is None: + raise RuntimeError("Call set_dynamic_grn_path() before compute_syn().") + root = Path(self.dynamic_grn_path) + if not root.is_dir(): + raise FileNotFoundError(f"Dynamic Green's function root does not exist: {root}") + + suffix = f"_{format_float(dist)}" + matches = [path for path in root.iterdir() if path.is_dir() and path.name.endswith(suffix)] + if not matches: + raise FileNotFoundError(f"No Green's function directory matching dist={format_float(dist)} under {root}.") + if len(matches) > 1: + names = ", ".join(path.name for path in sorted(matches)) + raise RuntimeError(f"Multiple Green's function directories match dist={format_float(dist)} under {root}: {names}.") + return str(matches[0]) + + def _boundary_option(self) -> str: + return { + "free": "f", + "rigid": "r", + "halfspace": "h", + }[self.topbound] + { + "free": "F", + "rigid": "R", + "halfspace": "H", + }[self.botbound] + + @staticmethod + def _convergence_option(value: str) -> str: + options = {"AUTO": "", "DCM": "d", "PTAM": "p", "NONE": "n"} + try: + return options[value.upper()] + except KeyError: + raise ValueError(f"Unsupported convergence method: {value}") from None + + @staticmethod + def _source_options( + source: str, + strike: Optional[float], + dip: Optional[float], + rake: Optional[float], + force: Optional[Sequence[float]], + moment_tensor: Optional[Sequence[float]], + ) -> Dict[str, str]: + source = source.upper() + if source == "EX": + return {} + if source == "DC": + if strike is None or dip is None or rake is None: + raise ValueError("DC source requires strike, dip and rake.") + return {"M": f"-M{format_float(strike)}/{format_float(dip)}/{format_float(rake)}"} + if source == "TS": + if strike is None or dip is None: + raise ValueError("TS source requires strike and dip.") + return {"M": f"-M{format_float(strike)}/{format_float(dip)}"} + if source == "SF": + if force is None or len(force) != 3: + raise ValueError("SF source requires force=(fN, fE, fZ).") + return {"F": "-F" + "/".join(format_float(value) for value in force)} + if source == "MT": + if moment_tensor is None or len(moment_tensor) != 6: + raise ValueError("MT source requires six moment-tensor values.") + return {"T": "-T" + "/".join(format_float(value) for value in moment_tensor)} + raise ValueError(f"Unsupported source type: {source}") diff --git a/pygrt/signals.py b/pygrt/signals.py index ba71bfce..3674efdd 100755 --- a/pygrt/signals.py +++ b/pygrt/signals.py @@ -1,20 +1,19 @@ """ - :file: signals.py - :author: Zhu Dengda (zhudengda@mail.iggcas.ac.cn) - :date: 2024-07-24 - - 该文件包括一些常见的时间信号,最高幅值均为1 + :file: signals.py + :author: Zhu Dengda (zhudengda@mail.iggcas.ac.cn) + :date: 2024-07-24 + 该文件包括一些常见的时间信号,最高幅值均为1 """ - -import numpy as np +import numpy as np import numpy.ctypeslib as npct from ctypes import byref, cast from .c_interfaces import * + __all__ = [ "gen_triangle_wave", "gen_parabola_wave", @@ -22,14 +21,15 @@ "gen_ricker_wave", ] + def gen_triangle_wave(vlen, dt): ''' - generate triangle-shape wave + generate triangle-shape wave - :param vlen: signal length (s) - :param dt: time interval (s) + :param vlen: signal length (s) + :param dt: time interval (s) - :return: + :return: - **wave** - amplitude sequence ''' return gen_trap_wave(vlen/2.0, vlen/2.0, vlen, dt) @@ -37,12 +37,12 @@ def gen_triangle_wave(vlen, dt): def gen_parabola_wave(vlen, dt): ''' - generate parabola-shape wave + generate parabola-shape wave - :param vlen: signal length (s) - :param dt: time interval (s) - - :return: + :param vlen: signal length (s) + :param dt: time interval (s) + + :return: - **wave** - amplitude sequence ''' ct1 = c_float(vlen) @@ -55,16 +55,17 @@ def gen_parabola_wave(vlen, dt): return arr + def gen_trap_wave(t1, t2, t3, dt): ''' - generate trapezoid-shape wave + generate trapezoid-shape wave - :param t1: ramp-up cutoff time (s) - :param t2: plateau cutoff time (s) - :param t3: ramp-down cutoff time (s) - :param dt: time interval (s) + :param t1: ramp-up cutoff time (s) + :param t2: plateau cutoff time (s) + :param t3: ramp-down cutoff time (s) + :param dt: time interval (s) - :return: + :return: - **wave** - amplitude sequence ''' ct1 = c_float(t1) @@ -99,4 +100,4 @@ def gen_ricker_wave(f0:float, dt:float): C_grt_free(carr) - return arr \ No newline at end of file + return arr diff --git a/pygrt/utils.py b/pygrt/utils.py index 5c2e5c40..4d2946b3 100755 --- a/pygrt/utils.py +++ b/pygrt/utils.py @@ -4,970 +4,305 @@ :date: 2024-07-24 该文件包含一些数据处理操作上的补充: - 1、剪切源、张裂源、单力源、爆炸源、矩张量源、有限断层 通过格林函数合成理论地震图的函数\n - 2、Stream类型的时域卷积、微分、积分 (基于numpy和scipy) \n - 3、读取波数积分和峰谷平均法过程文件 \n - 4、其它辅助函数 \n """ +from __future__ import annotations + +import os +import glob +from copy import deepcopy +from pathlib import Path +from typing import List, Union -import numpy as np -import numpy.ctypeslib as npct import matplotlib.pyplot as plt +import numpy as np from matplotlib.axes import Axes from matplotlib.figure import Figure -from obspy import Stream, Trace -from obspy.core import AttribDict -from copy import deepcopy +from obspy import Stream, read +from scipy.interpolate import interpn +from scipy.io import netcdf_file from scipy.signal import oaconvolve -from scipy.fft import rfft, irfft from scipy.special import jv -from scipy.interpolate import interpn -import math -import os -import glob -import warnings -from typing import List, Union, Tuple -from copy import deepcopy - -from numpy.typing import ArrayLike - -from .c_interfaces import * - - -def _warn_xarr_yarr_deprecated(stacklevel:int=3): - warnings.warn( - "Arguments 'xarr'/'yarr' (and dict keys '_xarr'/'_yarr') are deprecated; " - "prefer 'norths'/'easts' (and '_norths'/'_easts').", - FutureWarning, - stacklevel=stacklevel, - ) - - -def _resolve_static_ne_coords( - norths=None, easts=None, xarr=None, yarr=None, - default_norths=None, default_easts=None, - stacklevel:int=3, -) -> Tuple[object, object]: - """解析 north/east 坐标,兼容旧公开 API 名 xarr/yarr""" - if xarr is not None or yarr is not None: - _warn_xarr_yarr_deprecated(stacklevel=stacklevel) - if norths is not None or easts is not None: - raise ValueError("Use either norths/easts or xarr/yarr, not both.") - norths, easts = xarr, yarr - if norths is None: - norths = default_norths - if easts is None: - easts = default_easts - return norths, easts - - -def _pop_static_ne_from_kwargs( - kwargs:dict, - default_norths=None, - default_easts=None, - stacklevel:int=3, -) -> Tuple[object, object]: - """从 kwargs 取出 norths/easts(或旧名 xarr/yarr)""" - return _resolve_static_ne_coords( - kwargs.pop('norths', None), kwargs.pop('easts', None), - kwargs.pop('xarr', None), kwargs.pop('yarr', None), - default_norths=default_norths, default_easts=default_easts, - stacklevel=stacklevel, - ) - - -def _static_dict_norths_easts(d:dict, stacklevel:int=3) -> Tuple[object, object]: - """从静态结果字典读取 north/east,兼容旧键 _xarr/_yarr""" - if '_norths' in d and '_easts' in d: - return d['_norths'], d['_easts'] - if '_xarr' in d and '_yarr' in d: - _warn_xarr_yarr_deprecated(stacklevel=stacklevel) - return d['_xarr'], d['_yarr'] - raise KeyError("static dict missing '_norths'/'_easts' (or deprecated '_xarr'/'_yarr').") - -from enum import Enum, unique - -@unique -class GRT_SYN_TYPE(Enum): - GRT_SYN_EX = 0 - GRT_SYN_SF = 1 - GRT_SYN_DC = 2 - GRT_SYN_TS = 3 - GRT_SYN_MT = 4 +import numpy.ctypeslib as npct +from .cli import run_grt +from .c_interfaces import C_grt_solve_lamb1 __all__ = [ - "gen_syn_from_gf_DC", - "gen_syn_from_gf_SF", - "gen_syn_from_gf_EX", - "gen_syn_from_gf_MT", - + "read_static_nc", + "read_static_grn", "compute_strain", "compute_rotation", "compute_stress", - "stream_convolve", "stream_integral", "stream_diff", "stream_write_sac", - "read_kernels_freqs", "read_statsfile", "read_statsfile_ptam", "plot_statsdata", "plot_statsdata_ptam", - - "solve_lamb1" + "solve_lamb1", ] -#================================================================================================================= -# -# 根据辐射因子合成地震图 -# -#================================================================================================================= - -def _gen_syn_from_gf(st:Stream, calc_upar:bool, compute_type:GRT_SYN_TYPE, M0:float, az:float, ZNE=False, **kwargs): - r""" - 一个发起函数,根据不同震源参数,从格林函数中合成理论地震图 - - :param st: 计算好的时域格林函数, :class:`obspy.Stream` 类型 - :param calc_upar: 是否计算位移u的空间导数 - :param compute_type: 计算震源类型 - :param M0: 标量地震矩, 单位dyne*cm - :param az: 方位角(度) - :param ZNE: 是否以ZNE分量输出? - - """ - allchs = [tr.stats.channel for tr in st] - - # 为张裂计算 Vp/Vs - src_va = st[0].stats.sac['user6'] - src_vb = st[0].stats.sac['user7'] - VpVs_ratio = float(src_va / src_vb) - - azrad = REAL(np.deg2rad(az)) - dist = float(st[0].stats.sac['dist']) - npts = int(st[0].stats.npts) - - FPtrs = FPOINTER * CHANNEL_NUM - FGrid = FPtrs * SRC_M_NUM - FMat = FPtrs * CHANNEL_NUM - - def _trace_data(channel:str): - if channel not in allchs: - raise ValueError(f"Failed, channel=\"{channel}\" not exists.") - return np.ascontiguousarray(st.select(channel=channel)[0].data, dtype=np.float32) - - # 格林函数数组布局:gf[震源][分量][采样点] - gf_hold = [[None] * CHANNEL_NUM for _ in range(SRC_M_NUM)] - gf_uiz_hold = [[None] * CHANNEL_NUM for _ in range(SRC_M_NUM)] - gf_uir_hold = [[None] * CHANNEL_NUM for _ in range(SRC_M_NUM)] - for im, src_name in enumerate(SRC_M_NAME_ABBR): - for ic, ch in enumerate(ZRTchs): - # m=0 无 T 分量 - if SRC_M_ORDERS[im] == 0 and ch == 'T': - continue - channel = f'{src_name}{ch}' - # 仅加载存在的道(未用到的震源可缺省;C 侧对非零系数会校验) - if channel not in allchs: - continue - gf_hold[im][ic] = _trace_data(channel) - if calc_upar: - gf_uiz_hold[im][ic] = _trace_data(f'z{src_name}{ch}') - gf_uir_hold[im][ic] = _trace_data(f'r{src_name}{ch}') - - def _src_chnl_ptrs(hold): - return FGrid(*( - FPtrs(*( - (arr.ctypes.data_as(FPOINTER) if arr is not None else FPOINTER()) - for arr in row - )) - for row in hold - )) - - gf_ptrs = _src_chnl_ptrs(gf_hold) - gf_uiz_ptrs = _src_chnl_ptrs(gf_uiz_hold) if calc_upar else None - gf_uir_ptrs = _src_chnl_ptrs(gf_uir_hold) if calc_upar else None - - synarr = np.zeros((CHANNEL_NUM, npts), dtype=np.float32) - syn_upar_arr = np.zeros((CHANNEL_NUM, CHANNEL_NUM, npts), dtype=np.float32) - syn_ptrs = FPtrs(*(synarr[c].ctypes.data_as(FPOINTER) for c in range(CHANNEL_NUM))) - syn_upar_ptrs = FMat(*( - FPtrs(*(syn_upar_arr[d, c].ctypes.data_as(FPOINTER) for c in range(CHANNEL_NUM))) - for d in range(CHANNEL_NUM) - )) - - # ======================================================================== - # 调用 C 函数 - mchn = _set_source_mechanism(compute_type, **kwargs) - C_grt_syn_from_gf( - npts, dist, - gf_ptrs, gf_uiz_ptrs, gf_uir_ptrs, - compute_type.value, M0, VpVs_ratio, azrad, npct.as_ctypes(mchn), - ZNE, calc_upar, - syn_ptrs, syn_upar_ptrs, - ) - # ======================================================================== - - # C 可能因 r=0 强制 azrad=0,同步方位角头段 - az = float(np.rad2deg(azrad.value)) - baz = az + 180.0 - if baz >= 360.0: - baz -= 360.0 - - out_chs = ZNEchs if ZNE else ZRTchs - stall = Stream() - for c, ch in enumerate(out_chs): - tr:Trace = st[0].copy() - tr.data = synarr[c].copy() - tr.stats.channel = kcmpnm = f'{ch}' - __check_trace_attr_sac(tr, az=az, baz=baz, kcmpnm=kcmpnm) - stall.append(tr) - if calc_upar: - for d, dch in enumerate(out_chs): - tr = st[0].copy() - tr.data = syn_upar_arr[d, c].copy() - tr.stats.channel = kcmpnm = f'{dch.lower()}{ch}' - __check_trace_attr_sac(tr, az=az, baz=baz, kcmpnm=kcmpnm) - stall.append(tr) - - return stall - - -def _gen_syn_from_static_gf(grnDct:dict, calc_upar:bool, compute_type:GRT_SYN_TYPE, M0:float, ZNE=False, **kwargs): - r""" - 一个发起函数,根据不同震源参数,从静态格林函数中合成理论静态场 - - :param grnDct: 计算好的静态格林函数, 字典类型 - :param calc_upar: 是否计算位移u的空间导数 - :param compute_type: 计算震源类型 - :param M0: 标量地震矩, 单位dyne*cm - :param ZNE: 是否以ZNE分量输出? - :param kwargs: 其它各种参数,包括震源参数,新网格参数等 - - """ - # 为张裂计算 Vp/Vs - VpVs_ratio = float(grnDct['_src_va'] / grnDct['_src_vb']) - - norths0, easts0 = _static_dict_norths_easts(grnDct, stacklevel=4) - norths0 = np.ascontiguousarray(norths0, dtype=NPCT_REAL_TYPE) - easts0 = np.ascontiguousarray(easts0, dtype=NPCT_REAL_TYPE) - norths, easts = _pop_static_ne_from_kwargs( - kwargs, default_norths=norths0, default_easts=easts0, stacklevel=4) - norths = np.ascontiguousarray(norths, dtype=NPCT_REAL_TYPE) - easts = np.ascontiguousarray(easts, dtype=NPCT_REAL_TYPE) - nnorth0, neast0 = len(norths0), len(easts0) - nnorth, neast = len(norths), len(easts) - nr0, nr = nnorth0 * neast0, nnorth * neast - - def _pack_gf(prefix:str=''): - """打包为 C 侧 realChnlGrid[nr]:arr[震中距点][震源][分量]""" - arr = np.zeros((nr0, SRC_M_NUM, CHANNEL_NUM), dtype=NPCT_REAL_TYPE) - for isrc, src_name in enumerate(SRC_M_NAME_ABBR): - for ic, comp in enumerate(ZRTchs): - key = f'{prefix}{src_name}{comp}' - if key in grnDct: - arr[:, isrc, ic] = np.ravel(grnDct[key]) - return arr - - pygrn = _pack_gf() - pygrn_uiz = _pack_gf('z') if calc_upar else None - pygrn_uir = _pack_gf('r') if calc_upar else None - - syn = np.zeros((nr, CHANNEL_NUM), dtype=NPCT_REAL_TYPE) - syn_upar = np.zeros((nr, CHANNEL_NUM, CHANNEL_NUM), dtype=NPCT_REAL_TYPE) - - # ======================================================================== - # 调用 C 函数 - mchn = _set_source_mechanism(compute_type, **kwargs) - C_grt_static_syn_from_gf( - nnorth0, npct.as_ctypes(norths0), neast0, npct.as_ctypes(easts0), - nnorth, npct.as_ctypes(norths), neast, npct.as_ctypes(easts), - npct.as_ctypes(pygrn), - npct.as_ctypes(pygrn_uiz) if calc_upar else None, - npct.as_ctypes(pygrn_uir) if calc_upar else None, - compute_type.value, M0, VpVs_ratio, npct.as_ctypes(mchn), - ZNE, calc_upar, - npct.as_ctypes(syn), npct.as_ctypes(syn_upar), - ) - # ======================================================================== - - resDct = {k: deepcopy(v) for k, v in grnDct.items() if k.startswith('_')} - resDct['_norths'] = norths - resDct['_easts'] = easts - - chs = ZNEchs if ZNE else ZRTchs - for i1, c1 in enumerate(chs): - resDct[c1] = syn[:, i1].reshape((nnorth, neast)) - if calc_upar: - for i2, c2 in enumerate(chs): - resDct[f'{c2.lower()}{c1}'] = syn_upar[:, i2, i1].reshape((nnorth, neast)) - - return resDct - - -def _set_source_mechanism( - compute_type:GRT_SYN_TYPE, - fZ=None, fN=None, fE=None, - strike=None, dip=None, rake=None, - MT=None, **kwargs): - r""" - 整理 C 函数需要的震源机制数组 - """ - - mchn = np.zeros((MECHANISM_NUM,), dtype=NPCT_REAL_TYPE) - if compute_type == GRT_SYN_TYPE.GRT_SYN_EX: - pass - elif compute_type == GRT_SYN_TYPE.GRT_SYN_SF: - mchn[:3] = [fN, fE, fZ] - elif compute_type == GRT_SYN_TYPE.GRT_SYN_DC: - mchn[:3] = [strike, dip, rake] - elif compute_type == GRT_SYN_TYPE.GRT_SYN_TS: - mchn[:2] = [strike, dip] - elif compute_type == GRT_SYN_TYPE.GRT_SYN_MT: - mchn[:] = MT[:] - else: - raise ValueError("Unsupported source type.") - - return mchn - - -def gen_syn_from_gf_DC(st:Union[Stream,dict], M0:float, strike:float, dip:float, rake:float, az:float=-999, ZNE=False, calc_upar:bool=False, **kwargs): - ''' - Shear source, the unit of angles is all degrees(°) - - :param st: Green's functions in a :class:`obspy.Stream` (dynamic-case) or a dict (static-case) - :param M0: scalar seismic moment (dyne*cm) - :param strike: 0 <= strike <= 360 (north=0, clockwise as positive) - :param dip: 0 <= dip <= 90 - :param rake: -180 <= rake <= 180 (on the fault plane, counterclockwise as positive) - :param az: azimuth, 0 <= az <= 360 (not used for static case) - :param ZNE: whether output in 'ZNE'-coord, default is 'ZRT' - :param calc_upar: whether calculate the spatial derivatives of displacements. - :param kwargs: For static results, set ``norths``/``easts`` (preferred) or - deprecated ``xarr``/``yarr`` to define a new north/east grid; - synthesis interpolates in epicentral distance. - - :return: - - **stream** - :class:`obspy.Stream` - ''' - if isinstance(st, Stream): - if az > 360 or az < -360: - raise ValueError(f"WRONG azimuth ({az})") - return _gen_syn_from_gf(st, calc_upar, GRT_SYN_TYPE.GRT_SYN_DC, M0, az, ZNE, strike=strike, dip=dip, rake=rake) - elif isinstance(st, dict): - return _gen_syn_from_static_gf(st, calc_upar, GRT_SYN_TYPE.GRT_SYN_DC, M0, ZNE, strike=strike, dip=dip, rake=rake, **kwargs) - else: - raise NotImplementedError - -def gen_syn_from_gf_TS(st:Union[Stream,dict], M0:float, strike:float, dip:float, az:float=-999, ZNE=False, calc_upar:bool=False, **kwargs): - ''' - Tension source, the unit of angles is all degrees(°) - - :param st: Green's functions in a :class:`obspy.Stream` (dynamic-case) or a dict (static-case) - :param M0: scalar seismic moment (dyne*cm) - :param strike: 0 <= strike <= 360 (north=0, clockwise as positive) - :param dip: 0 <= dip <= 90 - :param az: azimuth, 0 <= az <= 360 (not used for static case) - :param ZNE: whether output in 'ZNE'-coord, default is 'ZRT' - :param calc_upar: whether calculate the spatial derivatives of displacements. - :param kwargs: For static results, set ``norths``/``easts`` (preferred) or - deprecated ``xarr``/``yarr`` to define a new north/east grid; - synthesis interpolates in epicentral distance. - - :return: - - **stream** - :class:`obspy.Stream` - ''' - if isinstance(st, Stream): - if az > 360 or az < -360: - raise ValueError(f"WRONG azimuth ({az})") - return _gen_syn_from_gf(st, calc_upar, GRT_SYN_TYPE.GRT_SYN_TS, M0, az, ZNE, strike=strike, dip=dip) - elif isinstance(st, dict): - return _gen_syn_from_static_gf(st, calc_upar, GRT_SYN_TYPE.GRT_SYN_TS, M0, ZNE, strike=strike, dip=dip, **kwargs) - else: - raise NotImplementedError - - -def gen_syn_from_gf_SF(st:Union[Stream,dict], S:float, fN:float, fE:float, fZ:float, az:float=-999, ZNE=False, calc_upar:bool=False, **kwargs): - ''' - Single-force source (dyne) - - :param st: Green's functions in a :class:`obspy.Stream` (dynamic-case) or a dict (static-case) - :param S: scaling factor (dyne) - :param fN: coefficient of Northward force - :param fE: coefficient of Eastward force - :param fZ: coefficient of Vertical(Downward) force - :param az: azimuth, 0 <= az <= 360 (not used for static case) - :param ZNE: whether output in 'ZNE'-coord, default is 'ZRT' - :param calc_upar: whether calculate the spatial derivatives of displacements. - :param kwargs: For static results, set ``norths``/``easts`` (preferred) or - deprecated ``xarr``/``yarr`` to define a new north/east grid; - synthesis interpolates in epicentral distance. - - :return: - - **stream** - :class:`obspy.Stream` - ''' - if isinstance(st, Stream): - if az > 360 or az < -360: - raise ValueError(f"WRONG azimuth ({az})") - return _gen_syn_from_gf(st, calc_upar, GRT_SYN_TYPE.GRT_SYN_SF, S, az, ZNE, fN=fN, fE=fE, fZ=fZ) - elif isinstance(st, dict): - return _gen_syn_from_static_gf(st, calc_upar, GRT_SYN_TYPE.GRT_SYN_SF, S, ZNE, fN=fN, fE=fE, fZ=fZ, **kwargs) - else: - raise NotImplementedError - - -def gen_syn_from_gf_EX(st:Union[Stream,dict], M0:float, az:float=-999, ZNE=False, calc_upar:bool=False, **kwargs): - ''' - Explosion - - :param st: Green's functions in a :class:`obspy.Stream` (dynamic-case) or a dict (static-case) - :param M0: scalar seismic moment (dyne*cm) - :param az: azimuth, 0 <= az <= 360 (not used for static case) - :param ZNE: whether output in 'ZNE'-coord, default is 'ZRT' - :param calc_upar: whether calculate the spatial derivatives of displacements. - :param kwargs: For static results, set ``norths``/``easts`` (preferred) or - deprecated ``xarr``/``yarr`` to define a new north/east grid; - synthesis interpolates in epicentral distance. - - :return: - - **stream** - :class:`obspy.Stream` - ''' - if isinstance(st, Stream): - if az > 360 or az < -360: - raise ValueError(f"WRONG azimuth ({az})") - return _gen_syn_from_gf(st, calc_upar, GRT_SYN_TYPE.GRT_SYN_EX, M0, az, ZNE) - elif isinstance(st, dict): - return _gen_syn_from_static_gf(st, calc_upar, GRT_SYN_TYPE.GRT_SYN_EX, M0, ZNE, **kwargs) - else: - raise NotImplementedError - - -def gen_syn_from_gf_MT(st:Union[Stream,dict], M0:float, MT:ArrayLike, az:float=-999, ZNE=False, calc_upar:bool=False, **kwargs): - ''' - Moment tensor - - :param st: Green's functions in a :class:`obspy.Stream` (dynamic-case) or a dict (static-case) - :param M0: scalar seismic moment (dyne*cm) - :param MT: coefficient of Moment tensor (M11, M12, M13, M22, M23, M33), subscripts 1,2,3 denote Northward,Eastward,Downward - :param az: azimuth, 0 <= az <= 360 (not used for static case) - :param ZNE: whether output in 'ZNE'-coord, default is 'ZRT' - :param calc_upar: whether calculate the spatial derivatives of displacements. - :param kwargs: For static results, set ``norths``/``easts`` (preferred) or - deprecated ``xarr``/``yarr`` to define a new north/east grid; - synthesis interpolates in epicentral distance. - - :return: - - **stream** - :class:`obspy.Stream` - ''' - if isinstance(st, Stream): - if az > 360 or az < -360: - raise ValueError(f"WRONG azimuth ({az})") - return _gen_syn_from_gf(st, calc_upar, GRT_SYN_TYPE.GRT_SYN_MT, M0, az, ZNE, MT=MT) - elif isinstance(st, dict): - return _gen_syn_from_static_gf(st, calc_upar, GRT_SYN_TYPE.GRT_SYN_MT, M0, ZNE, MT=MT, **kwargs) - else: - raise NotImplementedError - - -#================================================================================================================= -# -# 根据几何方程和本构方程合成应力、应变、旋转张量 -# -#================================================================================================================= +PathLike = Union[str, os.PathLike] -def _compute_strain_rotation(st_syn:Stream, Type:str): - r""" - Compute dynamic strain/rotation tensor from synthetic spatial derivatives. +QWV_NUM = 3 +INTEG_NUM = 4 +SRC_M_NUM = 6 +SRC_M_ORDERS = [0, 0, 1, 0, 1, 2] +SRC_M_NAME_ABBR = ["EX", "VF", "HF", "DD", "DS", "SS"] +qwvchs = ["q", "w", "v"] +NPCT_REAL_TYPE = "f8" +NPCT_CMPLX_TYPE = "c16" - :param st_syn: synthetic spatial derivatives. - :param Type: "strain" or "rotation" - - :return: - - **stream** - dynamic strain/rotation tensor, in :class:`obspy.Stream` class. - """ - if Type == 'strain': - i1_end = 3 - i2_offset = 0 - elif Type == 'rotation': - i1_end = 2 - i2_offset = 1 - else: - raise ValueError(f"{Type} not supported.") - - chs = ZRTchs - - # 判断是否有标志性的trace - if len(st_syn.select(channel=f"nN")) > 0: - chs = ZNEchs - - npts = st_syn[0].stats.npts - dist = st_syn[0].stats.sac['dist'] - u, upar, u_ptrs, upar_ptrs = _prepare_dynamic_postprocess_arrays(st_syn, chs, npts) - resarr, res_ptrs = _prepare_dynamic_postprocess_result(npts) - if Type == 'strain': - C_grt_compute_strain(npts, dist, u_ptrs, upar_ptrs, res_ptrs, chs == ZNEchs) - else: - C_grt_compute_rotation(npts, dist, u_ptrs, upar_ptrs, res_ptrs, chs == ZNEchs) - - stres = Stream() - for i1 in range(i1_end): - c1 = chs[i1] - for i2 in range(i1+i2_offset, 3): - c2 = chs[i2] - tr = st_syn.select(channel=f"{c2.lower()}{c1}")[0].copy() - tr.data = resarr[i2, i1] - tr.stats.channel = tr.stats.sac['kcmpnm'] = f"{c1}{c2}" - stres.append(tr) - - return stres - - -def _prepare_dynamic_postprocess_arrays(st_syn:Stream, chs:List[str], npts:int): - """收集动态位移/偏导数组,并构造 ctypes 通道指针表。""" - FPtrs = FPOINTER * CHANNEL_NUM - FMat = FPtrs * CHANNEL_NUM - - def data(channel:str): - st = st_syn.select(channel=channel) - if len(st) == 0: - raise NameError(f"{channel} not exists.") - if st[0].stats.npts != npts: - raise ValueError("All dynamic traces must have the same number of samples.") - return np.ascontiguousarray(st[0].data, dtype=np.float32) - - u = [data(c) for c in chs] - upar = [[data(f"{d.lower()}{c}") for c in chs] for d in chs] - u_ptrs = FPtrs(*(arr.ctypes.data_as(FPOINTER) for arr in u)) - upar_ptrs = FMat(*( - FPtrs(*(arr.ctypes.data_as(FPOINTER) for arr in row)) for row in upar)) - return u, upar, u_ptrs, upar_ptrs - - -def _prepare_dynamic_postprocess_result(npts:int): - """分配动态后处理结果数组及其 ctypes 通道指针表。""" - FPtrs = FPOINTER * CHANNEL_NUM - FMat = FPtrs * CHANNEL_NUM - - resarr = np.zeros((CHANNEL_NUM, CHANNEL_NUM, npts), dtype=np.float32) - res_ptrs = FMat(*( - FPtrs(*(resarr[c2, c1].ctypes.data_as(FPOINTER) for c1 in range(CHANNEL_NUM))) - for c2 in range(CHANNEL_NUM))) - return resarr, res_ptrs - - -def _prepare_static_postprocess_arrays(syn:dict, chs:List[str]): - """整理静态后处理所需的连续内存数组与 ctypes 通道指针表。""" - RPtrs = PREAL * CHANNEL_NUM - RMat = RPtrs * CHANNEL_NUM - - norths, easts = _static_dict_norths_easts(syn, stacklevel=4) - norths = np.ascontiguousarray(norths, dtype=np.float64) - easts = np.ascontiguousarray(easts, dtype=np.float64) - if norths.ndim != 1 or easts.ndim != 1: - raise ValueError("'_norths' and '_easts' must be one-dimensional arrays.") - - u = [np.ascontiguousarray(syn[c], dtype=np.float64) for c in chs] - upar = [ - [np.ascontiguousarray(syn[f"{d.lower()}{c}"], dtype=np.float64) for c in chs] - for d in chs - ] - expected_shape = (len(norths), len(easts)) - if any(arr.shape != expected_shape for arr in u) or any( - arr.shape != expected_shape for row in upar for arr in row): - raise ValueError("Static displacement and derivative arrays must match '_norths'/'_easts'.") - - u_ptrs = RPtrs(*(arr.ctypes.data_as(PREAL) for arr in u)) - upar_ptrs = RMat(*( - RPtrs(*(arr.ctypes.data_as(PREAL) for arr in row)) for row in upar)) - resarr = np.zeros((CHANNEL_NUM, CHANNEL_NUM, *expected_shape), dtype=np.float64) - res_ptrs = RMat(*( - RPtrs(*(resarr[c2, c1].ctypes.data_as(PREAL) for c1 in range(CHANNEL_NUM))) - for c2 in range(CHANNEL_NUM))) - return norths, easts, u, upar, u_ptrs, upar_ptrs, resarr, res_ptrs - - -def _compute_static_strain_rotation(syn:dict, Type:str): - r""" - Compute static strain/rotation tensor from synthetic spatial derivatives. +def _attribute_value(value): + """Convert a NetCDF attribute to a convenient Python value.""" + if isinstance(value, np.ndarray) and value.ndim == 0: + return value.item() + if isinstance(value, bytes): + return value.decode("utf-8") + return value - :param syn: synthetic spatial derivatives. - :param Type: "strain" or "rotation" - :return: - - **res** - static strain/rotation tensor, in dict class. +def read_static_nc(path: PathLike) -> dict: """ + Read a static NetCDF grid produced by ``grt static`` modules. - if Type == 'strain': - i1_end = 3 - i2_offset = 0 - elif Type == 'rotation': - i1_end = 2 - i2_offset = 1 - else: - raise ValueError(f"{Type} not supported.") - - chs = ZRTchs - - # 判断是否有标志性的分量名 - if f"nN" in syn.keys(): - chs = ZNEchs - - norths, easts, u, upar, u_ptrs, upar_ptrs, resarr, res_ptrs = \ - _prepare_static_postprocess_arrays(syn, chs) - - # 结果字典 - resDct = {} + The returned dictionary contains three top-level entries: - # 基本数据拷贝 - for k in syn.keys(): - if k[0] != '_': - continue - resDct[k] = deepcopy(syn[k]) + * ``dimensions`` - mapping from dimension name to length + * ``variables`` - mapping from variable name to + ``{"dimensions", "data", "attributes"}`` + * ``attributes`` - global NetCDF attributes - if Type == 'strain': - C_grt_static_compute_strain( - len(norths), len(easts), norths.ctypes.data_as(PREAL), easts.ctypes.data_as(PREAL), - u_ptrs, upar_ptrs, res_ptrs, chs == ZNEchs) - else: - C_grt_static_compute_rotation( - len(norths), len(easts), norths.ctypes.data_as(PREAL), easts.ctypes.data_as(PREAL), - u_ptrs, upar_ptrs, res_ptrs, chs == ZNEchs) - - for i1 in range(i1_end): - for i2 in range(i1+i2_offset, CHANNEL_NUM): - resDct[f"{chs[i1]}{chs[i2]}"] = resarr[i2, i1] - - return resDct - - -def compute_strain(st:Union[Stream,dict]): - r""" - Compute dynamic/static strain tensor from synthetic spatial derivatives. + Variable arrays are available at ``variables[name]["data"]``. - :param st: synthetic spatial derivatives - :class:`obspy.Stream` class for dynamic case, dict class for static case. + :param path: Path to the static NetCDF file. - :return: - - **stres** - dynamic/static strain tensor, in :class:`obspy.Stream` class or dict class. + :return: A dictionary containing the NetCDF data and metadata. """ - if isinstance(st, Stream): - return _compute_strain_rotation(st, "strain") - elif isinstance(st, dict): - return _compute_static_strain_rotation(st, "strain") - else: - raise NotImplementedError - -def compute_rotation(st:Union[Stream,dict]): - r""" - Compute dynamic/static rotation tensor from synthetic spatial derivatives. - - :param st: synthetic spatial derivatives - :class:`obspy.Stream` class for dynamic case, dict class for static case. - - :return: - - **stres** - dynamic/static rotation tensor, in :class:`obspy.Stream` class or dict class. + path = str(path) + if not Path(path).is_file(): + raise FileNotFoundError(f"NetCDF file does not exist: {path}") + + with netcdf_file(path, mode="r", mmap=False) as dataset: + dimensions = {name: int(length) for name, length in dataset.dimensions.items()} + attributes = {name: _attribute_value(getattr(dataset, name)) for name in dataset._attributes} + variables = {} + result = { + "dimensions": dimensions, + "variables": variables, + "attributes": attributes, + } + for name, variable in dataset.variables.items(): + data = np.array(variable[:], copy=True) + variable_attributes = {key: _attribute_value(value) for key, value in variable._attributes.items()} + variables[name] = { + "dimensions": tuple(variable.dimensions), + "data": data, + "attributes": variable_attributes, + } + return result + + +def read_static_grn(path: PathLike) -> dict: """ - if isinstance(st, Stream): - return _compute_strain_rotation(st, "rotation") - elif isinstance(st, dict): - return _compute_static_strain_rotation(st, "rotation") - else: - raise NotImplementedError + Read a static Green's function NetCDF file. + This is an alias of :func:`read_static_nc`. -def _compute_stress(st_syn:Stream): - r""" - Compute dynamic stress tensor from synthetic spatial derivatives. - - :param st_syn: synthetic spatial derivatives. + :param path: Path to the static Green's function file. - :return: - - **stream** - dynamic stress tensor (unit: dyne/cm^2 = 0.1 Pa), in :class:`obspy.Stream` class. + :return: A dictionary containing the NetCDF data and metadata. """ - - # 由于有Q值的存在,lambda和mu变成了复数,需在频域进行 - - chs = ZRTchs - rot2ZNE:bool = False - - # 判断是否有标志性的trace - if len(st_syn.select(channel=f"nN")) > 0: - chs = ZNEchs - rot2ZNE = True - - nt = st_syn[0].stats.npts - dt = st_syn[0].stats.delta - dist = st_syn[0].stats.sac['dist'] - va = st_syn[0].stats.sac['user1'] - vb = st_syn[0].stats.sac['user2'] - rho = st_syn[0].stats.sac['user3'] - Qainv = st_syn[0].stats.sac['user4'] - Qbinv = st_syn[0].stats.sac['user5'] - - u, upar, u_ptrs, upar_ptrs = _prepare_dynamic_postprocess_arrays(st_syn, chs, nt) - resarr, res_ptrs = _prepare_dynamic_postprocess_result(nt) - C_grt_compute_stress( - nt, dt, dist, va, vb, rho, Qainv, Qbinv, - u_ptrs, upar_ptrs, res_ptrs, rot2ZNE) - - stres = Stream() - for i1 in range(3): - c1 = chs[i1] - for i2 in range(i1, 3): - c2 = chs[i2] - - tr = st_syn.select(channel=f"{c2.lower()}{c1}")[0].copy() - tr.data = resarr[i2, i1] - tr.stats.channel = tr.stats.sac['kcmpnm'] = f"{c1}{c2}" - stres.append(tr) - - return stres - - -def _compute_static_stress(syn:dict): - r""" - Compute static stress tensor from synthetic spatial derivatives. - - :param syn: synthetic spatial derivatives. - - :return: - - **res** - static stress tensor (unit: dyne/cm^2 = 0.1 Pa), in dict class. + return read_static_nc(path) + + +def _postprocess(path: PathLike, module: str, return_result: bool): + path = Path(path) + if path.is_dir(): + run_grt([module, path]) + # 动态结果只能靠文件名前缀区分 strain/rotation/stress + return read(str(path / f"{module}_*.sac")) if return_result else None + if path.is_file(): + run_grt(["static", module, path]) + return read_static_nc(path) if return_result else None + raise FileNotFoundError(f"Synthesis result does not exist: {path}") + + +def compute_strain( + path: PathLike, + *, + return_result: bool = False, +): """ - - chs = ZRTchs - rot2ZNE:bool = False - - # 判断是否有标志性的分量名 - if f"nN" in syn.keys(): - chs = ZNEchs - rot2ZNE = True - - norths, easts, u, upar, u_ptrs, upar_ptrs, resarr, res_ptrs = \ - _prepare_static_postprocess_arrays(syn, chs) - va = syn['_rcv_va'] - vb = syn['_rcv_vb'] - rho = syn['_rcv_rho'] - mu = vb*vb*rho*1e10 - lam = va*va*rho*1e10 - 2.0*mu + Compute the strain tensor in place from synthetic spatial derivatives. - # 结果字典 - resDct = {} + ``path`` may be either: - # 基本数据拷贝 - for k in syn.keys(): - if k[0] != '_': - continue - resDct[k] = deepcopy(syn[k]) + * a dynamic synthesis directory containing SAC files, processed by + ``grt strain`` + * a static synthesis NetCDF file, processed by ``grt static strain`` - C_grt_static_compute_stress( - len(norths), len(easts), norths.ctypes.data_as(PREAL), easts.ctypes.data_as(PREAL), - u_ptrs, upar_ptrs, res_ptrs, rot2ZNE, mu, lam) + The synthesis must have been computed with ``calc_upar=True``. Results are + written back to the same directory or file. - for i1 in range(CHANNEL_NUM): - for i2 in range(i1, CHANNEL_NUM): - resDct[f"{chs[i1]}{chs[i2]}"] = resarr[i2, i1] + :param path: Dynamic SAC directory or static NetCDF file. + :param return_result: If true, read and return the processed result. + For a dynamic directory this reads + ``strain_*.sac`` only; for a static NetCDF file + this returns the full NetCDF dictionary. - return resDct - - -def compute_stress(st:Union[Stream,dict]): - r""" - Compute dynamic/static stress tensor from synthetic spatial derivatives. + :return: An :class:`obspy.Stream` or NetCDF dictionary when + ``return_result`` is true; otherwise ``None``. + """ + return _postprocess(path, "strain", return_result) - :param st: synthetic spatial derivatives - :class:`obspy.Stream` class for dynamic case, dict class for static case. - :return: - - **stres** - dynamic/static stress tensor (unit: dyne/cm^2 = 0.1 Pa), in :class:`obspy.Stream` class or dict class. +def compute_rotation( + path: PathLike, + *, + return_result: bool = False, +): """ - if isinstance(st, Stream): - return _compute_stress(st) - elif isinstance(st, dict): - return _compute_static_stress(st) - else: - raise NotImplementedError + Compute the rotation tensor in place from synthetic spatial derivatives. + ``path`` may be either: -def __check_trace_attr_sac(tr:Trace, **kwargs): - ''' - 临时函数,检查trace中是否有sac字典,并将kwargs内容填入 - ''' - if hasattr(tr.stats, 'sac'): - for k, v in kwargs.items(): - tr.stats.sac[k] = v - else: - tr.stats.sac = AttribDict(**kwargs) + * a dynamic synthesis directory containing SAC files, processed by + ``grt rotation`` + * a static synthesis NetCDF file, processed by ``grt static rotation`` + The synthesis must have been computed with ``calc_upar=True``. Results are + written back to the same directory or file. -#================================================================================================================= -# -# 卷积、微分、积分、保存SAC -# -#================================================================================================================= + :param path: Dynamic SAC directory or static NetCDF file. + :param return_result: If true, read and return the processed result. + For a dynamic directory this reads + ``rotation_*.sac`` only; for a static NetCDF file + this returns the full NetCDF dictionary. + :return: An :class:`obspy.Stream` or NetCDF dictionary when + ``return_result`` is true; otherwise ``None``. + """ + return _postprocess(path, "rotation", return_result) -# def stream_convolve(st0:Stream, timearr:np.ndarray, inplace=True): -# ''' -# 频域实现线性卷 -# ''' -# st = st0 if inplace else deepcopy(st0) - -# sacAttr = st[0].stats.sac -# try: -# wI = sacAttr['user0'] # 虚频率 -# except: -# wI = 0.0 -# nt = sacAttr['npts'] -# dt = sacAttr['delta'] - -# nt2 = len(timearr) -# N = nt+nt2-1 -# nf = N//2 + 1 - -# wI_exp1 = np.exp(-wI*np.arange(0,nt)*dt) -# wI_exp2 = np.exp( wI*np.arange(0,nt)*dt) - -# fft_tf = np.ones((nf, ), dtype='c16') -# # if scale is None: -# # scale = 1.0/np.trapz(timearr, dx=dt) - -# timearr0 = timearr.copy() -# timearr0.resize((N,)) # 填充0 -# timearr0[:nt] *= wI_exp1 -# # FFT -# fft_tf[:] = rfft(timearr0, N) -# fft_tf[:] *= dt - -# # 对每一道做相同处理 -# for tr in st: -# data = tr.data -# # 虚频率 -# data[:] *= wI_exp1 +def compute_stress( + path: PathLike, + *, + return_result: bool = False, +): + """ + Compute the stress tensor in place from synthetic spatial derivatives. -# # FFT -# fft_d = rfft(data, N) + ``path`` may be either: -# # 卷积+系数 -# fft_d[:] *= fft_tf + * a dynamic synthesis directory containing SAC files, processed by + ``grt stress`` + * a static synthesis NetCDF file, processed by ``grt static stress`` -# # IFFT -# data[:] = irfft(fft_d, N)[:nt] + The synthesis must have been computed with ``calc_upar=True``. Results are + written back to the same directory or file. Stress unit is + dyne/cm² (= 0.1 Pa). -# # 虚频率 -# data[:] *= wI_exp2 + :param path: Dynamic SAC directory or static NetCDF file. + :param return_result: If true, read and return the processed result. + For a dynamic directory this reads + ``stress_*.sac`` only; for a static NetCDF file + this returns the full NetCDF dictionary. -# return st + :return: An :class:`obspy.Stream` or NetCDF dictionary when + ``return_result`` is true; otherwise ``None``. + """ + return _postprocess(path, "stress", return_result) -def stream_convolve(st0:Stream, signal0:np.ndarray, inplace=True): - ''' - convolve each trace with a signal +def stream_convolve(st0: Stream, signal0: np.ndarray, inplace: bool = True) -> Stream: + """ + Convolve every trace with a discrete signal. - :param st0: :class:`obspy.Stream` - :param signal0: convolution signal - :param inplace: whether change in-place + :param st0: Input ObsPy stream. + :param signal0: Discrete convolution signal. + :param inplace: Whether to modify ``st0`` in place. - :return: - - **stream** - convolution result, :class:`obspy.Stream` - ''' + :return: The convolved ObsPy stream. + """ st = st0 if inplace else deepcopy(st0) - signal = deepcopy(signal0) - - for tr in st: - data = tr.data - dt = tr.stats.delta - - fac = None - user_wI = hasattr(tr.stats, "sac") and "user0" in tr.stats.sac - # 使用虚频率先压制 - if user_wI: - npts = tr.stats.npts - wI = tr.stats.sac['user0'] - fac = np.exp(np.arange(0, npts)*dt*wI) - signal = deepcopy(signal0) - - signal[:] /= fac[:len(signal)] - data[:] /= fac - - data1 = np.pad(data, (len(signal)-1, 0), mode='wrap') # 强制循环卷 - data[:] = oaconvolve(data1, signal, mode='valid')[:data.shape[0]] * dt # dt是连续卷积的系数 - - if user_wI: - data[:] *= fac - + signal = np.asarray(signal0, dtype=float) + for trace in st: + dt = trace.stats.delta + data = trace.data + if hasattr(trace.stats, "sac") and "user0" in trace.stats.sac: + npts = trace.stats.npts + w_i = trace.stats.sac["user0"] + factor = np.exp(np.arange(npts) * dt * w_i) + adjusted_signal = signal / factor[: len(signal)] + data[:] /= factor + data1 = np.pad(data, (len(signal) - 1, 0), mode="wrap") + data[:] = oaconvolve(data1, adjusted_signal, mode="valid")[:npts] * dt + data[:] *= factor + else: + data1 = np.pad(data, (len(signal) - 1, 0), mode="wrap") + data[:] = oaconvolve(data1, signal, mode="valid")[: len(data)] * dt return st -def stream_integral(st0:Stream, inplace=True): - ''' - Perform integration on each trace - - :param st0: :class:`obspy.Stream` - :param inplace: whether change in-place +def stream_integral(st0: Stream, inplace: bool = True) -> Stream: + """ + Integrate every trace with the trapezoidal rule. - :return: - - **stream** - integration result, :class:`obspy.Stream` - ''' + :param st0: Input ObsPy stream. + :param inplace: Whether to modify ``st0`` in place. + + :return: The integrated ObsPy stream. + """ st = st0 if inplace else deepcopy(st0) - for tr in st: - dt = tr.stats.delta - data = tr.data - lastx = data[0] + for trace in st: + dt = trace.stats.delta + data = trace.data + last = data[0] data[0] = 0.0 - - for i in range(1, len(data)): - tmp = data[i] - data[i] = 0.5*(data[i] + lastx)*dt + data[i-1] - lastx = tmp - + for index in range(1, len(data)): + current = data[index] + data[index] = 0.5 * (current + last) * dt + data[index - 1] + last = current return st -def stream_diff(st0:Stream, inplace=True): - ''' - Perform central difference on each trace +def stream_diff(st0: Stream, inplace: bool = True) -> Stream: + """ + Differentiate every trace with a centered finite difference. - :param st0: :class:`obspy.Stream` - :param inplace: whether change in-place + :param st0: Input ObsPy stream. + :param inplace: Whether to modify ``st0`` in place. - :return: - - **stream** - difference result, :class:`obspy.Stream` - ''' + :return: The differentiated ObsPy stream. + """ st = st0 if inplace else deepcopy(st0) - - for tr in st: - data = tr.data - data[:] = np.gradient(data, tr.stats.delta) - + for trace in st: + trace.data[:] = np.gradient(trace.data, trace.stats.delta) return st -def stream_write_sac(st:Stream, dir:str): - ''' - save each trace to "dir/{channel}.sac" - - :param st: :class:`obspy.Stream` - :param dir: saving directory - - ''' - # 新建对应文件夹 - os.makedirs(dir, exist_ok=True) - - # 每一道的保存路径为 dir/{channel}.sac - for tr in st: - filepath = os.path.join(dir, f"{tr.stats.channel}.sac") - tr.write(filepath, format='SAC') - - +def stream_write_sac(st: Stream, directory: PathLike) -> None: + """ + Write each trace to ``directory/{channel}.sac``. + :param st: ObsPy stream to write. + :param directory: Directory for the SAC files. + """ + directory = Path(directory) + directory.mkdir(parents=True, exist_ok=True) + for trace in st: + trace.write(str(directory / f"{trace.stats.channel}.sac"), format="SAC") #================================================================================================================= @@ -990,7 +325,7 @@ def read_statsfile(statsfile:str): if len(Lst) != 1: raise OSError(f"{statsfile} should only match one file, but {len(Lst)} matched.") statsfile = Lst[0] - print(f"raed in {statsfile}.") + print(f"read in {statsfile}.") basename = os.path.basename(statsfile) @@ -1019,7 +354,7 @@ def read_kernels_freqs(statsdir:str, vels:Union[np.ndarray,None]=None, ktypes:Un :param vels: When a positive-order vels (km/s) is specified, files starting with `K_` are read and linear interpolation from wavenumber to phase velocity is performed. Otherwise read the files starting with `C_` - :param ktype: Specify the return of a series of kernel function names, + :param ktypes: Specify the return of a series of kernel function names, such as `EX_q`, `DS_w`, etc. By default, all are returned :return: @@ -1294,7 +629,7 @@ def plot_statsdata(statsdata:np.ndarray, dist:float, srctype:str, ptype:str, Ror ax3.plot(karr, np.real(Parr), lw=0.8, label='Real') else: ax3.plot(karr, np.imag(Parr), lw=0.8, label='Imag') - ax3.set_title(f'$\sum_k$ {FJname}') + ax3.set_title(rf'$\sum_k$ {FJname}') ax3.set_xlabel("k /$km^{-1}$") ax3.grid() ax3.legend(loc='lower left') @@ -1302,7 +637,7 @@ def plot_statsdata(statsdata:np.ndarray, dist:float, srctype:str, ptype:str, Ror return fig, (ax1, ax2, ax3) -def plot_statsdata_ptam(statsdata1:np.ndarray, statsdata2:np.ndarray, statsdata_ptam:np.ndarray, +def plot_statsdata_ptam(statsdata1:np.ndarray, statsdata2:np.ndarray, statsdata_ptam:np.ndarray, dist:float, srctype:str, ptype:str, RorI:Union[bool,int]=True, fig:Union[Figure,None]=None, axs:Union[Axes,None]=None): r''' @@ -1403,12 +738,11 @@ def plot_statsdata_ptam(statsdata1:np.ndarray, statsdata2:np.ndarray, statsdata_ ax3.plot(ptKarr, np.imag(ptFJarr), 'r+', markersize=6) - ax3.set_title(f'$\sum_k$ {FJname}') + ax3.set_title(rf'$\sum_k$ {FJname}') ax3.set_xlabel("k /$km^{-1}$") ax3.grid() ax3.legend(loc='lower left') - return fig, (ax1, ax2, ax3) diff --git a/test/_compare_c_py/compare.py b/test/_compare_c_py/compare.py index ff24f8e1..78af8fa0 100644 --- a/test/_compare_c_py/compare.py +++ b/test/_compare_c_py/compare.py @@ -1,172 +1,306 @@ -import numpy as np -import pygrt -from obspy import * -from compare_func import compare3, update_dict, static_compare3 - - -dist=10 -depsrc=2 -deprcv=3.3 - -nt=1024 -dt=0.01 - -modname="../milrow" - -modarr = np.loadtxt(modname) - -pymod = pygrt.PyModel1D(modarr, depsrc, deprcv) - -AVGRERR = [] - -#-------------------------- Dynamic ----------------------------------------- -# compute green functions -st_grn = pymod.compute_grn(dist, nt, dt, calc_upar=True)[0] - -S=1e24 -az=39.2 - -fn=2 -fe=-1 -fz=4 - -stk=77 -dip=88 -rak=99 - -M11=1 -M12=-2 -M13=-5 -M22=0.5 -M23=3 -M33=1.2 - -for ZNE in [False, True]: - suffix = "-N" if ZNE else "" - # synthetic - st = pygrt.utils.gen_syn_from_gf_EX(st_grn, S, az, ZNE=ZNE, calc_upar=True) - sigs = pygrt.sigs.gen_triangle_wave(0.4, dt) - pygrt.utils.stream_convolve(st, sigs) - AVGRERR.append(compare3(st, f"syn_ex{suffix}/", ZNE=ZNE)) - ststrain = pygrt.utils.compute_strain(st) - strotation = pygrt.utils.compute_rotation(st) - ststress = pygrt.utils.compute_stress(st) - AVGRERR.append(compare3(ststrain, f"syn_ex{suffix}/strain_", ZNE=ZNE, dim2=True)) - AVGRERR.append(compare3(strotation, f"syn_ex{suffix}/rotation_", ZNE=ZNE, dim2=True)) - AVGRERR.append(compare3(ststress, f"syn_ex{suffix}/stress_", ZNE=ZNE, dim2=True)) - - - st = pygrt.utils.gen_syn_from_gf_SF(st_grn, S, fn, fe, fz, az, ZNE=ZNE, calc_upar=True) - sigs = pygrt.sigs.gen_trap_wave(0.1, 0.3, 0.6, dt) - pygrt.utils.stream_convolve(st, sigs) - AVGRERR.append(compare3(st, f"syn_sf{suffix}/", ZNE=ZNE)) - ststrain = pygrt.utils.compute_strain(st) - strotation = pygrt.utils.compute_rotation(st) - ststress = pygrt.utils.compute_stress(st) - AVGRERR.append(compare3(ststrain, f"syn_sf{suffix}/strain_", ZNE=ZNE, dim2=True)) - AVGRERR.append(compare3(strotation, f"syn_sf{suffix}/rotation_", ZNE=ZNE, dim2=True)) - AVGRERR.append(compare3(ststress, f"syn_sf{suffix}/stress_", ZNE=ZNE, dim2=True)) - - st = pygrt.utils.gen_syn_from_gf_DC(st_grn, S, stk, dip, rak, az, ZNE=ZNE, calc_upar=True) - sigs = pygrt.sigs.gen_parabola_wave(0.6, dt) - pygrt.utils.stream_convolve(st, sigs) - AVGRERR.append(compare3(st, f"syn_dc{suffix}/", ZNE=ZNE)) - ststrain = pygrt.utils.compute_strain(st) - strotation = pygrt.utils.compute_rotation(st) - ststress = pygrt.utils.compute_stress(st) - AVGRERR.append(compare3(ststrain, f"syn_dc{suffix}/strain_", ZNE=ZNE, dim2=True)) - AVGRERR.append(compare3(strotation, f"syn_dc{suffix}/rotation_", ZNE=ZNE, dim2=True)) - AVGRERR.append(compare3(ststress, f"syn_dc{suffix}/stress_", ZNE=ZNE, dim2=True)) - - st = pygrt.utils.gen_syn_from_gf_TS(st_grn, S, stk, dip, az, ZNE=ZNE, calc_upar=True) - sigs = pygrt.sigs.gen_parabola_wave(0.6, dt) - pygrt.utils.stream_convolve(st, sigs) - AVGRERR.append(compare3(st, f"syn_ts{suffix}/", ZNE=ZNE)) - ststrain = pygrt.utils.compute_strain(st) - strotation = pygrt.utils.compute_rotation(st) - ststress = pygrt.utils.compute_stress(st) - AVGRERR.append(compare3(ststrain, f"syn_ts{suffix}/strain_", ZNE=ZNE, dim2=True)) - AVGRERR.append(compare3(strotation, f"syn_ts{suffix}/rotation_", ZNE=ZNE, dim2=True)) - AVGRERR.append(compare3(ststress, f"syn_ts{suffix}/stress_", ZNE=ZNE, dim2=True)) - - st = pygrt.utils.gen_syn_from_gf_MT(st_grn, S, [M11,M12,M13,M22,M23,M33], az, ZNE=ZNE, calc_upar=True) - sigs = pygrt.sigs.gen_ricker_wave(3, dt) - pygrt.utils.stream_convolve(st, sigs) - AVGRERR.append(compare3(st, f"syn_mt{suffix}/", ZNE=ZNE)) - ststrain = pygrt.utils.compute_strain(st) - strotation = pygrt.utils.compute_rotation(st) - ststress = pygrt.utils.compute_stress(st) - AVGRERR.append(compare3(ststrain, f"syn_mt{suffix}/strain_", ZNE=ZNE, dim2=True)) - AVGRERR.append(compare3(strotation, f"syn_mt{suffix}/rotation_", ZNE=ZNE, dim2=True)) - AVGRERR.append(compare3(ststress, f"syn_mt{suffix}/stress_", ZNE=ZNE, dim2=True)) - - -#-------------------------- Static ----------------------------------------- -# 为了方便测试,避免引入其他因素的误差,这里有意避开 0 -norths = np.arange(-3.1, 3.2, 0.6) -easts = np.arange(-4.1, 4.2, 0.8) - -static_grn = pymod.compute_static_grn(norths, easts, calc_upar=True) -AVGRERR2 = [] -# plot_static(static_grn, "static/stgrn.nc") - -for ZNE in [False, True]: - suffix = "-N" if ZNE else "" - static_syn = pygrt.utils.gen_syn_from_gf_EX(static_grn, S, ZNE=ZNE, calc_upar=True) - ststrain = pygrt.utils.compute_strain(static_syn) - strotation = pygrt.utils.compute_rotation(static_syn) - ststress = pygrt.utils.compute_stress(static_syn) - update_dict(static_syn, ststrain, "strain_") - update_dict(static_syn, ststress, "stress_") - update_dict(static_syn, strotation, "rotation_") - AVGRERR2.append(static_compare3(static_syn, f"static/stsyn_ex{suffix}.nc")) - - static_syn = pygrt.utils.gen_syn_from_gf_SF(static_grn, S, fn, fe, fz, ZNE=ZNE, calc_upar=True) - ststrain = pygrt.utils.compute_strain(static_syn) - strotation = pygrt.utils.compute_rotation(static_syn) - ststress = pygrt.utils.compute_stress(static_syn) - update_dict(static_syn, ststrain, "strain_") - update_dict(static_syn, ststress, "stress_") - update_dict(static_syn, strotation, "rotation_") - AVGRERR2.append(static_compare3(static_syn, f"static/stsyn_sf{suffix}.nc")) - - static_syn = pygrt.utils.gen_syn_from_gf_DC(static_grn, S, stk, dip, rak, ZNE=ZNE, calc_upar=True) - ststrain = pygrt.utils.compute_strain(static_syn) - strotation = pygrt.utils.compute_rotation(static_syn) - ststress = pygrt.utils.compute_stress(static_syn) - update_dict(static_syn, ststrain, "strain_") - update_dict(static_syn, ststress, "stress_") - update_dict(static_syn, strotation, "rotation_") - AVGRERR2.append(static_compare3(static_syn, f"static/stsyn_dc{suffix}.nc")) - - static_syn = pygrt.utils.gen_syn_from_gf_TS(static_grn, S, stk, dip, ZNE=ZNE, calc_upar=True) - ststrain = pygrt.utils.compute_strain(static_syn) - strotation = pygrt.utils.compute_rotation(static_syn) - ststress = pygrt.utils.compute_stress(static_syn) - update_dict(static_syn, ststrain, "strain_") - update_dict(static_syn, ststress, "stress_") - update_dict(static_syn, strotation, "rotation_") - AVGRERR2.append(static_compare3(static_syn, f"static/stsyn_ts{suffix}.nc")) - - static_syn = pygrt.utils.gen_syn_from_gf_MT(static_grn, S, [M11,M12,M13,M22,M23,M33], ZNE=ZNE, calc_upar=True) - ststrain = pygrt.utils.compute_strain(static_syn) - strotation = pygrt.utils.compute_rotation(static_syn) - ststress = pygrt.utils.compute_stress(static_syn) - update_dict(static_syn, ststrain, "strain_") - update_dict(static_syn, ststress, "stress_") - update_dict(static_syn, strotation, "rotation_") - AVGRERR2.append(static_compare3(static_syn, f"static/stsyn_mt{suffix}.nc")) - - -print("---------------- dynamic --------------------") -AVGRERR = np.array(AVGRERR) -print(AVGRERR) -print(np.mean(AVGRERR), np.min(AVGRERR), np.max(AVGRERR)) -print("---------------- static --------------------") -AVGRERR2 = np.array(AVGRERR2) -print(AVGRERR2) -print(np.mean(AVGRERR2), np.min(AVGRERR2), np.max(AVGRERR2)) - -if np.mean(AVGRERR) > 0.05 or np.mean(AVGRERR2) > 1e-5: - raise ValueError +""" +端到端比较:同一组物理参数下,直接调用 grt CLI 与 Python API 的结果是否一致 +两侧最终都执行同一 grt 可执行文件,因此本测试主要验证 Python 参数拼装与 +手写 C 命令是否一致,而非两套独立数值实现之间的交叉校验 +""" + +from __future__ import annotations + +import shutil +import sys +from pathlib import Path + +HERE = Path(__file__).resolve().parent +if str(HERE) not in sys.path: + sys.path.insert(0, str(HERE)) + +import pygrt +from pygrt.cli import find_grt, format_float, run_grt +from compare_func import compare_nc_files, compare_sac_dirs, summarize_errors + + +MODEL = (HERE.parent / "milrow").resolve() +WORKDIR = HERE / "_work_compare" + +DIST = 10.0 +DEPSRC = 2.0 +DEPRCV = 3.3 +NT = 1024 +DT = 0.01 +SCALE = 1e24 +AZ = 39.2 +FN, FE, FZ = 2.0, -1.0, 4.0 +STK, DIP, RAK = 77.0, 88.0, 99.0 +MT = (1.0, -2.0, -5.0, 0.5, 3.0, 1.2) +NORTHS = (-3.1, 3.1, 0.6) +EASTS = (-4.1, 4.1, 0.8) + +# Python 包装会显式写出默认选项;C 直调侧使用相同命令,保证参数解析一致 +DYNAMIC_THRESH = 1e-10 +STATIC_THRESH = 1e-10 + + +def _clean_workdir() -> None: + if WORKDIR.exists(): + shutil.rmtree(WORKDIR) + WORKDIR.mkdir(parents=True) + + +def _grn_subdir(root: Path) -> Path: + name = ( + f"{MODEL.name}_{format_float(DEPSRC)}_" + f"{format_float(DEPRCV)}_{format_float(DIST)}" + ) + return root / name + + +def run_c_dynamic(c_root: Path) -> None: + """用与 Python 默认映射一致的命令行计算动态结果""" + grn_root = c_root / "GRN" + grn_root.mkdir(parents=True) + + run_grt([ + "greenfn", + f"-M{MODEL}", + f"-D{format_float(DEPSRC)}/{format_float(DEPRCV)}", + f"-N{NT}/{format_float(DT)}+w0.8+n1", + f"-R{format_float(DIST)}", + f"-O{grn_root}", + "-BfH", + "-H-1/-1", + "-L0", + "-K+k50+s2+e-1", + "-E0/0", + "-e", + ]) + + gdir = _grn_subdir(grn_root) + cases = [ + ("syn_ex", [], "t/0.2/0.2/0.4"), + ("syn_sf", [f"-F{format_float(FN)}/{format_float(FE)}/{format_float(FZ)}"], "t/0.1/0.3/0.6"), + ("syn_dc", [f"-M{format_float(STK)}/{format_float(DIP)}/{format_float(RAK)}"], "p/0.6"), + ("syn_ts", [f"-M{format_float(STK)}/{format_float(DIP)}"], "p/0.6"), + ( + "syn_mt", + ["-T" + "/".join(format_float(v) for v in MT)], + "r/3", + ), + ] + for zne in (False, True): + suffix = "-N" if zne else "" + for name, extra, tf in cases: + out = c_root / f"{name}{suffix}" + cmd = [ + "syn", + f"-G{gdir}", + f"-A{format_float(AZ)}", + f"-S{format_float(SCALE)}", + f"-O{out}", + *extra, + f"-D{tf}", + "-e", + ] + if zne: + cmd.append("-N") + run_grt(cmd) + run_grt(["strain", str(out)]) + run_grt(["rotation", str(out)]) + run_grt(["stress", str(out)]) + + +def run_py_dynamic(py_root: Path) -> None: + """Python API 计算动态结果""" + model = pygrt.PyModel1D(MODEL, "free", "halfspace") + model.set_dynamic_grn_path(py_root / "GRN") + model.compute_grn( + depsrc=DEPSRC, + deprcv=DEPRCV, + distarr=DIST, + nt=NT, + dt=DT, + calc_upar=True, + print_log=False, + ) + + cases = [ + ("syn_ex", "EX", {}, "t/0.2/0.2/0.4"), + ("syn_sf", "SF", {"force": (FN, FE, FZ)}, "t/0.1/0.3/0.6"), + ("syn_dc", "DC", {"strike": STK, "dip": DIP, "rake": RAK}, "p/0.6"), + ("syn_ts", "TS", {"strike": STK, "dip": DIP}, "p/0.6"), + ("syn_mt", "MT", {"moment_tensor": MT}, "r/3"), + ] + for zne in (False, True): + suffix = "-N" if zne else "" + for name, source, kwargs, tf in cases: + out = py_root / f"{name}{suffix}" + model.compute_syn( + dist=DIST, + azimuth=AZ, + scale=SCALE, + output_path=out, + source=source, + time_function=tf, + zne=zne, + calc_upar=True, + **kwargs, + ) + pygrt.utils.compute_strain(out) + pygrt.utils.compute_rotation(out) + pygrt.utils.compute_stress(out) + + +def run_c_static(c_root: Path) -> None: + """用与 Python 默认映射一致的命令行计算静态结果""" + static_dir = c_root / "static" + static_dir.mkdir(parents=True) + grn = static_dir / "stgrn.nc" + + run_grt([ + "static", + "greenfn", + f"-M{MODEL}", + f"-D{format_float(DEPSRC)}/{format_float(DEPRCV)}", + f"-O{grn}", + "-BfH", + f"-X{NORTHS[0]}/{NORTHS[1]}/{NORTHS[2]}", + f"-Y{EASTS[0]}/{EASTS[1]}/{EASTS[2]}", + "-L15", + "-K+k50+e-1", + "-e", + ]) + + cases = [ + ("stsyn_ex", []), + ("stsyn_sf", [f"-F{format_float(FN)}/{format_float(FE)}/{format_float(FZ)}"]), + ("stsyn_dc", [f"-M{format_float(STK)}/{format_float(DIP)}/{format_float(RAK)}"]), + ("stsyn_ts", [f"-M{format_float(STK)}/{format_float(DIP)}"]), + ("stsyn_mt", ["-T" + "/".join(format_float(v) for v in MT)]), + ] + for zne in (False, True): + suffix = "-N" if zne else "" + for name, extra in cases: + out = static_dir / f"{name}{suffix}.nc" + cmd = [ + "static", + "syn", + f"-G{grn}", + f"-S{format_float(SCALE)}", + f"-O{out}", + *extra, + "-e", + ] + if zne: + cmd.append("-N") + run_grt(cmd) + run_grt(["static", "strain", str(out)]) + run_grt(["static", "rotation", str(out)]) + run_grt(["static", "stress", str(out)]) + + +def run_py_static(py_root: Path) -> None: + """Python API 计算静态结果""" + static_dir = py_root / "static" + static_dir.mkdir(parents=True) + model = pygrt.PyModel1D(MODEL) + model.set_static_grn_path(static_dir / "stgrn.nc") + model.compute_static_grn( + depsrc=DEPSRC, + deprcv=DEPRCV, + norths=NORTHS, + easts=EASTS, + calc_upar=True, + ) + + cases = [ + ("stsyn_ex", "EX", {}), + ("stsyn_sf", "SF", {"force": (FN, FE, FZ)}), + ("stsyn_dc", "DC", {"strike": STK, "dip": DIP, "rake": RAK}), + ("stsyn_ts", "TS", {"strike": STK, "dip": DIP}), + ("stsyn_mt", "MT", {"moment_tensor": MT}), + ] + for zne in (False, True): + suffix = "-N" if zne else "" + for name, source, kwargs in cases: + out = static_dir / f"{name}{suffix}.nc" + model.compute_static_syn( + scale=SCALE, + output_path=out, + source=source, + zne=zne, + calc_upar=True, + **kwargs, + ) + pygrt.utils.compute_strain(out) + pygrt.utils.compute_rotation(out) + pygrt.utils.compute_stress(out) + + +def compare_dynamic(c_root: Path, py_root: Path) -> list[float]: + errors = [] + # 格林函数 + errors.append( + compare_sac_dirs(_grn_subdir(py_root / "GRN"), _grn_subdir(c_root / "GRN")) + ) + + for zne in (False, True): + suffix = "-N" if zne else "" + for name in ("syn_ex", "syn_sf", "syn_dc", "syn_ts", "syn_mt"): + py_dir = py_root / f"{name}{suffix}" + c_dir = c_root / f"{name}{suffix}" + # 位移三分量 + disp = "[ZNE].sac" if zne else "[ZRT].sac" + errors.append(compare_sac_dirs(py_dir, c_dir, pattern=disp)) + # 空间导数(两位:小写方向 + 大写分量) + deriv = "[zne][ZNE].sac" if zne else "[zrt][ZRT].sac" + errors.append(compare_sac_dirs(py_dir, c_dir, pattern=deriv)) + # 张量按文件名前缀分别比对,避免 channel 撞名漏比 + for prefix in ("strain_", "rotation_", "stress_"): + errors.append(compare_sac_dirs(py_dir, c_dir, pattern=f"{prefix}*.sac")) + return errors + + +def compare_static(c_root: Path, py_root: Path) -> list[float]: + errors = [] + errors.append( + compare_nc_files(py_root / "static" / "stgrn.nc", c_root / "static" / "stgrn.nc") + ) + for zne in (False, True): + suffix = "-N" if zne else "" + for name in ("stsyn_ex", "stsyn_sf", "stsyn_dc", "stsyn_ts", "stsyn_mt"): + errors.append( + compare_nc_files( + py_root / "static" / f"{name}{suffix}.nc", + c_root / "static" / f"{name}{suffix}.nc", + ) + ) + return errors + + +def main(): + print(f"using grt: {find_grt()}") + _clean_workdir() + c_root = WORKDIR / "c" + py_root = WORKDIR / "py" + c_root.mkdir() + py_root.mkdir() + + print("=== dynamic: C CLI ===") + run_c_dynamic(c_root) + print("=== dynamic: Python API ===") + run_py_dynamic(py_root) + dyn_errors = compare_dynamic(c_root, py_root) + + print("=== static: C CLI ===") + run_c_static(c_root) + print("=== static: Python API ===") + run_py_static(py_root) + st_errors = compare_static(c_root, py_root) + + summarize_errors("dynamic", dyn_errors, DYNAMIC_THRESH) + summarize_errors("static", st_errors, STATIC_THRESH) + print("All end-to-end comparisons passed.") + + +if __name__ == "__main__": + main() diff --git a/test/_compare_c_py/compare_func.py b/test/_compare_c_py/compare_func.py index 894ca306..e8489c02 100644 --- a/test/_compare_c_py/compare_func.py +++ b/test/_compare_c_py/compare_func.py @@ -1,124 +1,158 @@ -import numpy as np -from obspy import * -from scipy.io import netcdf_file -from typing import List -import matplotlib.pyplot as plt - +""" +比较 C CLI 与 Python 文件工作流结果的辅助函数 +""" -def compare3(st_py:Stream, c_prefix:str, ZNE:bool=False, dim2:bool=False): - """return average relative error""" - - if ZNE: - pattern = "[ZNE]" - else: - pattern = "[ZRT]" +from __future__ import annotations - if dim2: - pattern = pattern*2 +from pathlib import Path +from typing import Iterable, Optional, Sequence, Union - st_c = read(f"{c_prefix}{pattern}.sac") - - print(f"{c_prefix}{pattern}") +import numpy as np +from obspy import read +from scipy.io import netcdf_file - error = 0.0 - nerr = 0 - for tr_c in st_c: - tr_py = st_py.select(channel=tr_c.stats.channel)[0] - if np.all(tr_c.data == 0.0) and np.all(tr_py.data == 0.0): - continue - rerr = np.sum(np.abs(tr_c.data - tr_py.data)) / np.mean(np.abs(tr_py.data)) - print(tr_c.stats.channel, rerr) +PathLike = Union[str, Path] - error += rerr - nerr += 1 - - - # fig, axs = plt.subplots(len(st_c), 1, figsize=(10, 1.5*len(st_c))) - # for i, tr_c in enumerate(st_c): - # ax = axs[i] - # t = np.arange(0, tr_c.stats.npts)*tr_c.stats.delta - # tr_py = st_py.select(channel=tr_c.stats.channel)[0] - # ax.plot(t, tr_c.data, label="c_"+tr_c.stats.channel) - # ax.plot(t, tr_py.data, label="py_"+tr_py.stats.channel) - # # 总误差水平线 - # ax.hlines(np.sum(np.abs(tr_c.data - tr_py.data)), *ax.get_xlim()) - # ax.legend() - # ax.grid() - - # plt.show() - - return error/nerr - -def update_dict(resDct:dict, Dct0:dict, prefix:str): - keys = resDct.keys() - for k in Dct0.keys(): - if k in keys: - continue - resDct.update({f"{prefix}{k}": Dct0[k]}) +def _rel_error(a: np.ndarray, b: np.ndarray) -> Optional[float]: + """相对误差;双方全零时返回 None 表示跳过""" + if np.all(a == 0.0) and np.all(b == 0.0): + return None + denom = np.mean(np.abs(b)) + if denom == 0.0: + denom = np.mean(np.abs(a)) + if denom == 0.0: + return None + return float(np.sum(np.abs(a - b)) / denom) -def static_compare3(resDct:dict, c_prefix:str): - """return average relative error""" - print(c_prefix) +def compare_sac_dirs( + dir_py: PathLike, + dir_c: PathLike, + pattern: str = "*.sac", +) -> float: + """ + 比较两个目录下同名 SAC 波形,返回平均相对误差 - # read nc - with netcdf_file(c_prefix, mmap=False) as f: - error = 0.0 - nerr = 0 + 以文件名(而非 channel)配对。应变/旋转/应力的 channel 可能同名 + (如 EE),只能靠 strain_/rotation_/stress_ 前缀区分 + """ + dir_py = Path(dir_py) + dir_c = Path(dir_c) + files_py = {p.name: p for p in dir_py.glob(pattern)} + files_c = {p.name: p for p in dir_c.glob(pattern)} - for k in f.variables: - if k == 'north' or k == 'east': - continue - val1 = resDct[k] - val2 = f.variables[k][:] - - if np.all(val1 == 0.0) and np.all(val2 == 0.0): - continue + print(f"compare SAC: {dir_py} vs {dir_c} ({pattern})") - rerr = np.sum(np.abs(val1 - val2)) / np.mean(np.abs(val2)) - print(k, rerr) + if not files_py and not files_c: + raise AssertionError(f"no SAC files matching {pattern!r} under {dir_py} or {dir_c}") - error += rerr - nerr += 1 + missing_py = sorted(set(files_c) - set(files_py)) + missing_c = sorted(set(files_py) - set(files_c)) + if missing_py: + raise AssertionError(f"missing file(s) in {dir_py}: {missing_py}") + if missing_c: + raise AssertionError(f"missing file(s) in {dir_c}: {missing_c}") - # plot_static(resDct, c_prefix) - - return error/nerr - -def plot_static(resDct:dict, c_prefix:str): - n = len([k for k in resDct.keys() if k[0] != '_']) - fig, axs = plt.subplots(n, 3, figsize=(8, 3*n)) - with netcdf_file(c_prefix, mmap=False) as f: - norths = resDct['_norths'] - easts = resDct['_easts'] + error = 0.0 + nerr = 0 + for name in sorted(files_c): + tr_c = read(str(files_c[name]))[0] + tr_py = read(str(files_py[name]))[0] + if len(tr_c.data) != len(tr_py.data): + raise AssertionError(f"npts mismatch for {name}: py={len(tr_py.data)} c={len(tr_c.data)}") + rerr = _rel_error(tr_py.data, tr_c.data) + if rerr is None: + continue + print(f" {name}: {rerr:.6e}") + error += rerr + nerr += 1 - keys = f.variables - keys.pop('north') - keys.pop('east') + if nerr == 0: + return 0.0 + return error / nerr - for i, k in enumerate(keys): - val1 = resDct[k] - val2 = f.variables[k][:] - vmin = np.min(val1) - vmax = np.max(val1) - norm = np.abs(val2) - norm[norm == 0.0] = 1.0 +def _nc_variable_map(path: PathLike) -> dict: + """读取 NetCDF 变量数据,跳过坐标轴 north/east""" + result = {} + with netcdf_file(str(path), mmap=False) as dataset: + for name, variable in dataset.variables.items(): + if name in {"north", "east"}: + continue + result[name] = np.array(variable[:], copy=True) + return result - print(k, np.mean(np.abs(val1 - val2) / norm), np.max(np.abs(val1 - val2) / norm)) - pcm0 = axs[i, 0].pcolorfast(easts, norths, val1.T, vmin=vmin, vmax=vmax) - pcm1 = axs[i, 1].pcolorfast(easts, norths, val2.T, vmin=vmin, vmax=vmax) - pcm2 = axs[i, 2].pcolorfast(easts, norths, ((val1 - val2) / norm).T) +def compare_nc_files(path_py: PathLike, path_c: PathLike) -> float: + """ + 比较两个静态 NetCDF 文件中的物理量变量,返回平均相对误差 + """ + path_py = Path(path_py) + path_c = Path(path_c) + print(f"compare NC: {path_py} vs {path_c}") - axs[i, 0].set_title("py_" + k) - axs[i, 1].set_title("c_" + k) + py_vars = _nc_variable_map(path_py) + c_vars = _nc_variable_map(path_c) - fig.colorbar(pcm0) - fig.colorbar(pcm1) - fig.colorbar(pcm2) + keys = sorted(set(py_vars) | set(c_vars)) + missing_py = sorted(set(c_vars) - set(py_vars)) + missing_c = sorted(set(py_vars) - set(c_vars)) + if missing_py: + raise AssertionError(f"missing in py NC: {missing_py}") + if missing_c: + raise AssertionError(f"missing in c NC: {missing_c}") - import os - fig.savefig(os.path.basename(c_prefix)[:-3] + ".pdf", bbox_inches='tight') + error = 0.0 + nerr = 0 + for key in keys: + val_py = py_vars[key] + val_c = c_vars[key] + if val_py.shape != val_c.shape: + raise AssertionError(f"shape mismatch for {key}: py={val_py.shape} c={val_c.shape}") + rerr = _rel_error(val_py, val_c) + if rerr is None: + continue + print(f" {key}: {rerr:.6e}") + error += rerr + nerr += 1 + + if nerr == 0: + return 0.0 + return error / nerr + + +def assert_command_has(command: Sequence[object], *tokens: str) -> None: + """断言命令列表包含给定 token""" + values = [str(item) for item in command] + for token in tokens: + if token not in values: + raise AssertionError(f"expected token {token!r} in command:\n {' '.join(values)}") + + +def assert_command_equals(command: Sequence[object], expected: Iterable[str]) -> None: + """断言命令列表与期望完全一致""" + actual = [str(item) for item in command] + expect = [str(item) for item in expected] + if actual != expect: + raise AssertionError( + "command mismatch:\n" + f" actual: {' '.join(actual)}\n" + f" expected: {' '.join(expect)}" + ) + + +def summarize_errors(name: str, errors: Sequence[float], threshold: float) -> float: + """打印误差统计,超阈值则抛错""" + arr = np.asarray(errors, dtype=float) + mean = float(np.mean(arr)) if arr.size else 0.0 + print(f"---------------- {name} --------------------") + print(arr) + if arr.size: + print(f"mean={mean:.6e} min={np.min(arr):.6e} max={np.max(arr):.6e}") + if mean > threshold: + raise AssertionError( + f"{name} mean relative error {mean:.6e} exceeds {threshold:.6e}" + ) + return mean diff --git a/test/_compare_c_py/compare_staticXY.py b/test/_compare_c_py/compare_staticXY.py index 67cdd39b..bf81fc59 100644 --- a/test/_compare_c_py/compare_staticXY.py +++ b/test/_compare_c_py/compare_staticXY.py @@ -1,94 +1,167 @@ +""" +静态合成时指定新 XY 网格:比较 C CLI 与 Python API 结果 + +两侧最终都执行同一 grt 可执行文件,因此本测试主要验证 Python 参数拼装与 +手写 C 命令是否一致,而非两套独立数值实现之间的交叉校验 +""" + +from __future__ import annotations + +import shutil +import sys +from pathlib import Path + +HERE = Path(__file__).resolve().parent +if str(HERE) not in sys.path: + sys.path.insert(0, str(HERE)) + import numpy as np -import pygrt -from obspy import * -from compare_func import update_dict, static_compare3 - -depsrc=2 -deprcv=3.3 - -modname="../milrow" - -modarr = np.loadtxt(modname) - -pymod = pygrt.PyModel1D(modarr, depsrc, deprcv) - -#-------------------------- Static ----------------------------------------- -# 为了方便测试,避免引入其他因素的误差,这里有意避开 0 -norths = np.arange(-3.1, 3.2, 0.6) -easts = np.arange(-4.1, 4.2, 0.8) -S=1e24 - -fn=2 -fe=-1 -fz=4 - -stk=77 -dip=88 -rak=99 - -M11=1 -M12=-2 -M13=-5 -M22=0.5 -M23=3 -M33=1.2 - -static_grn = pymod.compute_static_grn(distarr=np.arange(0, 10+1e-8, 0.1), calc_upar=True) -AVGRERR2 = [] -# plot_static(static_grn, "static/stgrn.nc") - -for ZNE in [False, True]: - suffix = "-N" if ZNE else "" - static_syn = pygrt.utils.gen_syn_from_gf_EX(static_grn, S, ZNE=ZNE, calc_upar=True, norths=norths, easts=easts) - ststrain = pygrt.utils.compute_strain(static_syn) - strotation = pygrt.utils.compute_rotation(static_syn) - ststress = pygrt.utils.compute_stress(static_syn) - update_dict(static_syn, ststrain, "strain_") - update_dict(static_syn, ststress, "stress_") - update_dict(static_syn, strotation, "rotation_") - AVGRERR2.append(static_compare3(static_syn, f"static/stsyn_ex{suffix}.nc")) - - static_syn = pygrt.utils.gen_syn_from_gf_SF(static_grn, S, fn, fe, fz, ZNE=ZNE, calc_upar=True, norths=norths, easts=easts) - ststrain = pygrt.utils.compute_strain(static_syn) - strotation = pygrt.utils.compute_rotation(static_syn) - ststress = pygrt.utils.compute_stress(static_syn) - update_dict(static_syn, ststrain, "strain_") - update_dict(static_syn, ststress, "stress_") - update_dict(static_syn, strotation, "rotation_") - AVGRERR2.append(static_compare3(static_syn, f"static/stsyn_sf{suffix}.nc")) - - static_syn = pygrt.utils.gen_syn_from_gf_DC(static_grn, S, stk, dip, rak, ZNE=ZNE, calc_upar=True, norths=norths, easts=easts) - ststrain = pygrt.utils.compute_strain(static_syn) - strotation = pygrt.utils.compute_rotation(static_syn) - ststress = pygrt.utils.compute_stress(static_syn) - update_dict(static_syn, ststrain, "strain_") - update_dict(static_syn, ststress, "stress_") - update_dict(static_syn, strotation, "rotation_") - AVGRERR2.append(static_compare3(static_syn, f"static/stsyn_dc{suffix}.nc")) - - static_syn = pygrt.utils.gen_syn_from_gf_TS(static_grn, S, stk, dip, ZNE=ZNE, calc_upar=True, norths=norths, easts=easts) - ststrain = pygrt.utils.compute_strain(static_syn) - strotation = pygrt.utils.compute_rotation(static_syn) - ststress = pygrt.utils.compute_stress(static_syn) - update_dict(static_syn, ststrain, "strain_") - update_dict(static_syn, ststress, "stress_") - update_dict(static_syn, strotation, "rotation_") - AVGRERR2.append(static_compare3(static_syn, f"static/stsyn_ts{suffix}.nc")) - - static_syn = pygrt.utils.gen_syn_from_gf_MT(static_grn, S, [M11,M12,M13,M22,M23,M33], ZNE=ZNE, calc_upar=True, norths=norths, easts=easts) - ststrain = pygrt.utils.compute_strain(static_syn) - strotation = pygrt.utils.compute_rotation(static_syn) - ststress = pygrt.utils.compute_stress(static_syn) - update_dict(static_syn, ststrain, "strain_") - update_dict(static_syn, ststress, "stress_") - update_dict(static_syn, strotation, "rotation_") - AVGRERR2.append(static_compare3(static_syn, f"static/stsyn_mt{suffix}.nc")) - - -print("---------------- static --------------------") -AVGRERR2 = np.array(AVGRERR2) -print(AVGRERR2) -print(np.mean(AVGRERR2), np.min(AVGRERR2), np.max(AVGRERR2)) - -if np.mean(AVGRERR2) > 1e-5: - raise ValueError +import pygrt +from pygrt.cli import find_grt, format_float, run_grt +from compare_func import compare_nc_files, summarize_errors + + +MODEL = (HERE.parent / "milrow").resolve() +WORKDIR = HERE / "_work_compare_staticXY" + +DEPSRC = 2.0 +DEPRCV = 3.3 +SCALE = 1e24 +FN, FE, FZ = 2.0, -1.0, 4.0 +STK, DIP, RAK = 77.0, 88.0, 99.0 +MT = (1.0, -2.0, -5.0, 0.5, 3.0, 1.2) +# 格林函数库用震中距采样;合成时再插值到新网格 +DISTARR = np.arange(0.0, 10.0 + 1e-8, 0.1) +NORTHS = (-3.1, 3.1, 0.6) +EASTS = (-4.1, 4.1, 0.8) +THRESH = 1e-10 + + +def _clean_workdir() -> None: + if WORKDIR.exists(): + shutil.rmtree(WORKDIR) + WORKDIR.mkdir(parents=True) + + +def run_c(c_root: Path) -> None: + static_dir = c_root / "static" + static_dir.mkdir(parents=True) + grn = static_dir / "stgrn.nc" + rs = ",".join(format_float(float(r)) for r in DISTARR) + + run_grt([ + "static", + "greenfn", + f"-M{MODEL}", + f"-D{format_float(DEPSRC)}/{format_float(DEPRCV)}", + f"-O{grn}", + "-BfH", + f"-R{rs}", + "-L15", + "-K+k50+e-1", + "-e", + ]) + + cases = [ + ("stsyn_ex", []), + ("stsyn_sf", [f"-F{format_float(FN)}/{format_float(FE)}/{format_float(FZ)}"]), + ("stsyn_dc", [f"-M{format_float(STK)}/{format_float(DIP)}/{format_float(RAK)}"]), + ("stsyn_ts", [f"-M{format_float(STK)}/{format_float(DIP)}"]), + ("stsyn_mt", ["-T" + "/".join(format_float(v) for v in MT)]), + ] + for zne in (False, True): + suffix = "-N" if zne else "" + for name, extra in cases: + out = static_dir / f"{name}{suffix}.nc" + cmd = [ + "static", + "syn", + f"-G{grn}", + f"-S{format_float(SCALE)}", + f"-O{out}", + f"-X{NORTHS[0]}/{NORTHS[1]}/{NORTHS[2]}", + f"-Y{EASTS[0]}/{EASTS[1]}/{EASTS[2]}", + *extra, + "-e", + ] + if zne: + cmd.append("-N") + run_grt(cmd) + run_grt(["static", "strain", str(out)]) + run_grt(["static", "rotation", str(out)]) + run_grt(["static", "stress", str(out)]) + + +def run_py(py_root: Path) -> None: + static_dir = py_root / "static" + static_dir.mkdir(parents=True) + model = pygrt.PyModel1D(MODEL) + model.set_static_grn_path(static_dir / "stgrn.nc") + model.compute_static_grn( + depsrc=DEPSRC, + deprcv=DEPRCV, + distarr=DISTARR, + calc_upar=True, + ) + + cases = [ + ("stsyn_ex", "EX", {}), + ("stsyn_sf", "SF", {"force": (FN, FE, FZ)}), + ("stsyn_dc", "DC", {"strike": STK, "dip": DIP, "rake": RAK}), + ("stsyn_ts", "TS", {"strike": STK, "dip": DIP}), + ("stsyn_mt", "MT", {"moment_tensor": MT}), + ] + for zne in (False, True): + suffix = "-N" if zne else "" + for name, source, kwargs in cases: + out = static_dir / f"{name}{suffix}.nc" + model.compute_static_syn( + scale=SCALE, + output_path=out, + source=source, + norths=NORTHS, + easts=EASTS, + zne=zne, + calc_upar=True, + **kwargs, + ) + pygrt.utils.compute_strain(out) + pygrt.utils.compute_rotation(out) + pygrt.utils.compute_stress(out) + + +def main(): + print(f"using grt: {find_grt()}") + _clean_workdir() + c_root = WORKDIR / "c" + py_root = WORKDIR / "py" + c_root.mkdir() + py_root.mkdir() + + print("=== staticXY: C CLI ===") + run_c(c_root) + print("=== staticXY: Python API ===") + run_py(py_root) + + errors = [ + compare_nc_files( + py_root / "static" / "stgrn.nc", + c_root / "static" / "stgrn.nc", + ) + ] + for zne in (False, True): + suffix = "-N" if zne else "" + for name in ("stsyn_ex", "stsyn_sf", "stsyn_dc", "stsyn_ts", "stsyn_mt"): + errors.append( + compare_nc_files( + py_root / "static" / f"{name}{suffix}.nc", + c_root / "static" / f"{name}{suffix}.nc", + ) + ) + + summarize_errors("staticXY", errors, THRESH) + print("All staticXY comparisons passed.") + + +if __name__ == "__main__": + main() diff --git a/test/_compare_c_py/test_cli_args.py b/test/_compare_c_py/test_cli_args.py new file mode 100644 index 00000000..2eb90350 --- /dev/null +++ b/test/_compare_c_py/test_cli_args.py @@ -0,0 +1,537 @@ +""" +验证 Python API 到 grt CLI 参数的映射是否正确 + +不实际执行数值积分,只捕获 run_grt 收到的命令行参数 +""" + +from __future__ import annotations + +import sys +from pathlib import Path + +import pygrt +from compare_func import assert_command_equals, assert_command_has + + +HERE = Path(__file__).resolve().parent +MODEL = (HERE.parent / "milrow").resolve() + + +class CapturedRunner: + """临时替换 run_grt,记录调用参数""" + + def __init__(self): + self.commands = [] + self.kwargs = [] + + def __call__(self, command, **kwargs): + self.commands.append([str(item) for item in command]) + self.kwargs.append(kwargs) + + +def _patch_run_grt(monkey_target, runner: CapturedRunner): + original = monkey_target.run_grt + monkey_target.run_grt = runner + return original + + +def _restore_run_grt(monkey_target, original): + monkey_target.run_grt = original + + +def test_invalid_gf_source_and_freqband_and_distarr(): + runner = CapturedRunner() + original = _patch_run_grt(pygrt.pymod, runner) + try: + model = pygrt.PyModel1D(MODEL) + model.set_dynamic_grn_path(HERE / "_tmp_args_grn") + + try: + model.compute_grn(depsrc=1.0, deprcv=0.0, distarr=1.0, nt=8, dt=0.1, gf_source=["XX"]) + except ValueError as exc: + assert "gf_source" in str(exc) + else: + raise AssertionError("invalid gf_source should raise ValueError") + + try: + model.compute_grn(depsrc=1.0, deprcv=0.0, distarr=1.0, nt=8, dt=0.1, freqband=[1.0]) + except ValueError as exc: + assert "freqband" in str(exc) + else: + raise AssertionError("short freqband should raise ValueError") + + try: + model.compute_grn(depsrc=1.0, deprcv=0.0, distarr="10.5", nt=8, dt=0.1) + except TypeError as exc: + assert "distarr" in str(exc) + else: + raise AssertionError("string distarr should raise TypeError") + finally: + _restore_run_grt(pygrt.pymod, original) + + +def test_print_log_forwarded_to_run_grt(): + runner = CapturedRunner() + original = _patch_run_grt(pygrt.pymod, runner) + try: + model = pygrt.PyModel1D(MODEL) + model.set_dynamic_grn_path(HERE / "_tmp_args_grn") + + model.compute_grn(depsrc=1.0, deprcv=0.0, distarr=1.0, nt=8, dt=0.1, print_log=True) + assert runner.kwargs[-1].get("print_log") is True + assert "-s" not in runner.commands[-1] + + model.compute_grn(depsrc=1.0, deprcv=0.0, distarr=1.0, nt=8, dt=0.1, print_log=False) + assert runner.kwargs[-1].get("print_log") is False + assert "-s" in runner.commands[-1] + finally: + _restore_run_grt(pygrt.pymod, original) + + +def test_compute_grn_default_and_optional_flags(): + runner = CapturedRunner() + original = _patch_run_grt(pygrt.pymod, runner) + try: + model = pygrt.PyModel1D(MODEL, "free", "halfspace") + model.set_dynamic_grn_path(HERE / "_tmp_args_grn") + + # 默认参数应显式带上 C 侧常用默认片段 + model.compute_grn( + depsrc=2.0, + deprcv=0.0, + distarr=[1.0, 2.5], + nt=32, + dt=0.05, + ) + cmd = runner.commands[-1] + assert_command_equals( + cmd, + [ + "greenfn", + f"-M{MODEL}", + "-D2/0", + "-N32/0.05+w0.8+n1", + "-R1,2.5", + f"-O{model.dynamic_grn_path}", + "-BfH", + "-H-1/-1", + "-L0", + "-K+k50+s2+e-1", + "-E0/0", + ], + ) + + # 各类可选参数拼接到正确的 CLI 选项 + model.compute_grn( + depsrc=3.5, + deprcv=1.25, + distarr=10.0, + nt=64, + dt=0.02, + upsampling_n=2, + freqband=(0.1, 5.0), + zeta=0.6, + keepAllFreq=True, + vmin_ref=1.5, + keps=1e-3, + ampk=3.0, + k0=40.0, + use_kmax_ref=True, + Length=20.0, + filonLength=5.0, + filonCut=2.0, + converg_method="PTAM", + delayT0=1.2, + delayV0=3.4, + skipImagComps=True, + calc_upar=True, + gf_source=["EX", "DC", "HF"], + statsidxs=[0, 3, 7], + print_log=False, + ) + cmd = runner.commands[-1] + assert_command_has( + cmd, + "greenfn", + f"-M{MODEL}", + "-D3.5/1.25", + "-N64/0.02+w0.6+n2+a+f", + "-R10", + f"-O{model.dynamic_grn_path}", + "-BfH", + "-H0.1/5", + "-L20+l5+o2", + "-Cp", + "-K+k40+f+s3+e0.001+v1.5", + "-E1.2/3.4", + "-Gesh", + "-S0,3,7", + "-e", + "-s", + ) + + # ref_first_p 对应 -Ep + model.compute_grn( + depsrc=2.0, + deprcv=0.0, + distarr=5.0, + nt=16, + dt=0.1, + delayT0=0.5, + ref_first_p=True, + converg_method="DCM", + safilonTol=1e-4, + filonCut=1.0, + ) + cmd = runner.commands[-1] + assert_command_has( + cmd, + "-N16/0.1+w0.8+n1", + "-L0+a0.0001+o1", + "-Cd", + "-Ep0.5", + ) + + # 边界条件映射 + for top, bot, expected in [ + ("free", "halfspace", "-BfH"), + ("rigid", "free", "-BrF"), + ("halfspace", "rigid", "-BhR"), + ]: + model2 = pygrt.PyModel1D(MODEL, top, bot) + model2.set_dynamic_grn_path(HERE / "_tmp_args_grn2") + model2.compute_grn( + depsrc=1.0, + deprcv=0.0, + distarr=1.0, + nt=8, + dt=0.1, + ) + assert_command_has(runner.commands[-1], expected) + + # NONE 收敛方法 + model.compute_grn( + depsrc=1.0, + deprcv=0.0, + distarr=1.0, + nt=8, + dt=0.1, + converg_method="NONE", + ) + assert_command_has(runner.commands[-1], "-Cn") + finally: + _restore_run_grt(pygrt.pymod, original) + + +def test_compute_static_grn_xy_and_distarr(): + runner = CapturedRunner() + original = _patch_run_grt(pygrt.pymod, runner) + try: + model = pygrt.PyModel1D(MODEL) + model.set_static_grn_path(HERE / "_tmp_args_static.nc") + + model.compute_static_grn( + depsrc=2.0, + deprcv=3.3, + norths=[-3.1, 3.1, 0.6], + easts=[-4.1, 4.1, 0.8], + calc_upar=True, + stats=True, + use_kmax_ref=True, + k0=30.0, + keps=1e-4, + Length=12.0, + filonLength=2.0, + filonCut=0.5, + converg_method="DCM", + ) + cmd = runner.commands[-1] + assert_command_equals( + cmd, + [ + "static", + "greenfn", + f"-M{MODEL}", + "-D2/3.3", + f"-O{model.static_grn_path}", + "-BfH", + "-X-3.1/3.1/0.6", + "-Y-4.1/4.1/0.8", + "-L12+l2+o0.5", + "-Cd", + "-K+k30+f+e0.0001", + "-S", + "-e", + ], + ) + + model.compute_static_grn( + depsrc=1.0, + deprcv=0.0, + distarr=[0.0, 1.5, 3.0], + safilonTol=1e-5, + converg_method="PTAM", + ) + cmd = runner.commands[-1] + assert_command_has( + cmd, + "static", + "greenfn", + "-R0,1.5,3", + "-L15+a1e-05", + "-Cp", + "-K+k50+e-1", + ) + assert "-X" not in " ".join(cmd) + assert "-Y" not in " ".join(cmd) + finally: + _restore_run_grt(pygrt.pymod, original) + + +def test_compute_syn_source_and_time_function_options(): + runner = CapturedRunner() + original = _patch_run_grt(pygrt.pymod, runner) + try: + model = pygrt.PyModel1D(MODEL) + grn_root = HERE / "_tmp_args_grn" + model.set_dynamic_grn_path(grn_root) + # 按 dist 匹配子目录,测试前需先准备假目录 + grn_dir = grn_root / f"{MODEL.name}_2_3.3_10" + grn_dir.mkdir(parents=True, exist_ok=True) + out = HERE / "_tmp_args_syn" + + # 爆炸源 + 时间函数 / 积分微分 / ZNE / upar + model.compute_syn( + dist=10.0, + azimuth=39.2, + scale=1e24, + output_path=out / "ex", + time_function="t/0.2/0.2/0.4", + integrate_order=1, + differentiate_order=2, + scale_with_mu=True, + zne=True, + calc_upar=True, + ) + cmd = runner.commands[-1] + assert_command_equals( + cmd, + [ + "syn", + f"-G{grn_dir}", + "-A39.2", + "-Su1e+24", + f"-O{out / 'ex'}", + "-Dt/0.2/0.2/0.4", + "-I1", + "-J2", + "-N", + "-e", + ], + ) + + # 单力源 + model.compute_syn( + dist=10.0, + azimuth=12.0, + scale=1e20, + output_path=out / "sf", + source="SF", + force=(2.0, -1.0, 4.0), + time_function="t/0.1/0.3/0.6", + ) + assert_command_has( + runner.commands[-1], + "-F2/-1/4", + "-Dt/0.1/0.3/0.6", + "-S1e+20", + ) + + # 剪切源 / 张裂源 / 矩张量 + model.compute_syn( + dist=10.0, + azimuth=1.0, + scale=1e22, + output_path=out / "dc", + source="DC", + strike=77.0, + dip=88.0, + rake=99.0, + time_function="p/0.6", + ) + assert_command_has(runner.commands[-1], "-M77/88/99", "-Dp/0.6") + + model.compute_syn( + dist=10.0, + azimuth=1.0, + scale=1e22, + output_path=out / "ts", + source="TS", + strike=77.0, + dip=88.0, + time_function="p/0.6", + ) + assert_command_has(runner.commands[-1], "-M77/88", "-Dp/0.6") + + model.compute_syn( + dist=10.0, + azimuth=1.0, + scale=1e22, + output_path=out / "mt", + source="MT", + moment_tensor=(1.0, -2.0, -5.0, 0.5, 3.0, 1.2), + time_function="r/3", + ) + cmd = runner.commands[-1] + assert_command_has( + cmd, + f"-G{grn_dir}", + "-T1/-2/-5/0.5/3/1.2", + "-Dr/3", + ) + finally: + _restore_run_grt(pygrt.pymod, original) + + +def test_compute_static_syn_and_tensor_postprocess_args(): + runner = CapturedRunner() + original_pymod = _patch_run_grt(pygrt.pymod, runner) + original_utils = _patch_run_grt(pygrt.utils, runner) + try: + model = pygrt.PyModel1D(MODEL) + model.set_static_grn_path(HERE / "_tmp_args_static.nc") + out = HERE / "_tmp_args_static_syn.nc" + + model.compute_static_syn( + scale=1e24, + output_path=out, + source="DC", + strike=33.0, + dip=50.0, + rake=120.0, + norths=[-5.0, 5.0, 1.0], + easts=[-4.0, 4.0, 2.0], + zne=True, + calc_upar=True, + scale_with_mu=True, + ) + assert_command_equals( + runner.commands[-1], + [ + "static", + "syn", + f"-G{model.static_grn_path}", + "-Su1e+24", + f"-O{out}", + "-M33/50/120", + "-X-5/5/1", + "-Y-4/4/2", + "-N", + "-e", + ], + ) + + # 张量后处理:目录走动态模块,文件走 static 模块 + dyn = HERE / "_tmp_tensor_dyn" + dyn.mkdir(exist_ok=True) + stc = HERE / "_tmp_tensor_static.nc" + stc.write_bytes(b"placeholder") + + pygrt.utils.compute_strain(dyn) + assert_command_equals(runner.commands[-1], ["strain", str(dyn)]) + pygrt.utils.compute_rotation(dyn) + assert_command_equals(runner.commands[-1], ["rotation", str(dyn)]) + pygrt.utils.compute_stress(stc) + assert_command_equals( + runner.commands[-1], + ["static", "stress", str(stc)], + ) + finally: + _restore_run_grt(pygrt.pymod, original_pymod) + _restore_run_grt(pygrt.utils, original_utils) + + +def test_tensor_return_result_reads_prefix_only(): + """return_result=True 时只读对应前缀的 SAC,不与位移/其它张量混淆""" + import shutil + + import numpy as np + from obspy import Trace + + runner = CapturedRunner() + original = _patch_run_grt(pygrt.utils, runner) + dyn = HERE / "_tmp_tensor_return" + try: + if dyn.exists(): + shutil.rmtree(dyn) + dyn.mkdir() + + # 同 channel 名 EE/NE,只能靠文件名前缀区分 + samples = { + "Z.sac": np.array([1.0, 2.0], dtype=np.float32), + "strain_EE.sac": np.array([10.0, 20.0], dtype=np.float32), + "stress_EE.sac": np.array([100.0, 200.0], dtype=np.float32), + "rotation_NE.sac": np.array([3.0, 4.0], dtype=np.float32), + } + for name, data in samples.items(): + tr = Trace(data=data.copy()) + tr.stats.delta = 0.1 + tr.stats.channel = name.split("_")[-1].removesuffix(".sac") if "_" in name else name[0] + tr.write(str(dyn / name), format="SAC") + + st_strain = pygrt.utils.compute_strain(dyn, return_result=True) + assert_command_equals(runner.commands[-1], ["strain", str(dyn)]) + assert len(st_strain) == 1 + assert np.allclose(st_strain[0].data, samples["strain_EE.sac"]) + + st_rot = pygrt.utils.compute_rotation(dyn, return_result=True) + assert_command_equals(runner.commands[-1], ["rotation", str(dyn)]) + assert len(st_rot) == 1 + assert np.allclose(st_rot[0].data, samples["rotation_NE.sac"]) + + st_stress = pygrt.utils.compute_stress(dyn, return_result=True) + assert_command_equals(runner.commands[-1], ["stress", str(dyn)]) + assert len(st_stress) == 1 + assert np.allclose(st_stress[0].data, samples["stress_EE.sac"]) + finally: + _restore_run_grt(pygrt.utils, original) + shutil.rmtree(dyn, ignore_errors=True) + + +def test_format_helpers(): + from pygrt.cli import format_float, format_range + + assert format_float(1.0) == "1" + assert format_float(1.25) == "1.25" + assert format_float(1e24) == "1e+24" + assert format_range([-3.1, 3.1, 0.6], "norths") == "-3.1/3.1/0.6" + try: + format_range([1, 2], "norths") + except ValueError: + pass + else: + raise AssertionError("format_range should reject non-3-length input") + + +def main(): + tests = [ + test_format_helpers, + test_invalid_gf_source_and_freqband_and_distarr, + test_print_log_forwarded_to_run_grt, + test_compute_grn_default_and_optional_flags, + test_compute_static_grn_xy_and_distarr, + test_compute_syn_source_and_time_function_options, + test_compute_static_syn_and_tensor_postprocess_args, + test_tensor_return_result_reads_prefix_only, + ] + for func in tests: + print(f"[RUN] {func.__name__}") + func() + print(f"[OK ] {func.__name__}") + print("All CLI argument tests passed.") + + +if __name__ == "__main__": + # 保证可直接脚本运行 + sys.path.insert(0, str(HERE)) + main() diff --git a/test/_compare_c_py/test_compare_c_py.sh b/test/_compare_c_py/test_compare_c_py.sh index c489b8d8..a3c5595c 100755 --- a/test/_compare_c_py/test_compare_c_py.sh +++ b/test/_compare_c_py/test_compare_c_py.sh @@ -1,116 +1,24 @@ #!/bin/bash -# Compare results from C API and Python API +# 比较 C CLI 与 Python 文件工作流:参数映射 + 动态/静态结果 set -euo pipefail -dist=10 -depsrc=2 -deprcv=3.3 +cd "$(dirname "$0")" -nt=1024 -dt=0.01 +# 优先使用源码树内的 grt +ROOT="$(cd ../.. && pwd)" +export PATH="${ROOT}/pygrt/C_extension/bin:${PATH}" -modname="milrow" -out="GRN" +echo "grt = $(command -v grt)" +echo "python = $(command -v python)" -rm -rf $out syn_* +echo "========== CLI argument mapping ==========" +python -u test_cli_args.py - -#-------------------------- Dynamic ----------------------------------------- -grt greenfn -M../${modname} -O${out} -N${nt}/${dt} -D${depsrc}/${deprcv} -R${dist} -e - -# convolve different signals -G=$out/${modname}_${depsrc}_${deprcv}_${dist} -S=1e24 -az=39.2 -for N in "" "-N" ; do -grt syn -G$G -Osyn_ex$N -A$az -S$S -Dt/0.2/0.2/0.4 -e $N -grt strain syn_ex$N -grt rotation syn_ex$N -grt stress syn_ex$N - -fn=2 -fe=-1 -fz=4 -grt syn -G$G -Osyn_sf$N -A$az -S$S -F$fn/$fe/$fz -Dt/0.1/0.3/0.6 -e $N -grt strain syn_sf$N -grt rotation syn_sf$N -grt stress syn_sf$N - -stk=77 -dip=88 -rak=99 -grt syn -G$G -Osyn_dc$N -A$az -S$S -M$stk/$dip/$rak -Dp/0.6 -e $N -grt strain syn_dc$N -grt rotation syn_dc$N -grt stress syn_dc$N - -grt syn -G$G -Osyn_ts$N -A$az -S$S -M$stk/$dip -Dp/0.6 -e $N -grt strain syn_ts$N -grt rotation syn_ts$N -grt stress syn_ts$N - -M11=1 -M12=-2 -M13=-5 -M22=0.5 -M23=3 -M33=1.2 -grt syn -G$G -Osyn_mt$N -A$az -S$S -T$M11/$M12/$M13/$M22/$M23/$M33 -Dr/3 -e $N -grt strain syn_mt$N -grt rotation syn_mt$N -grt stress syn_mt$N -done - - -#-------------------------- Static ----------------------------------------- -x1=-3.1 -x2=3.1 -dx=0.6 - -y1=-4.1 -y2=4.1 -dy=0.8 - -rm -rf static - -mkdir -p static -cd static -grt static greenfn -M../../${modname} -D${depsrc}/${deprcv} -X$x1/$x2/$dx -Y$y1/$y2/$dy -e -Ostgrn.nc - -for N in "" "-N" ; do -grt static syn -S$S -e $N -Gstgrn.nc -Ostsyn_ex$N.nc -grt static strain stsyn_ex$N.nc -grt static rotation stsyn_ex$N.nc -grt static stress stsyn_ex$N.nc - -grt static syn -S$S -F$fn/$fe/$fz -e $N -Gstgrn.nc -Ostsyn_sf$N.nc -grt static strain stsyn_sf$N.nc -grt static rotation stsyn_sf$N.nc -grt static stress stsyn_sf$N.nc - -grt static syn -S$S -M$stk/$dip/$rak -e $N -Gstgrn.nc -Ostsyn_dc$N.nc -grt static strain stsyn_dc$N.nc -grt static rotation stsyn_dc$N.nc -grt static stress stsyn_dc$N.nc - -grt static syn -S$S -M$stk/$dip -e $N -Gstgrn.nc -Ostsyn_ts$N.nc -grt static strain stsyn_ts$N.nc -grt static rotation stsyn_ts$N.nc -grt static stress stsyn_ts$N.nc - -grt static syn -S$S -T$M11/$M12/$M13/$M22/$M23/$M33 -e $N -Gstgrn.nc -Ostsyn_mt$N.nc -grt static strain stsyn_mt$N.nc -grt static rotation stsyn_mt$N.nc -grt static stress stsyn_mt$N.nc -done - -cd - - - -# run pygrt and compare +echo "========== end-to-end C vs Python ==========" python -u compare.py +# 清理测试产生的临时文件,保持目录整洁 +rm -rf _work_compare _tmp_args_* _tmp_tensor_* __pycache__ -rm -rf $out syn_* -rm -rf static \ No newline at end of file +echo "All tests in test_compare_c_py.sh passed." diff --git a/test/_compare_c_py/test_compare_staticXY_c_py.sh b/test/_compare_c_py/test_compare_staticXY_c_py.sh index 8bb9f1be..ed25f1cf 100644 --- a/test/_compare_c_py/test_compare_staticXY_c_py.sh +++ b/test/_compare_c_py/test_compare_staticXY_c_py.sh @@ -1,82 +1,20 @@ #!/bin/bash - -# 再测试静态解合成时传入新网格的结果 +# 比较静态合成指定新 XY 网格时 C CLI 与 Python API 的结果 set -euo pipefail -depsrc=2 -deprcv=3.3 - -modname="milrow" -out="GRN" - -rm -rf $out syn_* - - -#-------------------------- Static ----------------------------------------- -x1=-3.1 -x2=3.1 -dx=0.6 - -y1=-4.1 -y2=4.1 -dy=0.8 - -S=1e24 -fn=2 -fe=-1 -fz=4 - -stk=77 -dip=88 -rak=99 - -M11=1 -M12=-2 -M13=-5 -M22=0.5 -M23=3 -M33=1.2 - -rm -rf static - -mkdir -p static -cd static -grt static greenfn -M../../${modname} -D${depsrc}/${deprcv} -R0/10/0.1 -e -Ostgrn.nc - -for N in "" "-N" ; do -grt static syn -S$S -e $N -Gstgrn.nc -Ostsyn_ex$N.nc -X$x1/$x2/$dx -Y$y1/$y2/$dy -grt static strain stsyn_ex$N.nc -grt static rotation stsyn_ex$N.nc -grt static stress stsyn_ex$N.nc - -grt static syn -S$S -F$fn/$fe/$fz -e $N -Gstgrn.nc -Ostsyn_sf$N.nc -X$x1/$x2/$dx -Y$y1/$y2/$dy -grt static strain stsyn_sf$N.nc -grt static rotation stsyn_sf$N.nc -grt static stress stsyn_sf$N.nc - -grt static syn -S$S -M$stk/$dip/$rak -e $N -Gstgrn.nc -Ostsyn_dc$N.nc -X$x1/$x2/$dx -Y$y1/$y2/$dy -grt static strain stsyn_dc$N.nc -grt static rotation stsyn_dc$N.nc -grt static stress stsyn_dc$N.nc - -grt static syn -S$S -M$stk/$dip -e $N -Gstgrn.nc -Ostsyn_ts$N.nc -X$x1/$x2/$dx -Y$y1/$y2/$dy -grt static strain stsyn_ts$N.nc -grt static rotation stsyn_ts$N.nc -grt static stress stsyn_ts$N.nc - -grt static syn -S$S -T$M11/$M12/$M13/$M22/$M23/$M33 -e $N -Gstgrn.nc -Ostsyn_mt$N.nc -X$x1/$x2/$dx -Y$y1/$y2/$dy -grt static strain stsyn_mt$N.nc -grt static rotation stsyn_mt$N.nc -grt static stress stsyn_mt$N.nc -done +cd "$(dirname "$0")" -cd - +ROOT="$(cd ../.. && pwd)" +export PATH="${ROOT}/pygrt/C_extension/bin:${PATH}" +echo "grt = $(command -v grt)" +echo "python = $(command -v python)" -# run pygrt and compare +echo "========== staticXY C vs Python ==========" python -u compare_staticXY.py +# 清理测试产生的临时文件,保持目录整洁 +rm -rf _work_compare_staticXY __pycache__ -rm -rf $out syn_* -rm -rf static \ No newline at end of file +echo "All tests in test_compare_staticXY_c_py.sh passed." diff --git a/test/eigenfn/test_eigenfn.sh b/test/eigenfn/test_eigenfn.sh index c1712190..4f76094e 100755 --- a/test/eigenfn/test_eigenfn.sh +++ b/test/eigenfn/test_eigenfn.sh @@ -9,4 +9,4 @@ grt eigenfn -Cphase_R.nc -F1 -N0/10 -Wegn_R.nc+z0/40/0.1 -Ugroup_R.nc -K+ccsens. grt disp2asc -Ugroup_R.nc -N > group_R.txt -rm *.nc *.txt \ No newline at end of file +rm -rf *.nc *.txt \ No newline at end of file diff --git a/test/eigenv/test_eigenv.sh b/test/eigenv/test_eigenv.sh index 7cd70868..45ffaeae 100755 --- a/test/eigenv/test_eigenv.sh +++ b/test/eigenv/test_eigenv.sh @@ -11,4 +11,4 @@ grt disp2asc -Cphase_R.nc -N > phase_R.txt grt eigenv -M../milrow -SR -X1 > secfunc_R.txt grt eigenv -M../milrow -SL -X1 > secfunc_L.txt -rm *.nc *.txt +rm -rf *.nc *.txt diff --git a/test/greenfn/test_greenfn.py b/test/greenfn/test_greenfn.py index 0b3cdca9..80d48293 100644 --- a/test/greenfn/test_greenfn.py +++ b/test/greenfn/test_greenfn.py @@ -1,46 +1,83 @@ -import numpy as np -import pygrt - -dist=10 -depsrc=2 -deprcv=3 +import shutil +from pathlib import Path -nt=600 -dt=0.02 - -modname="../milrow" +import pygrt -modarr = np.loadtxt(modname) +dist = 10.0 +depsrc = 2.0 +deprcv = 3.0 +nt = 600 +dt = 0.02 +modname = "../milrow" -pymod = pygrt.PyModel1D(modarr, depsrc, deprcv) +pymod = pygrt.PyModel1D(modname) +pymod.set_dynamic_grn_path("GRN") +pymod.compute_grn(depsrc=depsrc, deprcv=deprcv, distarr=dist, nt=nt, dt=dt) +pymod.compute_grn( + depsrc=depsrc, deprcv=deprcv, distarr=dist, nt=nt, dt=dt, calc_upar=True, +) +pymod.compute_grn( + depsrc=depsrc, deprcv=deprcv, distarr=dist, nt=nt, dt=dt, + zeta=0.6, upsampling_n=10, +) +pymod.compute_grn( + depsrc=depsrc, deprcv=deprcv, distarr=dist, nt=nt, dt=dt, + freqband=[1, 10], +) +pymod.compute_grn( + depsrc=depsrc, deprcv=deprcv, distarr=dist, nt=nt, dt=dt, Length=20, +) +pymod.compute_grn( + depsrc=depsrc, deprcv=deprcv, distarr=dist, nt=nt, dt=dt, + k0=4, ampk=1.2, keps=1e-3, vmin_ref=1.5, +) -stgrn = pymod.compute_grn(dist, nt, dt)[0] -stgrn = pymod.compute_grn(dist, nt, dt, calc_upar=True)[0] -stgrn = pymod.compute_grn(dist, nt, dt, zeta=0.6, upsampling_n=10)[0] -stgrn = pymod.compute_grn(dist, nt, dt, freqband=[1, 10])[0] -stgrn = pymod.compute_grn(dist, nt, dt, Length=20)[0] +pymod.compute_grn( + depsrc=depsrc, deprcv=0.0, distarr=2000, nt=1400, dt=1.0, safilonTol=1e-3, +) +pymod.compute_grn( + depsrc=depsrc, deprcv=0.0, distarr=2000, nt=1400, dt=1.0, + safilonTol=1e-3, keepAllFreq=True, +) -stgrn = pymod.compute_grn(dist, nt, dt, k0=4, ampk=1.2, keps=1e-3, vmin_ref=1.5)[0] +pymod.compute_grn( + depsrc=depsrc, deprcv=deprcv, distarr=dist, nt=nt, dt=dt, + Length=20, converg_method="DCM", +) +pymod.compute_grn( + depsrc=depsrc, deprcv=deprcv, distarr=dist, nt=nt, dt=dt, + Length=20, converg_method="PTAM", +) +pymod.compute_grn( + depsrc=depsrc, deprcv=deprcv, distarr=dist, nt=nt, dt=dt, + Length=20, converg_method="none", +) -stgrn = pymod.compute_grn(2000, 1400, 1.0, safilonTol=1e-3)[0] -stgrn = pymod.compute_grn(2000, 1400, 1.0, safilonTol=1e-3, keepAllFreq=True)[0] +pymod.compute_grn( + depsrc=depsrc, deprcv=deprcv, distarr=dist, nt=nt, dt=dt, + Length=20, statsidxs=[1, 10, 20], +) -stgrn = pymod.compute_grn(dist, nt, dt, Length=20, converg_method='DCM')[0] -stgrn = pymod.compute_grn(dist, nt, dt, Length=20, converg_method='PTAM')[0] -stgrn = pymod.compute_grn(dist, nt, dt, Length=20, converg_method='none')[0] +# multi distances +pymod.compute_grn( + depsrc=depsrc, deprcv=0.0, distarr=[6, 8, 10], nt=nt, dt=dt, +) -stgrn = pymod.compute_grn(dist, nt, dt, Length=20, statsfile="GRN_grtstats")[0] -stgrn = pymod.compute_grn(dist, nt, dt, Length=20, statsfile="GRN_grtstats", statsidxs=[1,10,20])[0] +# boundary condition +pymod = pygrt.PyModel1D(modname, topbound="free", botbound="free") +pymod.set_dynamic_grn_path("GRN") +pymod.compute_grn(depsrc=depsrc, deprcv=deprcv, distarr=dist, nt=nt, dt=dt) -# multi distances -stgrn = pymod.compute_grn([6, 8, 10], nt, dt)[0] +pymod = pygrt.PyModel1D(modname, topbound="halfspace", botbound="free") +pymod.set_dynamic_grn_path("GRN") +pymod.compute_grn(depsrc=depsrc, deprcv=deprcv, distarr=dist, nt=nt, dt=dt) +pymod = pygrt.PyModel1D(modname, topbound="rigid", botbound="rigid") +pymod.set_dynamic_grn_path("GRN") +pymod.compute_grn(depsrc=depsrc, deprcv=deprcv, distarr=dist, nt=nt, dt=dt) -# boundary condition -pymod = pygrt.PyModel1D(modarr, depsrc, deprcv, topbound='free', botbound='free') -stgrn = pymod.compute_grn(dist, nt, dt)[0] -pymod = pygrt.PyModel1D(modarr, depsrc, deprcv, topbound='halfspace', botbound='free') -stgrn = pymod.compute_grn(dist, nt, dt)[0] -pymod = pygrt.PyModel1D(modarr, depsrc, deprcv, topbound='rigid', botbound='rigid') -stgrn = pymod.compute_grn(dist, nt, dt)[0] +for name in ["GRN", "GRN_grtstats"]: + p = Path(name) + if p.is_dir(): + shutil.rmtree(p, ignore_errors=True) diff --git a/test/greenfn/test_greenfn.sh b/test/greenfn/test_greenfn.sh index 693ed820..91968ee5 100755 --- a/test/greenfn/test_greenfn.sh +++ b/test/greenfn/test_greenfn.sh @@ -37,7 +37,7 @@ cat > dists < lamb1 python -u test_lamb1.py -rm lamb1 \ No newline at end of file +rm -rf lamb1 \ No newline at end of file diff --git a/test/run_all.sh b/test/run_all.sh index 37b9a21a..38cdf5ee 100755 --- a/test/run_all.sh +++ b/test/run_all.sh @@ -20,4 +20,4 @@ done # 删除下载的脚本 tar -tf pygrt-tests.tar.gz | awk -F/ '{print $1}' | uniq | xargs rm -rf -rm pygrt-tests.tar.gz \ No newline at end of file +rm -rf pygrt-tests.tar.gz \ No newline at end of file diff --git a/test/static_greenfn/test_static_greenfn.py b/test/static_greenfn/test_static_greenfn.py index af6c1e7a..2acf8cad 100644 --- a/test/static_greenfn/test_static_greenfn.py +++ b/test/static_greenfn/test_static_greenfn.py @@ -1,31 +1,62 @@ -import numpy as np -import pygrt +import shutil +from pathlib import Path -depsrc=2 -deprcv=0 -norths = np.arange(-3., 3.1, 0.2) -easts = np.arange(-2., 2.1, 0.2) +import pygrt -modname="../milrow" +depsrc = 2.0 +deprcv = 0.0 +# norths/easts 各为 start/stop/step (km) +norths = [-3.0, 3.0, 0.2] +easts = [-2.0, 2.0, 0.2] +modname = "../milrow" -modarr = np.loadtxt(modname) +pymod = pygrt.PyModel1D(modname) +pymod.set_static_grn_path("stgrn.nc") -pymod = pygrt.PyModel1D(modarr, depsrc, deprcv) -stgrn = pymod.compute_static_grn(norths, easts) -stgrn = pymod.compute_static_grn(norths, easts, calc_upar=True) -stgrn = pymod.compute_static_grn(norths, easts, Length=20) +pymod.compute_static_grn(depsrc=depsrc, deprcv=deprcv, norths=norths, easts=easts) +pymod.compute_static_grn( + depsrc=depsrc, deprcv=deprcv, norths=norths, easts=easts, calc_upar=True, +) +pymod.compute_static_grn( + depsrc=depsrc, deprcv=deprcv, norths=norths, easts=easts, Length=20, +) -stgrn = pymod.compute_static_grn(norths, easts, Length=20, converg_method='DCM') -stgrn = pymod.compute_static_grn(norths, easts, Length=20, converg_method='PTAM') -stgrn = pymod.compute_static_grn(norths, easts, Length=20, converg_method='none') +pymod.compute_static_grn( + depsrc=depsrc, deprcv=deprcv, norths=norths, easts=easts, + Length=20, converg_method="DCM", +) +pymod.compute_static_grn( + depsrc=depsrc, deprcv=deprcv, norths=norths, easts=easts, + Length=20, converg_method="PTAM", +) +pymod.compute_static_grn( + depsrc=depsrc, deprcv=deprcv, norths=norths, easts=easts, + Length=20, converg_method="none", +) -stgrn = pymod.compute_static_grn(norths, easts, k0=4, keps=1e-3) -stgrn = pymod.compute_static_grn(norths, easts, k0=4, statsfile="stgrt_stats") +pymod.compute_static_grn( + depsrc=depsrc, deprcv=deprcv, norths=norths, easts=easts, k0=4, keps=1e-3, +) +pymod.compute_static_grn( + depsrc=depsrc, deprcv=deprcv, norths=norths, easts=easts, k0=4, stats=True, +) # boundary condition -pymod = pygrt.PyModel1D(modarr, depsrc, deprcv, topbound='free', botbound='free') -stgrn = pymod.compute_static_grn(norths, easts) -pymod = pygrt.PyModel1D(modarr, depsrc, deprcv, topbound='halfspace', botbound='free') -stgrn = pymod.compute_static_grn(norths, easts) -pymod = pygrt.PyModel1D(modarr, depsrc, deprcv, topbound='rigid', botbound='rigid') -stgrn = pymod.compute_static_grn(norths, easts) \ No newline at end of file +pymod = pygrt.PyModel1D(modname, topbound="free", botbound="free") +pymod.set_static_grn_path("stgrn.nc") +pymod.compute_static_grn(depsrc=depsrc, deprcv=deprcv, norths=norths, easts=easts) + +pymod = pygrt.PyModel1D(modname, topbound="halfspace", botbound="free") +pymod.set_static_grn_path("stgrn.nc") +pymod.compute_static_grn(depsrc=depsrc, deprcv=deprcv, norths=norths, easts=easts) + +pymod = pygrt.PyModel1D(modname, topbound="rigid", botbound="rigid") +pymod.set_static_grn_path("stgrn.nc") +pymod.compute_static_grn(depsrc=depsrc, deprcv=deprcv, norths=norths, easts=easts) + +for name in ["stgrn.nc", "stgrtstats"]: + p = Path(name) + if p.is_dir(): + shutil.rmtree(p, ignore_errors=True) + elif p.is_file(): + p.unlink(missing_ok=True) diff --git a/test/static_greenfn/test_static_greenfn.sh b/test/static_greenfn/test_static_greenfn.sh index 0417a379..6e231088 100755 --- a/test/static_greenfn/test_static_greenfn.sh +++ b/test/static_greenfn/test_static_greenfn.sh @@ -20,7 +20,7 @@ grt static greenfn -M../milrow -D2/0 -X-3/3/0.2 -Y-2/2/0.2 -S -Ostgrn.nc grt static greenfn -M../milrow -D2/0 -R0/10/0.1 -Ostgrn.nc seq 0 0.1 10 > dists grt static greenfn -M../milrow -D2/0 -Rdists -Ostgrn.nc -rm dists +rm -rf dists grt static greenfn -M../milrow -D2/0 -R2,3,5,8 -Ostgrn.nc diff --git a/test/static_syn/test_static_syn.py b/test/static_syn/test_static_syn.py index 4c0e78e5..91393c27 100644 --- a/test/static_syn/test_static_syn.py +++ b/test/static_syn/test_static_syn.py @@ -1,22 +1,38 @@ -import numpy as np -import pygrt - -depsrc=2 -deprcv=0 -norths = np.arange(-3., 3.1, 0.2) -easts = np.arange(-2., 2.1, 0.2) +from pathlib import Path -modname="../milrow" - -modarr = np.loadtxt(modname) +import pygrt -pymod = pygrt.PyModel1D(modarr, depsrc, deprcv) -stgrn = pymod.compute_static_grn(norths, easts, calc_upar=True) +depsrc = 2.0 +deprcv = 0.0 +norths = [-3.0, 3.0, 0.2] +easts = [-2.0, 2.0, 0.2] +modname = "../milrow" -stsyn = pygrt.utils.gen_syn_from_gf_EX(stgrn, 1e20, 22) -stsyn = pygrt.utils.gen_syn_from_gf_SF(stgrn, 1e16, fN=-1, fE=2, fZ=-4, az=22) -stsyn = pygrt.utils.gen_syn_from_gf_DC(stgrn, 1e20, strike=33, dip=44, rake=55, az=22) -stsyn = pygrt.utils.gen_syn_from_gf_TS(stgrn, 1e20, strike=33, dip=44, az=22) -stsyn = pygrt.utils.gen_syn_from_gf_MT(stgrn, 1e20, [1, -2, -5, 0.5, 3, 1.2], az=22) +pymod = pygrt.PyModel1D(modname) +pymod.set_static_grn_path("stgrn.nc") +pymod.compute_static_grn( + depsrc=depsrc, deprcv=deprcv, norths=norths, easts=easts, calc_upar=True, +) +pymod.compute_static_syn( + scale=1e20, output_path="stsyn.nc", source="EX", +) +pymod.compute_static_syn( + scale=1e16, output_path="stsyn.nc", source="SF", force=(-1, 2, -4), +) +pymod.compute_static_syn( + scale=1e20, output_path="stsyn.nc", source="DC", + strike=33, dip=44, rake=55, +) +pymod.compute_static_syn( + scale=1e20, output_path="stsyn.nc", source="TS", strike=33, dip=44, +) +pymod.compute_static_syn( + scale=1e20, output_path="stsyn.nc", source="MT", + moment_tensor=(1, -2, -5, 0.5, 3, 1.2), +) +for name in ["stgrn.nc", "stsyn.nc"]: + p = Path(name) + if p.is_file(): + p.unlink(missing_ok=True) diff --git a/test/static_syn/test_static_syn.sh b/test/static_syn/test_static_syn.sh index 5afed0ca..c9d37044 100755 --- a/test/static_syn/test_static_syn.sh +++ b/test/static_syn/test_static_syn.sh @@ -19,4 +19,4 @@ grt static syn -S1e20 -F2/-1/4 -N -e -Gstgrn.nc -Ostsyn.nc python -u test_static_syn.py -rm *.nc \ No newline at end of file +rm -rf *.nc \ No newline at end of file diff --git a/test/static_tensors/test_static_tensors.py b/test/static_tensors/test_static_tensors.py index 78fd333c..3438a928 100644 --- a/test/static_tensors/test_static_tensors.py +++ b/test/static_tensors/test_static_tensors.py @@ -1,25 +1,35 @@ -import numpy as np -import pygrt - -depsrc=2 -deprcv=0 -norths = np.arange(-3., 3.1, 0.2) -easts = np.arange(-2., 2.1, 0.2) +from pathlib import Path -modname="../milrow" +import pygrt -modarr = np.loadtxt(modname) +depsrc = 2.0 +deprcv = 0.0 +norths = [-3.0, 3.0, 0.2] +easts = [-2.0, 2.0, 0.2] +modname = "../milrow" -pymod = pygrt.PyModel1D(modarr, depsrc, deprcv) -stgrn = pymod.compute_static_grn(norths, easts, calc_upar=True) +pymod = pygrt.PyModel1D(modname) +pymod.set_static_grn_path("stgrn.nc") +pymod.compute_static_grn( + depsrc=depsrc, deprcv=deprcv, norths=norths, easts=easts, calc_upar=True, +) -stsyn = pygrt.utils.gen_syn_from_gf_EX(stgrn, 1e20, 22, calc_upar=True) +pymod.compute_static_syn( + scale=1e20, output_path="stsyn.nc", source="EX", calc_upar=True, +) +pygrt.utils.compute_strain("stsyn.nc") +pygrt.utils.compute_stress("stsyn.nc") +pygrt.utils.compute_rotation("stsyn.nc") -strain = pygrt.utils.compute_strain(stsyn) -stress = pygrt.utils.compute_stress(stsyn) -rotation = pygrt.utils.compute_rotation(stsyn) +pymod.compute_static_syn( + scale=1e20, output_path="stsyn_zne.nc", source="EX", + zne=True, calc_upar=True, +) +pygrt.utils.compute_strain("stsyn_zne.nc") +pygrt.utils.compute_stress("stsyn_zne.nc") +pygrt.utils.compute_rotation("stsyn_zne.nc") -stsyn = pygrt.utils.gen_syn_from_gf_EX(stgrn, 1e20, 22, ZNE=True, calc_upar=True) -strain = pygrt.utils.compute_strain(stsyn) -stress = pygrt.utils.compute_stress(stsyn) -rotation = pygrt.utils.compute_rotation(stsyn) \ No newline at end of file +for name in ["stgrn.nc", "stsyn.nc", "stsyn_zne.nc"]: + p = Path(name) + if p.is_file(): + p.unlink(missing_ok=True) diff --git a/test/static_tensors/test_static_tensors.sh b/test/static_tensors/test_static_tensors.sh index aaef5cde..7684fcc7 100755 --- a/test/static_tensors/test_static_tensors.sh +++ b/test/static_tensors/test_static_tensors.sh @@ -20,4 +20,4 @@ grt static rotation stsyn_ZNE.nc python -u test_static_tensors.py -rm *.nc \ No newline at end of file +rm -rf *.nc \ No newline at end of file diff --git a/test/syn/test_syn.py b/test/syn/test_syn.py index 880bef45..3fdd4e1f 100644 --- a/test/syn/test_syn.py +++ b/test/syn/test_syn.py @@ -1,50 +1,84 @@ -import numpy as np +import shutil +from pathlib import Path + import pygrt -dist=10 -depsrc=2 -deprcv=3 - -nt=600 -dt=0.02 - -modname="../milrow" - -modarr = np.loadtxt(modname) - -pymod = pygrt.PyModel1D(modarr, depsrc, deprcv) -stgrn = pymod.compute_grn(dist, nt, dt, calc_upar=True)[0] - -stsyn = pygrt.utils.gen_syn_from_gf_EX(stgrn, 1e20, 22) -stsyn = pygrt.utils.gen_syn_from_gf_SF(stgrn, 1e16, fN=-1, fE=2, fZ=-4, az=22) -stsyn = pygrt.utils.gen_syn_from_gf_DC(stgrn, 1e20, strike=33, dip=44, rake=55, az=22) -stsyn = pygrt.utils.gen_syn_from_gf_TS(stgrn, 1e20, strike=33, dip=44, az=22) -stsyn = pygrt.utils.gen_syn_from_gf_MT(stgrn, 1e20, [1, -2, -5, 0.5, 3, 1.2], az=22) - - -stsyn = pygrt.utils.gen_syn_from_gf_EX(stgrn, 1e20, 22) - -sigs = pygrt.sigs.gen_parabola_wave(0.6, dt) -stsyn0 = stsyn.copy() -pygrt.utils.stream_convolve(stsyn0, sigs) -sigs = pygrt.sigs.gen_trap_wave(0.2, 0.4, 0.7, dt) -stsyn0 = stsyn.copy() -pygrt.utils.stream_convolve(stsyn0, sigs) -sigs = pygrt.sigs.gen_triangle_wave(0.8, dt) -stsyn0 = stsyn.copy() -pygrt.utils.stream_convolve(stsyn0, sigs) -sigs = pygrt.sigs.gen_ricker_wave(1.2, dt) -stsyn0 = stsyn.copy() -pygrt.utils.stream_convolve(stsyn0, sigs) -sigs = np.array([0.0, 0.1, 0.2, 0.4, 0.4, 0.4, 0.2, 0.1, 0.0]) -stsyn0 = stsyn.copy() -pygrt.utils.stream_convolve(stsyn0, sigs) - -stsyn0 = stsyn.copy() -pygrt.utils.stream_integral(stsyn0) -stsyn0 = stsyn.copy() -pygrt.utils.stream_diff(stsyn0) - -stsyn = pygrt.utils.gen_syn_from_gf_EX(stgrn, 1e20, 22, calc_upar=True) -stsyn = pygrt.utils.gen_syn_from_gf_EX(stgrn, 1e20, 22, ZNE=True) -stsyn = pygrt.utils.gen_syn_from_gf_EX(stgrn, 1e20, 22, ZNE=True, calc_upar=True) \ No newline at end of file +dist = 10.0 +depsrc = 2.0 +deprcv = 3.0 +nt = 600 +dt = 0.02 +modname = "../milrow" +az = 22.0 + +pymod = pygrt.PyModel1D(modname) +pymod.set_dynamic_grn_path("GRN") +pymod.compute_grn( + depsrc=depsrc, deprcv=deprcv, distarr=dist, nt=nt, dt=dt, calc_upar=True, +) + +pymod.compute_syn( + dist=dist, azimuth=az, scale=1e20, output_path="syn", source="EX", +) +pymod.compute_syn( + dist=dist, azimuth=az, scale=1e16, output_path="syn", source="SF", + force=(-1, 2, -4), +) +pymod.compute_syn( + dist=dist, azimuth=az, scale=1e20, output_path="syn", source="DC", + strike=33, dip=44, rake=55, +) +pymod.compute_syn( + dist=dist, azimuth=az, scale=1e20, output_path="syn", source="TS", + strike=33, dip=44, +) +pymod.compute_syn( + dist=dist, azimuth=az, scale=1e20, output_path="syn", source="MT", + moment_tensor=(1, -2, -5, 0.5, 3, 1.2), +) + +# 时间函数 +pymod.compute_syn( + dist=dist, azimuth=az, scale=1e20, output_path="syn", source="EX", + time_function="p/0.6", +) +pymod.compute_syn( + dist=dist, azimuth=az, scale=1e20, output_path="syn", source="EX", + time_function="t/0.2/0.4/0.7", +) +pymod.compute_syn( + dist=dist, azimuth=az, scale=1e20, output_path="syn", source="EX", + time_function="t/0.4/0.4/0.8", +) +pymod.compute_syn( + dist=dist, azimuth=az, scale=1e20, output_path="syn", source="EX", + time_function="r/1.2", +) + +# 积分 / 微分 +pymod.compute_syn( + dist=dist, azimuth=az, scale=1e20, output_path="syn", source="EX", + integrate_order=1, +) +pymod.compute_syn( + dist=dist, azimuth=az, scale=1e20, output_path="syn", source="EX", + differentiate_order=1, +) + +# ZNE / 空间导数 +pymod.compute_syn( + dist=dist, azimuth=az, scale=1e20, output_path="syn", source="EX", zne=True, +) +pymod.compute_syn( + dist=dist, azimuth=az, scale=1e20, output_path="syn", source="EX", + calc_upar=True, +) +pymod.compute_syn( + dist=dist, azimuth=az, scale=1e20, output_path="syn", source="EX", + zne=True, calc_upar=True, +) + +for name in ["GRN", "syn"]: + p = Path(name) + if p.is_dir(): + shutil.rmtree(p, ignore_errors=True) diff --git a/test/syn/test_syn.sh b/test/syn/test_syn.sh index 1e8b7904..f0e1b9ea 100755 --- a/test/syn/test_syn.sh +++ b/test/syn/test_syn.sh @@ -4,19 +4,19 @@ set -euo pipefail grt syn -h -grt greenfn -M../milrow -D2/0 -N600/0.02 -R10 -e -OGRN - -grt syn -GGRN/milrow_2_0_10 -A22 -S1e20 -Osyn -grt syn -GGRN/milrow_2_0_10 -A22 -S1e16 -F-1/2/-4 -Osyn -grt syn -GGRN/milrow_2_0_10 -A22 -S1e20 -M33/44/55 -Osyn -grt syn -GGRN/milrow_2_0_10 -A22 -Su1e10 -M33/44/55 -Osyn -grt syn -GGRN/milrow_2_0_10 -A22 -Su1e10 -M33/44 -Osyn -grt syn -GGRN/milrow_2_0_10 -A22 -S1e20 -T1/-2/-5/0.5/3/1.2 -Osyn - -grt syn -GGRN/milrow_2_0_10 -A22 -S1e20 -Dp/0.6 -Osyn -grt syn -GGRN/milrow_2_0_10 -A22 -S1e20 -Dt/0.2/0.4/0.7 -Osyn -grt syn -GGRN/milrow_2_0_10 -A22 -S1e20 -Dt/0.4/0.4/0.8 -Osyn -grt syn -GGRN/milrow_2_0_10 -A22 -S1e20 -Dr/1.2 -Osyn +grt greenfn -M../milrow -D2/3 -N600/0.02 -R10 -e -OGRN + +grt syn -GGRN/milrow_2_3_10 -A22 -S1e20 -Osyn +grt syn -GGRN/milrow_2_3_10 -A22 -S1e16 -F-1/2/-4 -Osyn +grt syn -GGRN/milrow_2_3_10 -A22 -S1e20 -M33/44/55 -Osyn +grt syn -GGRN/milrow_2_3_10 -A22 -Su1e10 -M33/44/55 -Osyn +grt syn -GGRN/milrow_2_3_10 -A22 -Su1e10 -M33/44 -Osyn +grt syn -GGRN/milrow_2_3_10 -A22 -S1e20 -T1/-2/-5/0.5/3/1.2 -Osyn + +grt syn -GGRN/milrow_2_3_10 -A22 -S1e20 -Dp/0.6 -Osyn +grt syn -GGRN/milrow_2_3_10 -A22 -S1e20 -Dt/0.2/0.4/0.7 -Osyn +grt syn -GGRN/milrow_2_3_10 -A22 -S1e20 -Dt/0.4/0.4/0.8 -Osyn +grt syn -GGRN/milrow_2_3_10 -A22 -S1e20 -Dr/1.2 -Osyn cat > tfile < tfile < dists <