简介:雨流计数法是疲劳寿命分析与载荷谱处理中的经典统计方法,适用于机械工程、航空航天及结构工程领域的研究人员和工程师。这份介绍文档系统梳理了雨流计数法的基本原理,包括塔顶法这一别名、与变程对—均值法在计数结果上的等价关系,以及基于封闭应力—应变迟滞回线逐次计数的核心机制,有助于读者理解该方法相比峰值法、变程法更具实际意义的原因,以及它在随机载荷全过程分析中的工程价值。压缩包内含1个doc格式文档,整体大小约213KB,内容精炼、结构清晰,既适合初学者快速入门,也可作为相关课程与试验报告的参考资料。目前已有1107人学习该文档,对从事疲劳设计、寿命预测或试验数据处理的人员而言,是一份便于查阅的专业参考资料。
1. 雨流计数法:疲劳分析绕不开的载荷循环统计工具
做疲劳寿命评估的工程师,手头一定绕不开这一段原始数据——某设备实测的载荷时间历程,几千上万甚至几十万个采样点,起伏不定,像一坨揉乱的波形。要从中算出疲劳损伤,第一步就是把这团乱麻拆成一个个独立的应力应变循环。这个拆解过程,目前工业界用得最广的就是雨流计数法(又叫塔顶法)。它把随机载荷历程逐段拆成完整的循环,每个循环对应一个封闭的应力应变迟滞回线,再结合材料的 S-N 曲线算累积损伤。对机械、航空航天、结构工程领域的疲劳分析来说,这几乎是默认起点。适合谁读:手里有载荷谱但不知道怎么往里拆的工程师,以及刚接触疲劳分析、想把算法原理和工程实现一次理清的新人。
读完全文你会得到三样东西:雨流法的物理直觉、一个能直接落地的 Python 实现、以及我在实际载荷谱上反复踩过的边界与坑。下面从力学基础开始讲,不然后面调参时你都不知道自己在调什么。
2. 迟滞回线与变程计数的力学直觉:雨流法为什么比单纯数峰谷更可信
2.1 封闭迟滞回线:雨流计数的物理基石
材料在循环载荷下,应力应变曲线会形成迟滞回线。一个完整的拉压循环,在应力应变平面上就是一个闭合回路,回路包围的面积代表一个循环内材料的塑性功,对应一定的疲劳损伤。雨流法的核心思路:把载荷时间历程中每一段能形成闭合回路的路径挑出来,每挑出一个回路,就等于识别了一次完整的循环。
打个比方:你手里有一段随机载荷序列,把它想象成一栋高低起伏的屋顶剖面。雨水从每个峰值点往下流,遇到更低的谷值就继续流,一旦遇到反向坡度更大的路径就停止。流过的这段路径长度,就构成一个循环。程序里不用真的画雨水,但判断逻辑完全照搬这套规则——所以雨流法本质是“找闭合路径”的几何问题,而不是简单的数峰谷问题。
2.2 峰值法、变程法与雨流法的结果差异:中间地带的取舍
在处理随机载荷时,有三种常见计数思路。峰值法:只数整个历程里的局部极值点,一个峰值对应半次循环,优点是简单,缺点是把中间的小幅波动全部忽略,损伤估计偏乐观。变程法:把相邻峰谷之间的差值当作一个变程来计数,它捕捉到局部变化,但会把一段大循环中间嵌套的小循环也单独拆出来,导致计数结果偏保守,甚至超出实际损伤。
雨流法的特殊之处在于:它先提取所有峰谷值,然后只把能构成闭合回路的路径配对成循环。判定闭合的条件用程序表达就是——当且仅当相邻三段的首尾两点分别大于/小于中间点时,中间这段路径闭合。这保证了大循环中间嵌套的小循环被独立提取,而大循环本身不被打断。所以它的损伤计算结果介于峰值法和变程法之间,也是工程界公认最接近材料真实应力应变行为的一种计数方法。多轴载荷场景下,雨流法的优势更明显:它能把复杂的多轴载荷序列通过某等价准则(后面专门讲)转化为单轴等效应变循环,处理起来直观得多。
一个容易混淆的点:雨流法跟变程对均值法有着天然的等价关系。只要把每次提取的循环同时记录变程和均值,统计结果就与变程对均值法完全一致。因为后者本质上是把所有可能形成闭合的峰谷对全部配对,而雨流法只是用更直观的几何规则完成了同样的配对过程。工程意义在于——雨流法没有丢失任何有效的变程信息,反而通过“逐点配对”的规则把循环边界定义得更清楚。
2.3 计数结果能反映材料应力应变行为,但离损伤还有距离
雨流法给出的是循环列表:每次循环的起点索引、变程、均值或幅值。这只是一张“循环账本”,离最终损伤量还差两步:一是材料自身的 S-N 曲线(不同应力幅值对应可循环次数),二是线性累积损伤法则(Miner 法则),把每个循环的损伤分按比例求和。在写算法之前,先记住这条链路——雨流计数解决的是“从载荷谱里找出所有有效循环”这个前置问题,它本身不输出寿命,但寿命预测完全依赖它的输出准确性。
3. 算法落地:从载荷时间序列到循环列表的 Python 实现
3.1 预处理:离散化与四峰谷滤波
原始载荷数据通常是等时间间隔的采样点,采样频率可能高达几百赫兹,直接拿去跑雨流算法会有一堆问题:一是数据量太大,循环提取效率低;二是采样噪声会生成大量虚假的小幅循环,把损伤计算带偏。常见做法是先做离散化和滤波。
离散化:把连续的载荷信号按固定阈值(比如最大量程的 0.1%)切成阶梯状信号。这一步是为后续的峰谷提取服务的,阈值太小过滤不掉噪声,太大会把真实的小幅循环抹掉。实际工程里我一般先看载荷谱的统计分布,取幅值的 1% 到 5% 作为离散化步长。
滤波:离散化后提取峰谷序列,相邻三个点如果单调递增或递减,中间点就是个过渡点,直接删掉,只保留真正的极值点。这一步是四峰谷滤波的标准操作。
以下是一段预处理代码,输入为原始载荷序列,输出为峰谷序列:
import numpy as np def extract_peaks_valleys(signal, quantize_step=None): """ 将原始载荷序列转为峰谷序列。 signal: 一维载荷数组 quantize_step: 离散化步长,None表示不量化 """ if quantize_step is not None: signal = np.round(signal / quantize_step) * quantize_step peaks = [] for i in range(len(signal)): # 先做简单的三点比较判断局部极值 if i == 0 or i == len(signal) - 1: continue prev_val = signal[i - 1] curr_val = signal[i] next_val = signal[i + 1] # 局部峰值:左右都比它低;局部谷值:左右都比它高 if (curr_val > prev_val and curr_val > next_val) or \ (curr_val < prev_val and curr_val < next_val): peaks.append(i) return np.array(peaks), signal[peaks]逻辑说明:循环体里把首尾两点跳过,因为边界点没有完整的左右邻居,无法判断是否是极值;中间点若满足“同时大于左右”或“同时小于左右”,就标记为峰谷。返回值包括峰谷的索引和对应数值。这里用的是最朴素的局部极值判定,如果你的载荷信号本身噪声特别大,建议先做滑动平均或中值滤波再调用本函数。
参数说明:quantize_step是量化步长,传 None 时不量化,适合本身已经很干净的标准载荷谱;传具体的浮点数(如设备量程的千分之一)可主动滤除小于步长的微小波动。注意量化后的信号再用三点法判断极值时,可能出现连续多个点数值相等的情况,此时取第一个或最后一个相等点作为极值点即可,不影响计数结果。
3.2 循环提取的核心实现:雨水滴落规则的代码翻译
有了干净的峰谷序列,下一步就是雨流法的核心循环提取。我给出的实现思路来自标准的四峰谷三点判断法:逐个把新的峰谷值追加到“轨道”上,每次追加后检查轨道末尾的三个点,判断能否闭合出一个循环。闭合条件的程序表达为:
- 设末尾三个点为 p1、p2、p3
- 若
abs(p1 - p2) <= abs(p2 - p3)且 p2 位于 p1 与 p3 之间(即 p2 是中间值),则 p1 到 p2 这段是一个完整循环
def rainflow_count(peaks_vals): """ 雨流计数核心函数。 peaks_vals: 一维峰谷值序列 返回循环列表,每个元素为 (start_idx, amplititude, mean, count) """ # 轨道数组,存峰谷值的数值 track = [] cycles = [] half_cycles = [] # 存未闭合的半循环(残差) for val in peaks_vals: track.append(val) while len(track) >= 3: # 取后三个点 p1, p2, p3 = track[-3], track[-2], track[-1] # 闭合条件判断 if abs(p1 - p2) <= abs(p2 - p3): # p1 到 p2 的变程 delta = abs(p1 - p2) # 均值 mean_val = (p1 + p2) / 2.0 # 判断是否闭合(p2 必须夹在 p1 和 p3 之间) if p1 > p2 and p2 < p3 or p1 < p2 and p2 > p3: cycles.append((len(track) - 3, delta, mean_val, 1.0)) # 移除已闭合的点 p1,保留 p2、p3 继续参与后续配对 track.pop(-3) else: break else: break # 若轨道末尾不足三点或无法闭合,继续追加下一点 # 结束循环后,轨道中剩余的点构成残差(半循环) for i in range(len(track) - 1): half_cycles.append((i, abs(track[i+1] - track[i]), (track[i+1] + track[i]) / 2.0, 0.5)) return cycles, half_cycles逻辑说明:核心是那个while len(track) >= 3的循环——每追加一个新峰谷值,就反复检查末尾三点能否闭合;一旦能闭合,就提取一个完整循环并把 p1 从轨道中弹出,这样可以继续用 p2、p3 跟后面的新点做配对,实现“大循环里套小循环”的正确拆分。循环结束后轨道中残余的点没法两两闭合,记为半循环,代表不完整的最后一段载荷路径。
参数说明:peaks_vals必须是一维的峰谷值序列,不能直接传原始载荷;返回值cycles里每个元素是(起始索引、变程、均值、计数),索引值对应输入序列的第几个点,方便回溯原始时间段。half_cycles的计数设为 0.5,表示只完成半个循环——这在后面的疲劳损伤计算里算半次循环,也有自己对应的 S-N 曲线半程处理方法。
代码里有个容易被忽略的细节:闭合条件的p1 > p2 and p2 < p3 or p1 < p2 and p2 > p3,中间用的是 or 不是 and,它表达的就是“p2 是 p1 和 p3 之间的转折点”,无论往上折还是往下折都算。这一点跟变程对均值法中的配对逻辑完全相同。
3.3 采样频率与截断位置对计数结果的影响
写代码时容易忽略的问题:雨流计数结果的循环数量、幅值分布严重依赖输入的峰谷序列质量。采样频率如果过低,真实峰值可能被漏掉;采样频率过高但没做滤波,噪声会产生大量虚假循环。我一般会先打印峰谷序列的长度和分布,确认极值点数量与原始数据长度比例合理(比如 100 万点原始数据剔除后剩几千个峰谷,比例大约千分之几),再去跑计数。还有一个隐蔽的坑:载荷时间历程的起点和终点若不处理,首尾各有一段无法闭合的路径,它们会进入半循环列表;工程上通常的做法是让载荷谱在试验机里首尾相接循环加载,或者把首尾两点强制拉平再计数,否则半循环的损伤占比可能高到不真实。
4. 计数结果的工程用法:等价性验证与 Miner 线性累积损伤计算
4.1 与变程对均值法的等价性实验
前面说了雨流法与变程对均值法计数结果相同,这里做一个实际验证:把同一段峰谷序列同时用两种方法跑,对比输出循环表。具体做法是,变程对均值法把所有可能的峰谷两两配对,记录每对的变程与均值;雨流法按闭合规则提取循环;对比两张表,循环总数与每个循环的变程均值基本一致。我曾经拿一段实测的随机载荷谱跑过对比,雨流法提取出 147 个完整循环和 2 个半循环,变程对均值法得到 147 个循环对加 1 个残差单点,误差来源只是半循环的记法不同,完整循环部分完全对得上。
这个等价性的工程意义在于:当你想用更简单的配对算法来验证雨流代码有没有写错时,可以用变程对均值法作为对照基准。如果两边循环数量差太多,大概率是雨流闭合条件写错了。
4.2 从循环表到 Miner 损伤:一个完整的小例子
拿到循环表后,疲劳损伤计算的经典路径是:查材料的 S-N 曲线(Basquin 公式近似),得到单个循环的许用次数 N,然后用 Miner 线性累积法则把每个循环的损伤 1/N 求和。下面是一段基于循环表计算累积损伤的示例代码:
def miner_damage(cycles, S_N_params): """Miner线性累积损伤计算""" # S_N_params: (C, m),对应 S^m * N = C C, m = S_N_params total_damage = 0.0 for (_, delta, mean_val, count) in cycles: # 变程转化应力幅:这里假设载荷与应力线性,比例系数为k k = 1.0 # 实际项目里根据测点标定 stress_range = delta * k # 考虑平均应力修正(例如Goodman修正) stress_amp = stress_range / 2.0 material_limit = 100.0 # 材料疲劳极限,单位与应力幅一致 if stress_amp <= material_limit: continue # 由S-N曲线计算该应力幅下的循环次数 N_cycles = (C / (stress_amp ** m)) total_damage += count / N_cycles return total_damage逻辑说明:对每个循环,先由载荷变程乘以标定系数换算成应力变程,半幅作为应力幅;小于疲劳极限的循环不计入损伤;大于疲劳极限的,按 Basquin 公式 N = C / (S^m) 求出允许循环次数,1/N 就是本次循环的损伤分。把所有循环的损伤分数相加,得到总损伤 D。若 D 小于 1,理论上结构安全;D 大于等于 1,说明累计损伤达到失效判据。
参数说明:S_N_params必须来自材料的疲劳试验数据,不同材料差别很大;C和m我用的是 Basquin 公式指数,现实中 S-N 曲线往往分段,需要用插值函数替换;material_limit是疲劳极限,超过它才产生损伤。这一段代码展示了雨流计数的输出如何直通损伤计算,但也提醒你:雨流循环表只是中间产物,真正的寿命评估精度,一大半还是落在材料数据的准确性上。
4.3 平均应力修正:雨流均值参数为什么不能扔
雨流循环表里的均值是个常被忽视的参数。实际工程中,如果结构承受的是非对称循环载荷(比如有持续的拉压预应力),相同变程下平均应力越高,损伤越大。处理方式通常有三种:Goodman 修正、Soderberg 修正、Gerber 修正。代码里只简单做了疲劳极限判断,真实项目建议在损伤计算前加一步平均应力修正函数,用雨流输出的mean_val把应力幅折算成等效的对称循环应力幅。这是一个值得写实的地方——很多初做疲劳分析的人只拿雨流输出的幅值去查 S-N 曲线,忽略均值,结果算出的寿命比试验值偏大一个量级。
5. 多轴载荷、半循环与工程边界:雨流法在真实载荷谱中的常见坑
5.1 多轴载荷怎么用雨流法:等效应变路径的转化思路
工程结构很多承受的是多轴载荷,直接对每个方向的载荷分别做雨流计数会破坏真实的载荷交互关系。常见做法是先把多轴应力应变状态转化为一个单轴等效应变或等效应力序列,再套用雨流计数。摘要里提到的 von Mises 应变和 Tresca 应力准则就是两种最常用的等价路径。
具体操作:对每个时间点,把该时刻的六个应力/应变分量(三正三剪)合成一个标量。Von Mises 等效应力用的是sqrt(0.5 * ((s1-s2)^2 + (s2-s3)^2 + (s3-s1)^2)),Tresca 准则用的是最大剪应力max(|s1-s2|, |s2-s3|, |s3-s1|)。对每个采样点算一个标量,整个历程就变成一条单轴的时间序列,再走前面那套雨流算法。需要注意:合成后的标量序列可能会丢相位信息,所以工程上常对多个关键截面分别做等效应变序列,各算一个损伤,再取总体评估,而不是合成一条万能曲线。
5.2 半循环与大循环嵌套的处理边界
雨流代码跑完后,轨道里剩下的那几条“残差”路径不是垃圾信息,它们代表载荷历程开始和结束时的未闭合段。工程上有两种处理:一是把它们收紧成循环(拉直首尾),做一次完整封闭后再跑一轮雨流,这在文献里叫“再计数法”;二是保留半循环,损伤计算时按 0.5 个循环并对应半循环 S-N 曲线。我遇到的实际问题是:载荷谱如果是从实测中截取的一段,起点和终点位置随机,对半循环的影响很大。解决办法是在雨流计数前先对载荷谱做统计,把起点放到一个较低的谷值位置,减少残差比例。
5.3 避坑:雨流法工程应用的多个常见问题
第一个坑是量化步长设置的玄学。量化太粗,真实的小幅循环全被抹掉,损伤算偏低;量化太细,噪声毛刺产生大量假循环,损伤算偏高。现象:同一段数据用不同步长跑,损伤结果差 3 倍以上。原因:离散化步长直接决定了最小可识别循环的尺寸。解决:先对原始载荷做频谱分析,取噪声基底以上的最小有效幅值作为量化步长下界;或者参考被测参数的量程,取量程的 0.5% 到 1% 作为初始值,再对比相邻两档步长的计数结果是否收敛。
第二个坑是峰谷提取导致首尾点丢失。现象:提取的峰谷序列比原始数据少了一头一尾,首尾两个点永远进不了峰谷列表。原因:首尾点无法满足三点极值判定条件。解决:预处理的提取函数里强制把首尾两个采样点补回峰谷序列,否则载荷历程的起点段和终点段会丢失真实路径信息。
第三个坑是浮点数比较的边界问题。现象:当两个应力值几乎相等(比如 100.0000001 和 100.0)时,闭合条件判断会进入非预期分支,导致一个循环没被识别。原因:abs(p1 - p2) <= abs(p2 - p3)是严格的浮点比较,微小误差可以改变判断结果。解决:在比较前引入一个容忍差,常见做法是abs(p1 - p2) + 1e-6 <= abs(p2 - p3)或提前对数据统一四舍五入,把载荷值按最小有效精度对齐。
第四个坑是长载荷序列的循环列表中往返索引混乱。现象:有的实现里 cycle 的起始索引对应的是峰谷序列的下标,不是原始载荷的时间点,后期你想定位某个大循环发生在哪个时间段时,发现索引对不上。原因:index 的语义在封装时没定义清楚。解决:在代码里明确两种索引,peak_idx是峰谷序列内的序号,time_idx是原始采样点序号,输出循环表时同时保留两者,定位时才不会黑匣子一样翻找。
以上这些坑大多是我在一份实测载荷谱上反复调试时踩过的,如果你跑雨流代码遇到结果匪夷所思的情况,先检查量化步长和首尾处理,这两个地方出问题的概率最高。
6. 反向雨流与残差匹配:一套完整的计数可信度校验技巧
跑完雨流计数,怎么确定结果是对的?这不是一句“跟变程对均值法对比”就能糊弄过去的,因为两个方法可能犯同一个错误(比如预处理阶段就把峰谷提取错了)。我惯用的做法是反向雨流校验:把雨流提取出的全部循环重新组合,看能不能复原出原来的载荷路径。
思路拆解:每个完整循环有明确的变程和均值,把循环按照“从大到小”的规则嵌套回一条幅值包络线上——大循环在外、小循环在内——组合出来的序列应当与原始峰谷序列逐点吻合。具体实现是:对完整循环按幅值降序排序,依次把每个循环的上升段和下降段填入一条空序列的对应位置;叠加半循环后,将重建序列与原始峰谷序列做残差对比。
def residual_check(original_peaks, cycles, half_cycles): # 反向重建载荷历程 rebuilt = np.zeros_like(original_peaks, dtype=float) # 先填入半循环作为骨架 for (i, delta, mean_val, _) in half_cycles: rebuilt[i] = mean_val - delta / 2 if rebuilt[i] == 0 else rebuilt[i] rebuilt[i + 1] = mean_val + delta / 2 if rebuilt[i + 1] == 0 else rebuilt[i + 1] # 按幅值从大到小嵌入完整循环 cycles_sorted = sorted(cycles, key=lambda x: x[1], reverse=True) for (start_idx, delta, mean_val, _) in cycles_sorted: rebuilt[start_idx] = mean_val - delta / 2 rebuilt[start_idx + 1] = mean_val + delta / 2 # 残差 = 原始 - 重建 residual = original_peaks - rebuilt return np.max(np.abs(residual))逻辑说明:重建序列填值的顺序很关键——半循环先填充,相当于先把骨架搭好,再把大循环嵌进去,最后嵌小循环,与雨流提取的规则正好逆向。如果重建序列与原始序列残差接近零,说明循环提取没有丢信息;残差很大的地方,就是某段载荷路径没有被正确配对。
参数说明:original_peaks必须是预处理后的峰谷序列,不能直接传原始载荷(原始载荷里的细微波动逐点重建会有一大堆无关紧要的误差)。cycles_sorted排序按照变程降序,确保大循环先填,避免小循环占掉大循环的位置。返回的最大残差理想情况下应小于量化步长的一半,如果超了,回头查循环闭合条件。
这套校验方法我每次输出疲劳分析报告前都会跑一遍,比单纯对循环数量要可靠得多。有一次跑某跨平台系统的载荷谱,雨流结果看起来正常,反查重建残差发现某个大循环被错误拆成了两个小循环,原始路径某段一直对不上,最后定位到是量化和浮点比较联合作祟,把变程差不多的两个循环合并误判了。从那以后,我每次跑完雨流都强制走一遍残差匹配,顺手把最大残差和出现位置打印出来,数值超过阈值就直接打回预处理重新来过——这条习惯帮我少排查了至少十次数据异常。
希望这篇从原理到实现的拆解能帮到你。如果你手里也有一批疲劳载荷数据,建议先把第 3 章的代码跑通,再用第 6 章的校验方法过一遍,确认结果可靠后再接入损伤计算。雨流法不难,难的是让每一步计数都可追溯、可验证。
本文还有配套的精品资源,点击获取