typhoon-rainflow 0.1.8__pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.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 ADDED
@@ -0,0 +1,5 @@
1
+ from .typhoon import *
2
+
3
+ __doc__ = typhoon.__doc__
4
+ if hasattr(typhoon, "__all__"):
5
+ __all__ = typhoon.__all__
typhoon/__init__.pyi ADDED
@@ -0,0 +1,3 @@
1
+ from .typhoon import *
2
+
3
+ __all__: list[str]
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
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
+ [![CI](https://github.com/technokrat/typhoon/actions/workflows/CI.yml/badge.svg)](https://github.com/technokrat/typhoon/actions/workflows/CI.yml) ![PyPI - Version](https://img.shields.io/pypi/v/typhoon-rainflow)
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.pyi,sha256=RscP0R0X5YnLVJTFsHodgderrpINLricnk9OzXIuMlE,737
6
+ typhoon/typhoon.pypy310-pp73-ppc_64-linux-gnu.so,sha256=9-JhwSejWcAw4yzjhjIOtQG7YzVQsZ18nPNGfgyNhr0,1672264
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=TyjkovWgtOmm9e49SjFFYRv_cVIUdgoK6t5RCXiKq8A,163
10
+ typhoon_rainflow-0.1.8.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: maturin (1.10.1)
3
+ Root-Is-Purelib: false
4
+ Tag: pp310-pypy310_pp73-manylinux_2_17_ppc64le
5
+ Tag: pp310-pypy310_pp73-manylinux2014_ppc64le