弹性动力学基础
约 4111 字大约 14 分钟
2026-03-17
波动方程的解
标量波动方程
标量波动方程,也称声波方程或波动方程,是描述标量场(如声压等)随时间和空间传播的基本方程。一般形式为
∇2P−c21∂t2∂2P=−f(r,t)
其中 P 是标量场,c 是波速,f(r,t) 是源项。
获取标量波动方程的通解的方法有很多,下面仅以分离变量法求解一维齐次标量波动方程为例。
∂x2∂2u−c21∂t2∂2u=0
设 u(x,t)=X(x)T(t),将其代入波动方程,得到
c2∂x2∂2XX1=∂t2∂2TT1=−ω2
注
由于当前情境下没有边界条件和初始条件,分离常数可以是任何实数。
当分离常数大于 0 为 λ2 时,有
X(x)=A1eλx+A2e−λx
T(t)=B1eλct+B2e−λct
当分离常数等于 0 时,有
X(x)=A1+A2x
T(t)=B1+B2t
鉴于我们目前讨论的是无界空间中的波动问题,为简化分析,我们忽略这两种情况。后续的方程同理。
分别解得 Xω(x) 和 Tω(t) 的通解
Xω(x)=A1eikx+A2e−ikx
Tω(t)=B1eikct+B2e−ikct
其中 k=ω/c 为波数,A1,A2,B1,B2 为待定复常数。
u(x,t)=∫0∞Xω(x)Tω(t)dω=∫0∞[A1(ω)eikx+A2(ω)e−ikx][B1(ω)eikct+B2(ω)e−ikct]dω=∫−∞+∞[C1(k)eik(ct−x)+C2(k)eik(ct+x)]dk
写成复数解形式为
u~(x,t)=∫0+∞[Akei(ωt−kx)+Bkei(ωt+kx)]dk
其中 Ak,Bk 是复振幅。
行波解理论
在一维无界空间中,任何波动都可以分解为两个沿相反方向传播、且形状保持不变的波的叠加。这个结论通常被称为达朗贝尔公式:
u(x,t)=f(ct−x)+g(ct+x)
矢量波动方程
矢量波动方程,也称弹性波动方程或纳维尔方程,是描述矢量场(如位移、速度等)随时间和空间传播的基本方程。一般形式为
(λ+2μ)∇(∇⋅u)−μ∇×(∇×u)+f=ρ∂t2∂2u
考虑齐次矢量波动方程
(λ+2μ)∇(∇⋅u)−μ∇×(∇×u)=ρ∂t2∂2u
通过亥姆霍兹定理引入势函数
u=∇ϕ+∇×Ψ,∇⋅Ψ=0
亥姆霍兹定理
任何一个在无限空间中满足适当数学条件(即物理上合理、在无穷远处衰减足够快)的矢量场 F,都可以唯一地分解为两个部分的叠加:
一个无旋场:该部分的旋度为零,它可以用一个标量势函数 ϕ 的梯度来表示。这个部分由矢量场的散度(源)决定。
一个无散场:该部分的散度为零,它可以用一个矢量势函数 A 的旋度来表示。这个部分由矢量场的旋度(涡旋源)决定。
将势函数代入齐次矢量波动方程,得到两组独立的方程
∇2ϕ−α21∂t2∂2ϕ=0,α=ρλ+2μ
∇2Ψ−β21∂t2∂2Ψ=0,β=ρμ,∇⋅Ψ=0
由此可见,矢量波动方程实际上隐含了两种不同类型的波,分别对应于 ∇ϕ 和 ∇×Ψ,我们将其称之为纵波 (Primary Wave) 和横波 (Secondary Wave),简称 P 波和 S 波。P 波的传播速度通常大于 S 波,因此在地震记录中总是先到达。
纵波方程
对于 P 波,势函数 ϕ 满足标量波动方程,可以通过分离变量法求解。
ϕ(x,y,z,t)=X(x)Y(y)Z(z)T(t)
Tω(t)=A1eiωt+A2e−iωt
Xkx(x)=B1eikxx+B2e−ikxx,Yky(y)=C1eikyy+C2e−ikyy,Zkz(z)=D1eikzz+D2e−ikzz
其中 kx2+ky2+kz2=ω2/α2=k2,kx,ky,kz,ω>0,A1,A2,B1,B2,C1,C2,D1,D2 为待定复常数。
注意 ω 和 kx,ky,kz 取值的任意性,整合后得到势函数 ϕ 通解形式为
ϕ(r,t)=∫0∞∫kx2+ky2+kz2=k2Xkx(x)Yky(y)Zkz(z)Tω(t)dkxdkydkzdω=∫0∞Tω(t)dω∫∣k∣=kC(k)eik⋅rdk=∫k[A(k)ei(k⋅r−ωt)+B(k)ei(k⋅r+ωt)]dk
写成复数形式为
ϕ~(r,t)=∫kAkei(ωt−k⋅r)dk
可见 ϕ~ 实际上是不同方向不同频率的平面波的叠加。将其代入表达式,得到 P 波的形式为
u~P=∫∇ϕ~k=∫−ikϕ~k
即 P 波的振动方向与传播方向相同。
横波方程
对于 S 波,势函数 Ψ 满足矢量形式的标量波动方程和无散条件,可以通过相同方法求解。
Ψ(r,t)=∫k[A(k)ei(k⋅r−ωt)+B(k)ei(k⋅r+ωt)]dk
Ψ~(r,t)=∫kAkei(ωt−k⋅r)dk
代入无散条件 ∇⋅Ψ~=0,得到
k⋅Ak=0
u~S=∫∇×Ψ~k=∫−ik×Ψ~k
即 S 波的振动方向与传播方向垂直。
汉森矢量
汉森矢量是求解矢量亥姆霍兹方程的有力工具,它利用标量亥姆霍兹方程的解,构造出满足矢量方程的解。
亥姆霍兹方程
亥姆霍兹方程是描述波动现象空间分布的二阶椭圆型偏微分方程,其基本形式为
∇2A+k2A=0
设 A(r) 是一个标量函数,它满足亥姆霍兹方程
∇2A+k2A=0
那么基于标量函数 A 和常矢量 a 可以构造出三个线性无关的矢量函数,称为汉森矢量:
L=k1∇A
M=∇×(aA)
N=k1∇×∇×(aA)
注意
汉森矢量的系数有什么特殊意义?
它们相互独立且均满足矢量亥姆霍兹方程,L 是无旋场,M 和 N 是无散场。
对于弹性波动方程,我们可以利用汉森矢量表示 P 波和 S 波。
对两组由矢量波动方程得到的独立方程作傅里叶变换,得到频域下的两个方程
∇2ϕ+kP2ϕ=0,kP=ω/α
∇2Ψ+kS2Ψ=0,kS=ω/β
经过验证发现,uP 和 uS 在频域下也满足它们势函数对应的亥姆霍兹方程,即
∇2uP+kP2uP=0
∇2uS+kS2uS=0
为了求解这两个矢量亥姆霍兹方程,我们引入两个标量函数 ϕ 和 ψ,满足
∇2ϕ+kP2ϕ=0
∇2ψ+kS2ψ=0
结合汉森矢量的性质,可以得到
uP=LP=kP1∇ϕ
uSH=MS=∇×(aψ)
uSV=NS=kS1∇×∇×(aψ)
通常在直角坐标系和柱坐标系中取 a=z^,在球坐标系中取 a=r^。这样将 S 波分解为两种偏振方向的波,分别称为 SH 波(水平偏振波,对应 MS)和 SV 波(垂直偏振波,对应 NS)。
地震图的坐标变换
一般地震仪记录时选定的坐标为东西向东为正,南北向北为正,上下向上为正,同时记录三个方向上的时间序列,合成一个地面运动的时空图像。
由于地震波的偏振特性,水平记录经过坐标变换之后能更好的地显示偏振特征,以便于震相的识别。新的水平坐标由 R 轴和 T 轴组成,R 轴为震中与地震仪的连线,T 轴通过地震仪并垂直于 R 轴。

