typhoon-rainflow 0.1.8__cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- typhoon/__init__.py +5 -0
- typhoon/__init__.pyi +3 -0
- typhoon/helper.py +102 -0
- typhoon/py.typed +0 -0
- typhoon/typhoon.cpython-314-aarch64-linux-gnu.so +0 -0
- typhoon/typhoon.pyi +33 -0
- typhoon/woehler.py +278 -0
- typhoon_rainflow-0.1.8.dist-info/METADATA +204 -0
- typhoon_rainflow-0.1.8.dist-info/RECORD +10 -0
- typhoon_rainflow-0.1.8.dist-info/WHEEL +5 -0
typhoon/__init__.py
ADDED
typhoon/__init__.pyi
ADDED
typhoon/helper.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections import Counter
|
|
4
|
+
from typing import Iterable, Literal, Mapping, Tuple, cast
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
import pandas as pd
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
CycleKey = Tuple[float, float]
|
|
11
|
+
CycleCounter = Counter
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def merge_cycle_counters(counters: Iterable[Mapping[CycleKey, float]]) -> CycleCounter:
|
|
15
|
+
"""Merge multiple rainflow() cycle dicts/Counter objects using Counter.
|
|
16
|
+
|
|
17
|
+
Each input mapping should be like the first return value from ``rainflow``:
|
|
18
|
+
``{(s_lower, s_upper): count}``.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
total: CycleCounter = Counter()
|
|
22
|
+
for c in counters:
|
|
23
|
+
total.update(c)
|
|
24
|
+
return total
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def add_residual_half_cycles(
|
|
28
|
+
counter: Mapping[CycleKey, float],
|
|
29
|
+
residual_peaks: np.ndarray,
|
|
30
|
+
) -> CycleCounter:
|
|
31
|
+
"""Add half-cycles from residual waveform peaks to an existing Counter.
|
|
32
|
+
|
|
33
|
+
The residual peaks array is expected to be the second return value from
|
|
34
|
+
``rainflow``. It represents half-cycles between adjacent peaks.
|
|
35
|
+
|
|
36
|
+
Each half-cycle contributes 0.5 to the count for its (from, to) key.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
result: CycleCounter = Counter(counter)
|
|
40
|
+
|
|
41
|
+
if residual_peaks.size < 2:
|
|
42
|
+
return result
|
|
43
|
+
|
|
44
|
+
for i in range(len(residual_peaks) - 1):
|
|
45
|
+
f = float(residual_peaks[i])
|
|
46
|
+
t = float(residual_peaks[i + 1])
|
|
47
|
+
key: CycleKey = (f, t)
|
|
48
|
+
result[key] += 0.5 # type: ignore
|
|
49
|
+
|
|
50
|
+
return result
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def counter_to_full_interval_df(
|
|
54
|
+
counter: Mapping[CycleKey, float],
|
|
55
|
+
bin_size: float = 0.1,
|
|
56
|
+
closed: Literal["left", "right"] = "right",
|
|
57
|
+
round_decimals: int = 12,
|
|
58
|
+
) -> pd.DataFrame:
|
|
59
|
+
"""Convert a (from, to): count mapping to a full 2D interval DataFrame.
|
|
60
|
+
|
|
61
|
+
The returned DataFrame has a MultiIndex of (from_interval, to_interval)
|
|
62
|
+
covering the entire range, with zero counts where no cycles exist.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
if not counter:
|
|
66
|
+
# Return empty but well-formed DataFrame
|
|
67
|
+
return pd.DataFrame(
|
|
68
|
+
[],
|
|
69
|
+
index=pd.MultiIndex.from_arrays(
|
|
70
|
+
[pd.IntervalIndex([], name="from"), pd.IntervalIndex([], name="to")]
|
|
71
|
+
),
|
|
72
|
+
columns=["value"],
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
half = bin_size / 2.0
|
|
76
|
+
|
|
77
|
+
from_vals = sorted({f for (f, _) in counter.keys()})
|
|
78
|
+
to_vals = sorted({t for (_, t) in counter.keys()})
|
|
79
|
+
|
|
80
|
+
min_val = min(min(from_vals), min(to_vals))
|
|
81
|
+
max_val = max(max(from_vals), max(to_vals))
|
|
82
|
+
|
|
83
|
+
centers = np.arange(min_val, max_val + bin_size / 2.0, bin_size)
|
|
84
|
+
|
|
85
|
+
def make_interval(c: float) -> pd.Interval:
|
|
86
|
+
left = round(c - half, round_decimals)
|
|
87
|
+
right = round(c + half, round_decimals)
|
|
88
|
+
return pd.Interval(left, right, closed=closed)
|
|
89
|
+
|
|
90
|
+
from_bins = pd.IntervalIndex([make_interval(cast(float, c)) for c in centers], name="from")
|
|
91
|
+
to_bins = pd.IntervalIndex([make_interval(cast(float, c)) for c in centers], name="to")
|
|
92
|
+
|
|
93
|
+
full_idx = pd.MultiIndex.from_product([from_bins, to_bins], names=["from", "to"])
|
|
94
|
+
|
|
95
|
+
data = {
|
|
96
|
+
(make_interval(f), make_interval(t)): float(v) for (f, t), v in counter.items()
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
s = pd.Series(data, name="value", dtype="float64")
|
|
100
|
+
s = s.reindex(full_idx, fill_value=0.0)
|
|
101
|
+
|
|
102
|
+
return s.to_frame()
|
typhoon/py.typed
ADDED
|
File without changes
|
|
Binary file
|
typhoon/typhoon.pyi
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from collections.abc import Mapping
|
|
2
|
+
from typing import Any, TypeAlias
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
from numpy.typing import NDArray
|
|
6
|
+
|
|
7
|
+
Array1D: TypeAlias = NDArray[np.float32]
|
|
8
|
+
|
|
9
|
+
def init_tracing() -> None: ...
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def rainflow(
|
|
13
|
+
waveform: Array1D,
|
|
14
|
+
last_peaks: Array1D | None = ...,
|
|
15
|
+
bin_size: float = ...,
|
|
16
|
+
threshold: float | None = ...,
|
|
17
|
+
min_chunk_size: int = ...,
|
|
18
|
+
) -> tuple[dict[tuple[float, float], int], Array1D]:
|
|
19
|
+
...
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def goodman_transform(
|
|
23
|
+
cycles: Mapping[tuple[float, float], float] | Mapping[tuple[float, float], int],
|
|
24
|
+
m: float,
|
|
25
|
+
m2: float | None = ...,
|
|
26
|
+
) -> dict[float, float]:
|
|
27
|
+
...
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def summed_histogram(
|
|
31
|
+
hist: Mapping[float, float] | Mapping[float, int],
|
|
32
|
+
) -> list[tuple[float, float]]:
|
|
33
|
+
...
|
typhoon/woehler.py
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""Helpers for evaluating Woehler (S-N) curves.
|
|
4
|
+
|
|
5
|
+
The functions in this module are a NumPy-based translation of the TypeScript
|
|
6
|
+
implementation used in the UI project. They provide utilities for computing
|
|
7
|
+
load amplitudes for a given number of cycles and for generating a convenient
|
|
8
|
+
logarithmic cycle axis.
|
|
9
|
+
|
|
10
|
+
The central entry points are:
|
|
11
|
+
|
|
12
|
+
* :class:`WoehlerCurveParams` – container for curve parameters.
|
|
13
|
+
* :func:`woehler_loads` – probability-dependent Woehler curve.
|
|
14
|
+
* :func:`woehler_loads_basic` – Woehler curve without probability/scattering.
|
|
15
|
+
* :func:`woehler_log_space` – helper to create a log-spaced cycle axis.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from enum import Enum
|
|
20
|
+
from math import log10
|
|
21
|
+
from typing import Iterable
|
|
22
|
+
|
|
23
|
+
import numpy as np
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class MinerType(str, Enum):
|
|
27
|
+
NONE = "none"
|
|
28
|
+
ORIGINAL = "original"
|
|
29
|
+
ELEMENTARY = "elementary"
|
|
30
|
+
HAIBACH = "haibach"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class WoehlerCurveParams:
|
|
35
|
+
sd: float
|
|
36
|
+
nd: float
|
|
37
|
+
k1: float
|
|
38
|
+
k2: float | None = None
|
|
39
|
+
ts: float | None = None
|
|
40
|
+
tn: float | None = None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
_DEFAULT_FAILURE_PROBABILITY = 0.5
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _norm_ppf(p: float) -> float:
|
|
47
|
+
"""Approximate the inverse CDF (ppf) of the standard normal distribution.
|
|
48
|
+
|
|
49
|
+
Implementation based on Peter J. Acklam's algorithm. This avoids adding a
|
|
50
|
+
dependency on SciPy while being sufficiently accurate for engineering
|
|
51
|
+
purposes.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
if not (0.0 < p < 1.0):
|
|
55
|
+
raise ValueError("p must be in (0, 1)")
|
|
56
|
+
|
|
57
|
+
a = [
|
|
58
|
+
-3.969683028665376e01,
|
|
59
|
+
2.209460984245205e02,
|
|
60
|
+
-2.759285104469687e02,
|
|
61
|
+
1.383577518672690e02,
|
|
62
|
+
-3.066479806614716e01,
|
|
63
|
+
2.506628277459239e00,
|
|
64
|
+
]
|
|
65
|
+
b = [
|
|
66
|
+
-5.447609879822406e01,
|
|
67
|
+
1.615858368580409e02,
|
|
68
|
+
-1.556989798598866e02,
|
|
69
|
+
6.680131188771972e01,
|
|
70
|
+
-1.328068155288572e01,
|
|
71
|
+
]
|
|
72
|
+
c = [
|
|
73
|
+
-7.784894002430293e-03,
|
|
74
|
+
-3.223964580411365e-01,
|
|
75
|
+
-2.400758277161838e00,
|
|
76
|
+
-2.549732539343734e00,
|
|
77
|
+
4.374664141464968e00,
|
|
78
|
+
2.938163982698783e00,
|
|
79
|
+
]
|
|
80
|
+
d = [
|
|
81
|
+
7.784695709041462e-03,
|
|
82
|
+
3.224671290700398e-01,
|
|
83
|
+
2.445134137142996e00,
|
|
84
|
+
3.754408661907416e00,
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
plow = 0.02425
|
|
88
|
+
phigh = 1 - plow
|
|
89
|
+
|
|
90
|
+
if p < plow:
|
|
91
|
+
q = np.sqrt(-2 * np.log(p))
|
|
92
|
+
return (((((c[0] * q + c[1]) * q + c[2]) * q + c[3]) * q + c[4]) * q + c[5]) / (
|
|
93
|
+
(((d[0] * q + d[1]) * q + d[2]) * q + d[3]) * q + 1.0
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
if p > phigh:
|
|
97
|
+
q = np.sqrt(-2 * np.log(1 - p))
|
|
98
|
+
return -(
|
|
99
|
+
(((((c[0] * q + c[1]) * q + c[2]) * q + c[3]) * q + c[4]) * q + c[5])
|
|
100
|
+
/ ((((d[0] * q + d[1]) * q + d[2]) * q + d[3]) * q + 1.0)
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
q = p - 0.5
|
|
104
|
+
r = q * q
|
|
105
|
+
return (
|
|
106
|
+
(((((a[0] * r + a[1]) * r + a[2]) * r + a[3]) * r + a[4]) * r + a[5]) * q
|
|
107
|
+
) / (((((b[0] * r + b[1]) * r + b[2]) * r + b[3]) * r + b[4]) * r + 1.0)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
_NATIVE_PPF = 0.0 # _norm_ppf(_DEFAULT_FAILURE_PROBABILITY)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _scattering_range_to_std(t: float) -> float:
|
|
114
|
+
return 0.39015207303618954 * log10(t)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _derive_k2(params: WoehlerCurveParams, miner: MinerType) -> float:
|
|
118
|
+
if miner is MinerType.ORIGINAL:
|
|
119
|
+
return float("inf")
|
|
120
|
+
if miner is MinerType.ELEMENTARY:
|
|
121
|
+
return params.k1
|
|
122
|
+
if miner is MinerType.HAIBACH:
|
|
123
|
+
return 2.0 * params.k1 - 1.0
|
|
124
|
+
return float("inf")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _derive_ts(params: WoehlerCurveParams) -> float:
|
|
128
|
+
if params.ts is not None:
|
|
129
|
+
return params.ts
|
|
130
|
+
if params.tn is not None:
|
|
131
|
+
return float(params.tn) ** (1.0 / params.k1)
|
|
132
|
+
return 1.0
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _derive_tn(params: WoehlerCurveParams) -> float:
|
|
136
|
+
if params.tn is not None:
|
|
137
|
+
return params.tn
|
|
138
|
+
if params.ts is not None:
|
|
139
|
+
return float(params.ts) ** params.k1
|
|
140
|
+
return 1.0
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _make_k(
|
|
144
|
+
src: float, ref: float, params: WoehlerCurveParams, miner: MinerType
|
|
145
|
+
) -> float:
|
|
146
|
+
k2_derived = _derive_k2(params, miner)
|
|
147
|
+
if src < ref:
|
|
148
|
+
return k2_derived
|
|
149
|
+
return params.k1
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def woehler_loads_basic(
|
|
153
|
+
cycles: Iterable[float] | np.ndarray,
|
|
154
|
+
params: WoehlerCurveParams,
|
|
155
|
+
miner: MinerType = MinerType.NONE,
|
|
156
|
+
) -> np.ndarray:
|
|
157
|
+
"""Return Woehler curve loads for given cycle counts.
|
|
158
|
+
|
|
159
|
+
This variant corresponds to the "native" Woehler curve and does not apply
|
|
160
|
+
any probability or scattering transformation. It still honours the
|
|
161
|
+
``miner`` setting and thus the possible change of slope between the
|
|
162
|
+
finite-life and the endurance region.
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
cyc = np.asarray(list(cycles), dtype=float)
|
|
166
|
+
if cyc.ndim != 1:
|
|
167
|
+
raise ValueError("cycles must be 1D")
|
|
168
|
+
|
|
169
|
+
sd = params.sd
|
|
170
|
+
nd_transformed = params.nd
|
|
171
|
+
|
|
172
|
+
sd_transformed = sd
|
|
173
|
+
|
|
174
|
+
ref = -nd_transformed
|
|
175
|
+
k_values = np.array(
|
|
176
|
+
[_make_k(-float(c), ref, params, miner) for c in cyc], dtype=float
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
loads = np.empty_like(cyc, dtype=float)
|
|
180
|
+
mask_finite = np.isfinite(k_values)
|
|
181
|
+
loads[mask_finite] = sd_transformed * (cyc[mask_finite] / nd_transformed) ** (
|
|
182
|
+
-1.0 / k_values[mask_finite]
|
|
183
|
+
)
|
|
184
|
+
loads[~mask_finite] = sd_transformed
|
|
185
|
+
|
|
186
|
+
return loads
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def woehler_loads(
|
|
190
|
+
cycles: Iterable[float] | np.ndarray,
|
|
191
|
+
params: WoehlerCurveParams,
|
|
192
|
+
miner: MinerType = MinerType.NONE,
|
|
193
|
+
failure_probability: float = _DEFAULT_FAILURE_PROBABILITY,
|
|
194
|
+
) -> np.ndarray:
|
|
195
|
+
"""Return Woehler curve loads for given cycle counts.
|
|
196
|
+
|
|
197
|
+
Parameters
|
|
198
|
+
----------
|
|
199
|
+
cycles:
|
|
200
|
+
Iterable of cycle counts (e.g. values from :func:`woehler_log_space`).
|
|
201
|
+
params:
|
|
202
|
+
Woehler curve parameters such as fatigue strength and slopes.
|
|
203
|
+
miner:
|
|
204
|
+
Miner damage rule variant determining the second slope ``k2``.
|
|
205
|
+
failure_probability:
|
|
206
|
+
Target failure probability :math:`P_f` in the interval ``(0, 1)``.
|
|
207
|
+
|
|
208
|
+
Notes
|
|
209
|
+
-----
|
|
210
|
+
The implementation mirrors the TypeScript logic from the UI and is
|
|
211
|
+
vectorised for NumPy arrays. It uses an internal approximation of the
|
|
212
|
+
standard normal inverse CDF and applies the same transformations to
|
|
213
|
+
``sd`` and ``nd`` that are used in the UI.
|
|
214
|
+
"""
|
|
215
|
+
|
|
216
|
+
if not (0.0 < failure_probability < 1.0):
|
|
217
|
+
raise ValueError("failure_probability must be in (0, 1)")
|
|
218
|
+
|
|
219
|
+
cyc = np.asarray(list(cycles), dtype=float)
|
|
220
|
+
if cyc.ndim != 1:
|
|
221
|
+
raise ValueError("cycles must be 1D")
|
|
222
|
+
|
|
223
|
+
goal_ppf = _norm_ppf(failure_probability)
|
|
224
|
+
|
|
225
|
+
ts_derived = _derive_ts(params)
|
|
226
|
+
tn_derived = _derive_tn(params)
|
|
227
|
+
|
|
228
|
+
sd = params.sd
|
|
229
|
+
nd = params.nd
|
|
230
|
+
|
|
231
|
+
# Transform sd
|
|
232
|
+
sd_transformed = sd / 10.0 ** (
|
|
233
|
+
(_NATIVE_PPF - goal_ppf) * _scattering_range_to_std(ts_derived)
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
# Transform nd
|
|
237
|
+
transformed_nd = nd / 10.0 ** (
|
|
238
|
+
(_NATIVE_PPF - goal_ppf) * _scattering_range_to_std(tn_derived)
|
|
239
|
+
)
|
|
240
|
+
if sd_transformed != 0.0:
|
|
241
|
+
nd_transformed = transformed_nd * (sd_transformed / sd) ** (-params.k1)
|
|
242
|
+
else:
|
|
243
|
+
nd_transformed = transformed_nd
|
|
244
|
+
|
|
245
|
+
ref = -nd_transformed
|
|
246
|
+
k_values = np.array(
|
|
247
|
+
[_make_k(-float(c), ref, params, miner) for c in cyc], dtype=float
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
loads = np.empty_like(cyc, dtype=float)
|
|
251
|
+
mask_finite = np.isfinite(k_values)
|
|
252
|
+
loads[mask_finite] = sd_transformed * (cyc[mask_finite] / nd_transformed) ** (
|
|
253
|
+
-1.0 / k_values[mask_finite]
|
|
254
|
+
)
|
|
255
|
+
loads[~mask_finite] = sd_transformed
|
|
256
|
+
|
|
257
|
+
return loads
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def woehler_log_space(
|
|
261
|
+
minimum: float = 1.0,
|
|
262
|
+
maximum: float = 10.0**8,
|
|
263
|
+
n: int = 101,
|
|
264
|
+
) -> np.ndarray:
|
|
265
|
+
"""Return logarithmically spaced cycle counts between ``minimum`` and ``maximum``.
|
|
266
|
+
|
|
267
|
+
This is a small convenience wrapper around :func:`numpy.logspace` with
|
|
268
|
+
defaults that are suitable for typical Woehler curves.
|
|
269
|
+
"""
|
|
270
|
+
|
|
271
|
+
if n < 2:
|
|
272
|
+
raise ValueError("n must be >= 2")
|
|
273
|
+
|
|
274
|
+
log_min = log10(minimum)
|
|
275
|
+
log_max = log10(maximum)
|
|
276
|
+
# step = (log_max - log_min) / (n - 1)
|
|
277
|
+
exponents = np.linspace(log_min, log_max, num=n)
|
|
278
|
+
return 10.0**exponents
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: typhoon-rainflow
|
|
3
|
+
Version: 0.1.8
|
|
4
|
+
Classifier: Programming Language :: Rust
|
|
5
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
6
|
+
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
7
|
+
Classifier: Typing :: Typed
|
|
8
|
+
Requires-Dist: numpy
|
|
9
|
+
Requires-Dist: pandas
|
|
10
|
+
Requires-Dist: nox ; extra == 'test'
|
|
11
|
+
Provides-Extra: test
|
|
12
|
+
Summary: Fast rainflow counting for streaming input written in Rust
|
|
13
|
+
Author: Markus Wegmann
|
|
14
|
+
Author-email: mw@technokrat.ch
|
|
15
|
+
Requires-Python: >=3.8
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# typhoon
|
|
19
|
+
[](https://github.com/technokrat/typhoon/actions/workflows/CI.yml) 
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
Typhoon is a rainflow counting Python module written in Rust by Markus Wegmann (mw@technokrat.ch).
|
|
23
|
+
|
|
24
|
+
It uses a new windowed four-point counting method which can be run in parallel on multiple cores and allows for chunk-based sample stream processing, preserving half cycles for future chunks.
|
|
25
|
+
|
|
26
|
+
It is therefore intended for real-time processing of load captures and serves as as crucial part of i-Spring's in-edge data processing chain.
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
Add the package `typhoon-rainflow` to your Python project, e.g.
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
poetry add typhoon-rainflow
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Python API
|
|
36
|
+
|
|
37
|
+
The Python package exposes two main namespaces:
|
|
38
|
+
|
|
39
|
+
- `typhoon.typhoon`: low-level, performance‑critical functions implemented in Rust.
|
|
40
|
+
- `typhoon.helper`: convenience utilities for working with the rainflow output.
|
|
41
|
+
|
|
42
|
+
The top-level package re-exports everything from `typhoon.typhoon`, so you can either
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
import typhoon # recommended for normal use
|
|
46
|
+
from typhoon import rainflow, goodman_transform
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
or
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from typhoon.typhoon import rainflow
|
|
53
|
+
from typhoon import helper # for helper utilities
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Core functions (`typhoon.typhoon`)
|
|
57
|
+
|
|
58
|
+
All arguments are keyword-compatible with the examples below.
|
|
59
|
+
|
|
60
|
+
- `init_tracing() -> None`
|
|
61
|
+
- Initialize verbose tracing/logging from the Rust implementation.
|
|
62
|
+
- Intended mainly for debugging and performance analysis; it writes to stdout.
|
|
63
|
+
|
|
64
|
+
- `rainflow(waveform, last_peaks=None, bin_size=0.0, threshold=None, min_chunk_size=64*1024)`
|
|
65
|
+
- Perform windowed four-point rainflow counting on a 1D NumPy waveform.
|
|
66
|
+
- `waveform`: 1D `numpy.ndarray` of `float32` or `float64`.
|
|
67
|
+
- `last_peaks`: optional 1D array of peaks from the previous chunk (for streaming).
|
|
68
|
+
- `bin_size`: bin width for quantizing ranges; `0.0` disables quantization.
|
|
69
|
+
- `threshold`: minimum cycle amplitude to count; default `0.0`.
|
|
70
|
+
- `min_chunk_size`: minimum chunk size for internal parallelization.
|
|
71
|
+
- Returns `(cycles, residual_peaks)` where
|
|
72
|
+
- `cycles` is a dict `{(s_lower, s_upper): count}` and
|
|
73
|
+
- `residual_peaks` is a 1D NumPy array of remaining peaks to pass to the next call.
|
|
74
|
+
|
|
75
|
+
- `goodman_transform(cycles, m, m2=None)`
|
|
76
|
+
- Apply a (piecewise) Goodman-like mean stress correction to rainflow cycles.
|
|
77
|
+
- `cycles`: mapping `{(s_lower, s_upper): count}` (e.g. first return value of `rainflow`).
|
|
78
|
+
- `m`: main slope/parameter.
|
|
79
|
+
- `m2`: optional secondary slope; defaults to `m / 3` if omitted.
|
|
80
|
+
- Returns a dict `{s_a_ers: count}` where `s_a_ers` is the equivalent range.
|
|
81
|
+
|
|
82
|
+
- `summed_histogram(hist)`
|
|
83
|
+
- Build a descending cumulative histogram from the Goodman-transformed result.
|
|
84
|
+
- `hist`: mapping `{s_a_ers: count}` such as returned from `goodman_transform`.
|
|
85
|
+
- Returns a list of `(s_a_ers, cumulative_count)` pairs sorted from high to low range.
|
|
86
|
+
|
|
87
|
+
### Helper utilities (`typhoon.helper`)
|
|
88
|
+
|
|
89
|
+
The helper module provides convenience tools for post-processing and analysis.
|
|
90
|
+
|
|
91
|
+
- `merge_cycle_counters(counters)`
|
|
92
|
+
- Merge multiple `dict`/`Counter` objects of the form `{(from, to): count}`.
|
|
93
|
+
- Useful when combining rainflow results from multiple chunks or channels.
|
|
94
|
+
|
|
95
|
+
- `add_residual_half_cycles(counter, residual_peaks)`
|
|
96
|
+
- Convert the trailing `residual_peaks` from `rainflow` into half-cycles and add them to an existing counter.
|
|
97
|
+
- Each adjacent pair of peaks `(p_i, p_{i+1})` contributes `0.5` to the corresponding cycle key.
|
|
98
|
+
|
|
99
|
+
- `counter_to_full_interval_df(counter, bin_size=0.1, closed="right", round_decimals=12)`
|
|
100
|
+
- Convert a sparse `(from, to): count` mapping into a dense 2D `pandas.DataFrame` over all intervals.
|
|
101
|
+
- Returns a DataFrame with a `(from, to)` `MultiIndex` of `pd.Interval` and a single `"value"` column.
|
|
102
|
+
|
|
103
|
+
### Woehler curves (`typhoon.woehler`)
|
|
104
|
+
|
|
105
|
+
The `typhoon.woehler` module provides helpers for evaluating S–N (Woehler) curves.
|
|
106
|
+
|
|
107
|
+
Key entry points are:
|
|
108
|
+
|
|
109
|
+
- `WoehlerCurveParams(sd, nd, k1, k2=None, ts=None, tn=None)`
|
|
110
|
+
- Container for the curve parameters:
|
|
111
|
+
- `sd`: fatigue strength at `nd` cycles for the reference failure probability.
|
|
112
|
+
- `nd`: reference number of cycles (e.g. 1e6).
|
|
113
|
+
- `k1`: slope in the finite-life region.
|
|
114
|
+
- `k2`: optional slope in the endurance region; derived from the Miner
|
|
115
|
+
rule if omitted.
|
|
116
|
+
- `ts` / `tn`: optional scattering parameters controlling probability
|
|
117
|
+
transforms of `sd` and `nd`.
|
|
118
|
+
- `MinerType` enum
|
|
119
|
+
- Miner damage rule variant that determines the second slope `k2`:
|
|
120
|
+
`NONE`, `ORIGINAL`, `ELEMENTARY`, `HAIBACH`.
|
|
121
|
+
- `woehler_log_space(minimum=1.0, maximum=1e8, n=101)`
|
|
122
|
+
- Convenience helper to generate a logarithmically spaced cycle axis for
|
|
123
|
+
plotting Woehler curves.
|
|
124
|
+
- `woehler_loads_basic(cycles, params, miner=MinerType.NONE)`
|
|
125
|
+
- Compute a "native" Woehler curve without probability/scattering
|
|
126
|
+
transformation, but honouring the selected Miner type.
|
|
127
|
+
- `woehler_loads(cycles, params, miner=MinerType.NONE, failure_probability=0.5)`
|
|
128
|
+
- Compute a probability-dependent Woehler curve using an internal
|
|
129
|
+
approximation of the normal inverse CDF.
|
|
130
|
+
|
|
131
|
+
## Example Usage
|
|
132
|
+
|
|
133
|
+
### Basic rainflow counting
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
import numpy as np
|
|
137
|
+
import typhoon
|
|
138
|
+
|
|
139
|
+
waveform = np.array([0.0, 1.0, 2.0, 1.0, 2.0, 1.0, 3.0, 4.0], dtype=np.float32)
|
|
140
|
+
|
|
141
|
+
cycles, residual_peaks = typhoon.rainflow(
|
|
142
|
+
waveform=waveform,
|
|
143
|
+
last_peaks=None,
|
|
144
|
+
bin_size=1.0,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
print("Cycles:", cycles)
|
|
148
|
+
print("Residual peaks:", residual_peaks)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Streaming / chunked processing with helpers
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
from collections import Counter
|
|
155
|
+
|
|
156
|
+
import numpy as np
|
|
157
|
+
import typhoon
|
|
158
|
+
from typhoon import helper
|
|
159
|
+
|
|
160
|
+
waveform1 = np.array([0.0, 1.0, 2.0, 1.0, 2.0, 1.0, 3.0, 4.0], dtype=np.float32)
|
|
161
|
+
waveform2 = np.array([3.0, 5.0, 4.0, 2.0], dtype=np.float32)
|
|
162
|
+
|
|
163
|
+
# First chunk
|
|
164
|
+
cycles1, residual1 = typhoon.rainflow(waveform1, last_peaks=None, bin_size=1.0)
|
|
165
|
+
|
|
166
|
+
# Second chunk, passing residual peaks from the first
|
|
167
|
+
cycles2, residual2 = typhoon.rainflow(waveform2, last_peaks=residual1, bin_size=1.0)
|
|
168
|
+
|
|
169
|
+
# Merge cycle counts from both chunks
|
|
170
|
+
merged = helper.merge_cycle_counters([cycles1, cycles2])
|
|
171
|
+
|
|
172
|
+
# Optionally add remaining half-cycles from the final residual peaks
|
|
173
|
+
merged_with_residuals = helper.add_residual_half_cycles(merged, residual2)
|
|
174
|
+
|
|
175
|
+
print("Merged cycles:", merged_with_residuals)
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Goodman transform and summed histogram
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
import typhoon
|
|
182
|
+
from typhoon import helper
|
|
183
|
+
|
|
184
|
+
cycles, residual_peaks = typhoon.rainflow(waveform, last_peaks=None, bin_size=1.0)
|
|
185
|
+
|
|
186
|
+
# Apply Goodman transform
|
|
187
|
+
hist = typhoon.goodman_transform(cycles, m=0.3)
|
|
188
|
+
|
|
189
|
+
# Summed histogram from the Goodman result
|
|
190
|
+
summed = typhoon.summed_histogram(hist)
|
|
191
|
+
|
|
192
|
+
print("Goodman result:", hist)
|
|
193
|
+
print("Summed histogram:", summed)
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Testing
|
|
197
|
+
|
|
198
|
+
```sh
|
|
199
|
+
pipx install nox
|
|
200
|
+
|
|
201
|
+
nox -s build
|
|
202
|
+
nox -s test
|
|
203
|
+
nox -s develop
|
|
204
|
+
```
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
typhoon/__init__.py,sha256=61qdDpZxeAZUZYNQSz2J5rW7yQYJweTiuEM8WAvWr0E,112
|
|
2
|
+
typhoon/__init__.pyi,sha256=AtiqHbaF7R_ztLAgj0-CpA6lRPqK_-YeEhLiAsH0ymc,43
|
|
3
|
+
typhoon/helper.py,sha256=4sCHSb14URp0h5RPVf8nqjV_-cpc2nymryaiUIz6kCE,3104
|
|
4
|
+
typhoon/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
typhoon/typhoon.cpython-314-aarch64-linux-gnu.so,sha256=CExTaMUzlLmQjTe0a4z3GuqR5rYVwSCpAOF6K9QLTZo,1225240
|
|
6
|
+
typhoon/typhoon.pyi,sha256=RscP0R0X5YnLVJTFsHodgderrpINLricnk9OzXIuMlE,737
|
|
7
|
+
typhoon/woehler.py,sha256=IRepN-51jIa6VssMLJvZn7jkTYDy9UfB51_KXiuOlLA,7806
|
|
8
|
+
typhoon_rainflow-0.1.8.dist-info/METADATA,sha256=Tnj7TbVP2rRnaN3uYN-l5o6y0jofWTzfApP5YnXM5D0,7643
|
|
9
|
+
typhoon_rainflow-0.1.8.dist-info/WHEEL,sha256=9UwWcIpOj8NXiDs6HZa3SPjHA-8VRJilh6gch3DS5Ug,149
|
|
10
|
+
typhoon_rainflow-0.1.8.dist-info/RECORD,,
|