2. 雨流计数结果与峰值谷值序列的对应校验

为什么需要做峰谷校验

雨流计数的输入本质上是“转折点序列”,而不是全部采样点。若直接对等间隔原始时程逐点计数,算法会把大量单调段上的中间采样点当作潜在极值参与比较,虽然标准实现通常能通过“只保留转折点”隐式规避,但不同实现(尤其是简化版)容易在平台段、等值点、端点处产生差异。因此本例的核心任务是:从同一段应力时程分别构造“原始序列”和“峰谷序列”,各自跑一遍雨流计数,逐循环比对幅值、均值与计数,确认预处理不改变循环集合,并定位边界处理带来的差异。

峰谷提取与两路计数流程

峰谷提取采用“相邻差分符号变化”判据:对采样序列 s[0..N-1],计算 d[i]=s[i]-s[i-1],当 d[i] 与 d[i-1] 异号(含零的归并规则)时,s[i-1] 即为转折点。首尾点始终保留为端点。得到峰谷序列后,两路输入分别送入同一套雨流计数核心,输出循环列表 (幅值 Sa, 均值 Sm, 计数)。

原始应力时程 s[i] N 个等间隔采样点 峰谷提取 差分符号变化判据 峰谷序列 p[k] M 个转折点(M ≤ N) 雨流计数 A 输入:原始序列 雨流计数 B 输入:峰谷序列 循环集 A (Sa, Sm, n) 列表 含半循环 循环集 B (Sa, Sm, n) 列表 含半循环 逐循环比对 Sa / Sm / 计数 差异仅出现在端点半循环 完整循环集合应完全一致 校验关注点 ① 完整循环数一致 ② 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
平台段归并规则段末点需注明

把表中“待填”替换为实际运行输出,即完成本例的复现闭环。若完整循环比对出现不一致,优先检查峰谷提取是否把平台段端点误判为转折点,再检查雨流核心的等值比较符号。