由于 P 波的偏振与传播方向相同,在 R-T-Z 坐标系下它既有垂向的 Z 分量也有径向的 R 分量,SV 波也是如此。而 SH 波的偏振为水平横向,所以它只有 T 分量。

上图是一个实测地震图在 R-T-Z 坐标系下的图像,可以看到 P 波在 T 方向几乎没有能量,SH 波在 T 方向显示清晰的初动。P 波的 Z 分量大于 R 分量,表明射线入射的角度较小(射线与垂直方向的夹角)。SV 波的振幅特性刚好与 P 波相反。
也可以使用 ObsPy 拉取地震数据并进行坐标变换。
以下是一段示例代码,从 IRIS 下载 2013 Okhotsk Sea 地震的波形数据,并将水平分量从 N/E 旋转到 R/T:
"""
Download real waveform data from IRIS and
rotate NE -> RT for the 2013 Okhotsk Sea earthquake.
Requirements:
pip install obspy
"""
from typing import Optional, Tuple
from obspy import UTCDateTime
from obspy.clients.fdsn import Client
from obspy.geodetics.base import gps2dist_azimuth
def _pick_event_near_time(catalog, origin_time: UTCDateTime):
"""Pick the event whose origin time is closest to origin_time."""
if not catalog or len(catalog) == 0:
raise RuntimeError("No events returned from FDSN event service.")
def _event_time_delta_seconds(event) -> float:
origin = event.preferred_origin() or (
event.origins[0] if event.origins else None
)
if origin is None or origin.time is None:
return float("inf")
return float(abs(origin.time - origin_time))
return min(catalog, key=_event_time_delta_seconds)
def _get_event_lat_lon(
origin_time: UTCDateTime,
) -> Tuple[float, float, Optional[float], str]:
"""Fetch event metadata around origin_time.
Returns (lat, lon, depth_km, summary).
"""
# USGS is a reliable FDSN event provider; fall back to EarthScope if needed.
event_clients = [Client("USGS"), Client("EARTHSCOPE")]
last_error: Optional[Exception] = None
for event_client in event_clients:
try:
cat = event_client.get_events(
starttime=origin_time - 15 * 60,
endtime=origin_time + 15 * 60,
minmagnitude=7.0,
includearrivals=False,
)
event = _pick_event_near_time(cat, origin_time)
origin = event.preferred_origin() or (
event.origins[0] if event.origins else None
)
if origin is None:
raise RuntimeError("Selected event has no origin information.")
depth_km: Optional[float] = None
if origin.depth is not None:
depth_km = float(origin.depth) / 1000.0
magnitude = event.preferred_magnitude() or (
event.magnitudes[0] if event.magnitudes else None
)
mag_str = (
f"M{magnitude.mag:.1f}"
if (magnitude and magnitude.mag is not None)
else "M?"
)
summary = f"{mag_str} @ {origin.time.isoformat()}"
return float(origin.latitude), float(origin.longitude), depth_km, summary
except Exception as exc: # keep robust across providers
last_error = exc
raise RuntimeError(
"Failed to fetch event metadata from FDSN providers: "
f"{last_error}"
)
def _get_station_lat_lon(
client: Client,
network: str,
station: str,
location: str,
channel: str,
t_start: UTCDateTime,
t_end: UTCDateTime,
) -> Tuple[float, float, float, str]:
"""Fetch station metadata and return (lat, lon, elevation_m, site_name)."""
inv = client.get_stations(
network=network,
station=station,
location=location,
channel=channel,
starttime=t_start,
endtime=t_end,
level="station",
)
if len(inv.networks) == 0 or len(inv.networks[0].stations) == 0:
raise RuntimeError("Station metadata returned no stations.")
sta = inv.networks[0].stations[0]
site_name = sta.site.name if getattr(sta, "site", None) else ""
return float(sta.latitude), float(sta.longitude), float(sta.elevation), site_name
def main() -> None:
# -------------------------------
# Event / station parameters
# -------------------------------
origin_time = UTCDateTime("2013-05-24T05:44:49") # UTC
t_start = origin_time
t_end = origin_time + 30 * 60 # 30 minutes
network = "II"
station = "BFO"
channel = "BH?" # BHZ, BHN, BHE
location = "*"
# -------------------------------
# 0) Download metadata + compute azimuth/back_azimuth
# -------------------------------
data_client = Client("EARTHSCOPE")
ev_lat, ev_lon, ev_depth_km, ev_summary = _get_event_lat_lon(origin_time)
st_lat, st_lon, st_elev_m, st_site = _get_station_lat_lon(
client=data_client,
network=network,
station=station,
location=location,
channel=channel,
t_start=t_start,
t_end=t_end,
)
# gps2dist_azimuth(lat1, lon1, lat2, lon2) returns:
# distance_m, az12 (from 1->2), az21 (from 2->1)
# If point1=event and point2=station, az21 is the back-azimuth at station
# (station->event).
distance_m, az_ev_to_st, baz_st_to_ev = gps2dist_azimuth(
ev_lat,
ev_lon,
st_lat,
st_lon,
)
azimuth = float(az_ev_to_st)
back_azimuth = float(baz_st_to_ev)
print("Metadata:")
print(
f" Event: lat={ev_lat:.4f}, lon={ev_lon:.4f}, "
f"depth_km={ev_depth_km}, {ev_summary}"
)
print(
f" Station: lat={st_lat:.4f}, lon={st_lon:.4f}, "
f"elev_m={st_elev_m:.1f}, site='{st_site}'"
)
print(f" Distance: {distance_m/1000.0:.1f} km")
print(f" Azimuth (event->station): {azimuth:.2f}°")
print(f" Back-azimuth (station->event): {back_azimuth:.2f}°")
# -------------------------------
# 1) Download waveforms
# -------------------------------
st = data_client.get_waveforms(
network=network,
station=station,
channel=channel,
location=location,
starttime=t_start,
endtime=t_end,
attach_response=False,
)
print(
f"Downloaded {len(st)} traces for {network}.{station} "
f"from {t_start} to {t_end}"
)
print(st)
# Keep a copy for the "before" plot
st_before = st.copy()
# -------------------------------
# 4) Plot before rotation (Z, N, E)
# -------------------------------
st_before.plot(
equal_scale=False,
size=(1000, 600),
)
# -------------------------------
# 2) Basic preprocessing
# -------------------------------
st.detrend("demean")
st.detrend("linear")
st.filter("bandpass", freqmin=0.01, freqmax=0.5)
# -------------------------------
# 3) Rotate N/E -> R/T
# -------------------------------
# Requires N and E components to be present (BHN/BHE). Z is kept as-is.
st.rotate("NE->RT", back_azimuth=back_azimuth)
# -------------------------------
# 4) Plot after rotation (Z, R, T)
# -------------------------------
st.plot(
equal_scale=False,
size=(1000, 600),
)
if __name__ == "__main__":
main()得到 N-E-Z 和 R-T-Z 坐标系下的波形图:


