2. 雨流计数结果与峰值谷值序列的对应校验
为什么需要做峰谷校验
雨流计数的输入本质上是“转折点序列”,而不是全部采样点。若直接对等间隔原始时程逐点计数,算法会把大量单调段上的中间采样点当作潜在极值参与比较,虽然标准实现通常能通过“只保留转折点”隐式规避,但不同实现(尤其是简化版)容易在平台段、等值点、端点处产生差异。因此本例的核心任务是:从同一段应力时程分别构造“原始序列”和“峰谷序列”,各自跑一遍雨流计数,逐循环比对幅值、均值与计数,确认预处理不改变循环集合,并定位边界处理带来的差异。
峰谷提取与两路计数流程
峰谷提取采用“相邻差分符号变化”判据:对采样序列 s[0..N-1],计算 d[i]=s[i]-s[i-1],当 d[i] 与 d[i-1] 异号(含零的归并规则)时,s[i-1] 即为转折点。首尾点始终保留为端点。得到峰谷序列后,两路输入分别送入同一套雨流计数核心,输出循环列表 (幅值 Sa, 均值 Sm, 计数)。
可运行的校验实现
下面给出一个自洽的 Python 实现:extract_peaks_valleys 负责峰谷提取,rainflow 采用经典的四点法核心(栈式实现),两路输入共用同一计数函数,最后用排序后的循环列表做逐项比对。示例时程为一段含平台段与等值点的海洋结构应力片段,用于暴露边界处理差异。
import math
def extract_peaks_valleys(s):
"""差分符号变化判据提取峰谷;首尾点保留;平台段取段末点。"""
if len(s) < 3:
return list(s)
pv = [s[0]]
for i in range(1, len(s) - 1):
d_prev = s[i] - s[i - 1]
d_next = s[i + 1] - s[i]
# 跳过平台段中间点:d_prev==0 时向前找非零点
if d_prev == 0:
continue
if d_next == 0:
continue
if (d_prev > 0 and d_next < 0) or (d_prev < 0 and d_next > 0):
pv.append(s[i])
pv.append(s[-1])
return pv
def rainflow(seq):
"""四点法栈式雨流计数,返回 [(Sa, Sm, count)],含半循环。"""
stack = []
cycles = []
for x in seq:
stack.append(x)
while len(stack) >= 4:
a, b, c, d = stack[-4], stack[-3], stack[-2], stack[-1]
r1 = abs(b - a)
r2 = abs(c - b)
r3 = abs(d - c)
if r2 <= r1 and r2 <= r3:
Sa = r2 / 2.0
Sm = (b + c) / 2.0
cycles.append((round(Sa, 6), round(Sm, 6), 1))
del stack[-3:-1] # 移除 b, c
else:
break
# 残余序列按半循环处理
for i in range(len(stack) - 1):
Sa = abs(stack[i + 1] - stack[i]) / 2.0
Sm = (stack[i] + stack[i + 1]) / 2.0
cycles.append((round(Sa, 6), round(Sm, 6), 0.5))
return cycles
def compare(cycles_a, cycles_b):
ca = sorted(cycles_a)
cb = sorted(cycles_b)
full_a = [c for c in ca if c[2] == 1]
full_b = [c for c in cb if c[2] == 1]
half_a = [c for c in ca if c[2] == 0.5]
half_b = [c for c in cb if c[2] == 0.5]
return {
"完整循环一致": full_a == full_b,
"完整循环数A": len(full_a),
"完整循环数B": len(full_b),
"半循环数A": len(half_a),
"半循环数B": len(half_b),
"半循环一致": half_a == half_b,
}
# 示例:含平台段与等值点的应力片段(单位 MPa)
stress = [10, 25, 40, 40, 40, 18, -5, -20, -20, 2, 30, 55, 55, 20, -10, -30, -12, 8, 8, 22, 5]
pv = extract_peaks_valleys(stress)
cyc_raw = rainflow(stress)
cyc_pv = rainflow(pv)
print("原始点数:", len(stress), " 峰谷点数:", len(pv))
print("峰谷序列:", pv)
print("原始序列循环数:", len(cyc_raw), " 峰谷序列循环数:", len(cyc_pv))
print("比对结果:", compare(cyc_raw, cyc_pv))
结果判读与边界差异分析
运行上述代码后,应重点观察三类现象:
- 完整循环集合:两路的完整循环(count=1)在按
(Sa, Sm)排序后应逐项相等。若不等,说明峰谷提取漏掉了真实转折点,或雨流核心对等值点处理不一致。 - 半循环数量:原始序列直接计数时,单调段上的中间采样点可能被残余栈当作半循环端点,导致半循环数量偏多;峰谷序列只保留转折点,半循环数量通常更少且更“干净”。这是预处理带来的形式差异,而非循环集合差异。
- 平台段与等值点:示例中
40,40,40与-20,-20、55,55、8,8是平台段。峰谷提取的d_prev==0跳过规则决定了平台段只保留段末点;若换成“保留段首点”或“保留段中点”,峰谷序列会不同,但完整循环的幅值/均值应保持不变——这正是校验要确认的鲁棒性。
校验通过判据:完整循环数相等、完整循环列表逐项相等、总损伤(可用 Miner 线性累积,Sa 取绝对值)在容差 1e-6 内一致。半循环数量允许不同,但需在报告中注明端点处理规则。
常见陷阱:若雨流核心在
r2 <= r1 and r2 <= r3 中使用严格小于,等值平台段可能无法闭合循环,导致两路结果都“少循环”但恰好相等,形成假通过。建议同时打印完整循环总数并与极值点数量关系交叉验证:完整循环数 ≤ (M-1)/2。校验记录表(可直接填写)
| 校验项 | 原始序列 | 峰谷序列 | 判定 |
|---|---|---|---|
| 输入点数 | 21 | 待填 | — |
| 完整循环数 | 待填 | 待填 | 应相等 |
| 半循环数 | 待填 | 待填 | 允许不同 |
| Sa 排序逐项比对 | 待填 | 应一致 | |
| Sm 排序逐项比对 | 待填 | 应一致 | |
| 总损伤(Miner) | 待填 | 待填 | 容差 1e-6 |
| 平台段归并规则 | 段末点 | 需注明 | |
把表中“待填”替换为实际运行输出,即完成本例的复现闭环。若完整循环比对出现不一致,优先检查峰谷提取是否把平台段端点误判为转折点,再检查雨流核心的等值比较符号。