能流密度矢量
通过弹性介质力学,我们知道弹性介质的应变能密度或弹性势能函数可以表示为
W=21εijσij
但介质形变的过程不是准静态的,由于介质的形变运动,能量会在介质中转移,我们可以通过能量守恒来引入一个描述能量流动的物理量,称为能流密度矢量。
将弹性动力学方程与速度作点积,有
21ρ∂t∂u˙2=(∇⋅σ)⋅u˙+f⋅u˙
利用
(∇⋅σ)⋅u˙=∇⋅(u˙⋅σ)−σ:∇u˙
代入上式,得到
21ρ∂t∂u˙2+σ:∇u˙=∇⋅(u˙⋅σ)+f⋅u˙
又由于
σ:∇u˙=21∂t∂(ε:σ)=∂t∂W
代入上式,对整体积分,得到
dtd∫V(21ρu˙2+W)dV=∫Vf⋅u˙dV+∫∂V(σ⋅u˙)⋅dS
定义能流密度矢量为
J=−σ⋅u˙
表示单位时间内通过垂直单位面积的能量,其方向为能量流动方向。
平面波的能量
为了使推导过程更加直观,我们巧妙地选取坐标系,令 x1 轴与平面波的传播方向一致。此时,波数矢量退化为一维 k=(k,0,0),波的相位仅依赖于 (x1,t)。因此,所有物理量对 x2 和 x3 的偏导数均为零。
P 波的能量和能流密度
P 波的质点振动方向与传播方向一致,因此位移仅在 x1 方向有分量。
u=(u1,0,0),u1=Acos(ωt−kpx1+ϕ)
动能密度为
Tp=21ρu˙2=21ρA2ω2sin2(ωt−kpx1+ϕ)
势能密度为
Wp=21εijσij=21ε11σ11=21(λ+2μ)A2kp2sin2(ωt−kpx1+ϕ)=21ρA2ω2sin2(ωt−kpx1+ϕ)
我们发现动能密度等于势能密度(Tp=Wp)。因此时间平均总能量密度为
⟨Ep⟩=⟨Tp⟩+⟨Wp⟩=21ρA2ω2
同时也容易知道 P 波的能流密度仅在 x1 方向有分量,为
S1=−σ11u˙1=−(λ+2μ)ε11u˙1=ρω2A2vpsin2(ωt−kpx1+ϕ)
⟨S1⟩=21ρω2A2vp=⟨Ep⟩vp
S 波的能量和能流密度
S 波的质点振动方向垂直于传播方向,不妨取 x2 方向为偏振方向。
u=(0,u2,0),u2=Acos(ωt−ksx1+ϕ)
动能密度为
Ts=21ρu˙2=21ρA2ω2sin2(ωt−ksx1+ϕ)
势能密度为
Ws=21εijσij=21ε12σ12=21μA2ks2sin2(ωt−ksx1+ϕ)=21ρA2ω2sin2(ωt−ksx1+ϕ)
可以发现 S 波的动能密度同样等于势能密度(Ts=Ws)。
⟨Es⟩=⟨Ts⟩+⟨Ws⟩=21ρA2ω2
同样我们分析知道 S 波的能流密度仅在 x1 方向有分量,为
S1=−σ12u˙2=−με12u˙2=ρω2A2vssin2(ωt−ksx1+ϕ)
⟨S1⟩=21ρω2A2vs=⟨Es⟩vs
总结
- 无论是纵波还是横波,瞬时动能密度始终等于瞬时势能密度:T=W。
- 平均能量密度仅依赖于介质密度、角频率和振幅:⟨E⟩=21ρA2ω2。
- 平均能流密度等于平均能量密度乘以波速:⟨S⟩=⟨E⟩v。