Modal-Decomposition 0.0.4__tar.gz → 0.1.0__tar.gz
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.
- {modal_decomposition-0.0.4 → modal_decomposition-0.1.0}/PKG-INFO +3 -1
- {modal_decomposition-0.0.4 → modal_decomposition-0.1.0}/README.md +1 -0
- {modal_decomposition-0.0.4 → modal_decomposition-0.1.0}/pyproject.toml +3 -2
- modal_decomposition-0.1.0/src/Modal_Decomposition/CEEFD.py +132 -0
- modal_decomposition-0.1.0/src/Modal_Decomposition/CEEMD.py +135 -0
- modal_decomposition-0.1.0/src/Modal_Decomposition/CEEMDAN.py +92 -0
- modal_decomposition-0.1.0/src/Modal_Decomposition/EEMD.py +130 -0
- modal_decomposition-0.1.0/src/Modal_Decomposition/EFD.py +143 -0
- {modal_decomposition-0.0.4 → modal_decomposition-0.1.0}/src/Modal_Decomposition/EMD.py +23 -12
- {modal_decomposition-0.0.4 → modal_decomposition-0.1.0}/src/Modal_Decomposition/EWT.py +12 -0
- modal_decomposition-0.1.0/src/Modal_Decomposition/Error/CycleError.py +0 -0
- modal_decomposition-0.1.0/src/Modal_Decomposition/FMD.py +476 -0
- modal_decomposition-0.1.0/src/Modal_Decomposition/ICEEMDAN.py +329 -0
- modal_decomposition-0.1.0/src/Modal_Decomposition/LMD.py +178 -0
- {modal_decomposition-0.0.4 → modal_decomposition-0.1.0}/src/Modal_Decomposition/MEMD.py +5 -2
- modal_decomposition-0.1.0/src/Modal_Decomposition/RPSEMD.py +97 -0
- modal_decomposition-0.1.0/src/Modal_Decomposition/SSA.py +225 -0
- modal_decomposition-0.1.0/src/Modal_Decomposition/SVMD.py +324 -0
- modal_decomposition-0.1.0/src/Modal_Decomposition/Utils/EnvironmentMemory.py +20 -0
- modal_decomposition-0.1.0/src/Modal_Decomposition/Utils/LazyImport.py +21 -0
- modal_decomposition-0.1.0/src/Modal_Decomposition/Utils/Monotonicity.py +173 -0
- modal_decomposition-0.1.0/src/Modal_Decomposition/Utils/NumpyNdarray_MemoryCalculator.py +38 -0
- modal_decomposition-0.1.0/src/Modal_Decomposition/Utils/OneDimArray.py +27 -0
- modal_decomposition-0.1.0/src/Modal_Decomposition/Utils/__init__.py +30 -0
- {modal_decomposition-0.0.4 → modal_decomposition-0.1.0}/src/Modal_Decomposition/VMD.py +2 -0
- modal_decomposition-0.1.0/src/Modal_Decomposition/__init__.py +141 -0
- {modal_decomposition-0.0.4 → modal_decomposition-0.1.0}/src/Modal_Decomposition.egg-info/PKG-INFO +3 -1
- {modal_decomposition-0.0.4 → modal_decomposition-0.1.0}/src/Modal_Decomposition.egg-info/SOURCES.txt +8 -0
- {modal_decomposition-0.0.4 → modal_decomposition-0.1.0}/src/Modal_Decomposition.egg-info/requires.txt +1 -0
- modal_decomposition-0.1.0/tests/test.py +77 -0
- modal_decomposition-0.0.4/src/Modal_Decomposition/CEEFD.py +0 -166
- modal_decomposition-0.0.4/src/Modal_Decomposition/CEEMD.py +0 -130
- modal_decomposition-0.0.4/src/Modal_Decomposition/EEMD.py +0 -36
- modal_decomposition-0.0.4/src/Modal_Decomposition/EFD.py +0 -101
- modal_decomposition-0.0.4/src/Modal_Decomposition/FMD.py +0 -70
- modal_decomposition-0.0.4/src/Modal_Decomposition/ICEEMDAN.py +0 -166
- modal_decomposition-0.0.4/src/Modal_Decomposition/LMD.py +0 -159
- modal_decomposition-0.0.4/src/Modal_Decomposition/RPSEMD.py +0 -84
- modal_decomposition-0.0.4/src/Modal_Decomposition/SSA.py +0 -113
- modal_decomposition-0.0.4/src/Modal_Decomposition/SVMD.py +0 -59
- modal_decomposition-0.0.4/src/Modal_Decomposition/__init__.py +0 -86
- modal_decomposition-0.0.4/tests/test.py +0 -12
- {modal_decomposition-0.0.4 → modal_decomposition-0.1.0}/LICENSE +0 -0
- {modal_decomposition-0.0.4 → modal_decomposition-0.1.0}/setup.cfg +0 -0
- {modal_decomposition-0.0.4 → modal_decomposition-0.1.0}/src/Modal_Decomposition/COLOR/__init__.py +0 -0
- {modal_decomposition-0.0.4 → modal_decomposition-0.1.0}/src/Modal_Decomposition/COLOR/color_define.py +0 -0
- {modal_decomposition-0.0.4 → modal_decomposition-0.1.0}/src/Modal_Decomposition/COLOR/colorful_print.py +0 -0
- {modal_decomposition-0.0.4 → modal_decomposition-0.1.0}/src/Modal_Decomposition/help_function.py +0 -0
- {modal_decomposition-0.0.4 → modal_decomposition-0.1.0}/src/Modal_Decomposition.egg-info/dependency_links.txt +0 -0
- {modal_decomposition-0.0.4 → modal_decomposition-0.1.0}/src/Modal_Decomposition.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: Modal-Decomposition
|
|
3
|
-
Version: 0.0
|
|
3
|
+
Version: 0.1.0
|
|
4
4
|
Summary: a package for modal decomposition
|
|
5
5
|
Author-email: Mao_HaoChuan <2215269365@qq.com>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -22,6 +22,7 @@ Requires-Dist: scipy
|
|
|
22
22
|
Requires-Dist: antropy
|
|
23
23
|
Requires-Dist: tqdm
|
|
24
24
|
Requires-Dist: vmdpy
|
|
25
|
+
Requires-Dist: psutil
|
|
25
26
|
Provides-Extra: dev
|
|
26
27
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
27
28
|
Requires-Dist: black; extra == "dev"
|
|
@@ -97,6 +98,7 @@ This lib's dependence are:
|
|
|
97
98
|
- [numpy](https://github.com/numpy/numpy)
|
|
98
99
|
- [scipy](https://github.com/scipy/scipy)
|
|
99
100
|
- [vmdpy](https://github.com/vrcarva/vmdpy)
|
|
101
|
+
- [psutil](https://github.com/giampaolo/psutil)
|
|
100
102
|
|
|
101
103
|
*Other dependence please read "requirements.txt"*
|
|
102
104
|
|
|
@@ -66,6 +66,7 @@ This lib's dependence are:
|
|
|
66
66
|
- [numpy](https://github.com/numpy/numpy)
|
|
67
67
|
- [scipy](https://github.com/scipy/scipy)
|
|
68
68
|
- [vmdpy](https://github.com/vrcarva/vmdpy)
|
|
69
|
+
- [psutil](https://github.com/giampaolo/psutil)
|
|
69
70
|
|
|
70
71
|
*Other dependence please read "requirements.txt"*
|
|
71
72
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "Modal-Decomposition"
|
|
7
|
-
version = "0.0
|
|
7
|
+
version = "0.1.0"
|
|
8
8
|
requires-python = ">=3.8"
|
|
9
9
|
|
|
10
10
|
authors = [
|
|
@@ -28,7 +28,8 @@ dependencies = [
|
|
|
28
28
|
"scipy",
|
|
29
29
|
"antropy",
|
|
30
30
|
"tqdm",
|
|
31
|
-
"vmdpy"
|
|
31
|
+
"vmdpy",
|
|
32
|
+
"psutil"
|
|
32
33
|
]
|
|
33
34
|
optional-dependencies = {"dev"=["pytest>=7.0", "black"], "plot"=["matplotlib"]}
|
|
34
35
|
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Python version: (must)
|
|
3
|
+
3.10.11
|
|
4
|
+
|
|
5
|
+
Lib and Version: (if None write None)
|
|
6
|
+
EMD-S - 1.9.0
|
|
7
|
+
|
|
8
|
+
Only accessed by: (must)
|
|
9
|
+
Only __init__.py
|
|
10
|
+
|
|
11
|
+
Description: (if None write None)
|
|
12
|
+
Realize the CEEFD and CEEMDAN.
|
|
13
|
+
|
|
14
|
+
Modify: (must)
|
|
15
|
+
2026.3.25 - Create
|
|
16
|
+
2026.3.30 - Desperate the CEEMDAN and the CEEFD, and rename the Cyclic_CEEFD as CEEFD, del the origin CEEFD.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import numpy as np
|
|
20
|
+
from typing import Union, Tuple
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ceefd:
|
|
24
|
+
"""CEEFD: Cyclic Envelop Empirical Fourier Decomposition"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, fs=1.0, min_peak_distance=10, envelop_iter=3):
|
|
27
|
+
self.fs = fs
|
|
28
|
+
self.min_peak_distance = min_peak_distance
|
|
29
|
+
self.envelop_iter = envelop_iter
|
|
30
|
+
|
|
31
|
+
def __call__(self, S):
|
|
32
|
+
return self.decompose(S)
|
|
33
|
+
|
|
34
|
+
def _compute_spectral_envelope(self, mag_spectrum):
|
|
35
|
+
from scipy.signal import windows
|
|
36
|
+
|
|
37
|
+
n = len(mag_spectrum)
|
|
38
|
+
envelope = np.copy(mag_spectrum)
|
|
39
|
+
window_size = int(0.05 * n)
|
|
40
|
+
window = windows.boxcar(window_size)
|
|
41
|
+
|
|
42
|
+
for _ in range(self.envelop_iter):
|
|
43
|
+
envelope = np.convolve(envelope, window, mode='same') / window_size
|
|
44
|
+
envelope = np.maximum(envelope, mag_spectrum)
|
|
45
|
+
return envelope
|
|
46
|
+
|
|
47
|
+
def _extract_imf(self, signal, freq_bins):
|
|
48
|
+
N = len(signal)
|
|
49
|
+
fft_signal = np.fft.fft(signal)
|
|
50
|
+
|
|
51
|
+
mask = np.zeros(N, dtype=bool)
|
|
52
|
+
mask[freq_bins] = True
|
|
53
|
+
mask[N - np.array(freq_bins)] = True
|
|
54
|
+
|
|
55
|
+
imf_fft = fft_signal * mask
|
|
56
|
+
imf = np.fft.ifft(imf_fft).real
|
|
57
|
+
|
|
58
|
+
return imf, mask
|
|
59
|
+
|
|
60
|
+
def decompose(self, S: Union[list, np.ndarray], T: Union[list, np.ndarray]=None) -> Tuple[np.ndarray, np.ndarray]:
|
|
61
|
+
"""
|
|
62
|
+
CEEFD: Cyclic Envelope Empirical Fourier Decomposition
|
|
63
|
+
|
|
64
|
+
:param S: Signal
|
|
65
|
+
:param T: Time-axis, accepted formation is Unix array. Default uniformly sample.
|
|
66
|
+
:return: IMFs (2-dim), Res (1-dim)
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
from scipy.signal import find_peaks
|
|
70
|
+
|
|
71
|
+
if not isinstance(S, np.ndarray):
|
|
72
|
+
S = np.array(S)
|
|
73
|
+
|
|
74
|
+
if S.ndim == 0:
|
|
75
|
+
raise ValueError("The dim of the S must be 1-dim, not 0")
|
|
76
|
+
|
|
77
|
+
elif S.ndim > 1:
|
|
78
|
+
if 1 in S.shape:
|
|
79
|
+
S = S.reshape(-1)
|
|
80
|
+
|
|
81
|
+
else:
|
|
82
|
+
raise ValueError(f"The dim of S must be 1-dim, not {S.ndim}")
|
|
83
|
+
|
|
84
|
+
N = len(S)
|
|
85
|
+
|
|
86
|
+
if T is None: # if T is None, default generate uniform T-axis.
|
|
87
|
+
T = np.arange(N) # default fs = 1
|
|
88
|
+
print(f"Warn: T is None,default T = [0, 1, 2, ..., {N - 1}]")
|
|
89
|
+
|
|
90
|
+
else:
|
|
91
|
+
if not isinstance(T, np.ndarray):
|
|
92
|
+
T = np.array(T)
|
|
93
|
+
|
|
94
|
+
fft_signal = np.fft.fft(S)
|
|
95
|
+
mag_spectrum = np.abs(fft_signal[:N // 2 + 1])
|
|
96
|
+
|
|
97
|
+
envelope = self._compute_spectral_envelope(mag_spectrum)
|
|
98
|
+
|
|
99
|
+
peaks, properties = find_peaks(envelope, distance=self.min_peak_distance)
|
|
100
|
+
|
|
101
|
+
if len(peaks) == 0:
|
|
102
|
+
return [S], []
|
|
103
|
+
|
|
104
|
+
boundaries = [0]
|
|
105
|
+
for i in range(len(peaks) - 1):
|
|
106
|
+
boundary = (peaks[i] + peaks[i + 1]) // 2
|
|
107
|
+
boundaries.append(boundary)
|
|
108
|
+
boundaries.append(N // 2)
|
|
109
|
+
|
|
110
|
+
imfs = []
|
|
111
|
+
freq_masks = []
|
|
112
|
+
|
|
113
|
+
for i in range(len(boundaries) - 1):
|
|
114
|
+
start_bin = boundaries[i]
|
|
115
|
+
end_bin = boundaries[i + 1]
|
|
116
|
+
|
|
117
|
+
if end_bin - start_bin < 2:
|
|
118
|
+
continue
|
|
119
|
+
|
|
120
|
+
freq_bins = list(range(start_bin, end_bin))
|
|
121
|
+
imf, mask = self._extract_imf(S, freq_bins)
|
|
122
|
+
imfs.append(imf)
|
|
123
|
+
freq_masks.append(mask)
|
|
124
|
+
|
|
125
|
+
residual = S.copy()
|
|
126
|
+
for imf in imfs:
|
|
127
|
+
residual -= imf
|
|
128
|
+
|
|
129
|
+
if np.abs(residual).max() > 1e-10:
|
|
130
|
+
imfs.append(residual)
|
|
131
|
+
|
|
132
|
+
return np.array(imfs), residual
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Python version: (must)
|
|
3
|
+
3.10.11
|
|
4
|
+
|
|
5
|
+
Lib and Version: (if None write None)
|
|
6
|
+
numpy - 2.2.6
|
|
7
|
+
time - 1.39.2
|
|
8
|
+
tqdm - 4.67.3
|
|
9
|
+
|
|
10
|
+
Only accessed by: (must)
|
|
11
|
+
Only __init__.py
|
|
12
|
+
|
|
13
|
+
Description: (if None write None)
|
|
14
|
+
Realize the CEEMD.
|
|
15
|
+
|
|
16
|
+
Modify: (must)
|
|
17
|
+
2026.3.25 - Create
|
|
18
|
+
2026.4.2 - Finish the Optimization of the CEEMD.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from typing import Union, Tuple, Optional
|
|
22
|
+
from time import sleep
|
|
23
|
+
import numpy as np
|
|
24
|
+
from .EMD import emd
|
|
25
|
+
from .Utils import monotonic_increasing, monotonic_decreasing
|
|
26
|
+
from tqdm import tqdm
|
|
27
|
+
from warnings import warn
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def ceemd(S: Union[list, np.ndarray], T: Union[list, np.ndarray]=None, N_whitenoise=37, beta=0.3, max_imf: Optional[int]=None, dead_line: int=10, verbose: bool=False) \
|
|
31
|
+
-> Tuple[np.ndarray, np.ndarray]:
|
|
32
|
+
"""
|
|
33
|
+
CEEMD: Complementary Ensemble Empirical Mode Decomposition
|
|
34
|
+
|
|
35
|
+
:param S: Signal (1-dim)
|
|
36
|
+
:param T: the time axis.
|
|
37
|
+
:param N_whitenoise: the num of the added whitenoise.
|
|
38
|
+
:param beta:
|
|
39
|
+
:param max_imf: -1, None or other int | -1 means decompose completely, None means give a int auto, other int means the num of the IMFs
|
|
40
|
+
:param dead_line: Sometime it'll be in unuseful cycle, when the average of the N's sequence with added whitenoise is empty([]). It'll be forced exit when the time of the cycle above the deadline.
|
|
41
|
+
:param verbose:
|
|
42
|
+
:return: IMFs (n_IMFs, N), Res (N,)
|
|
43
|
+
"""
|
|
44
|
+
if beta <= 0:
|
|
45
|
+
raise ValueError("The beta should > 0")
|
|
46
|
+
|
|
47
|
+
if N_whitenoise <= 0 or not isinstance(N_whitenoise, int):
|
|
48
|
+
raise TypeError("N_whitenoise must be int type or > 0")
|
|
49
|
+
|
|
50
|
+
if not isinstance(S, np.ndarray):
|
|
51
|
+
S = np.array(S)
|
|
52
|
+
|
|
53
|
+
if S.ndim == 0:
|
|
54
|
+
raise ValueError("The dim of the S must be 1-dim, not 0")
|
|
55
|
+
|
|
56
|
+
elif S.ndim > 1:
|
|
57
|
+
if 1 in S.shape:
|
|
58
|
+
S = S.reshape(-1)
|
|
59
|
+
|
|
60
|
+
else:
|
|
61
|
+
raise ValueError(f"The dim of S must be 1-dim, not {S.ndim}")
|
|
62
|
+
|
|
63
|
+
N = len(S)
|
|
64
|
+
|
|
65
|
+
if T is None: # if T is None, default generate uniform T-axis.
|
|
66
|
+
T = np.arange(N) # default fs = 1
|
|
67
|
+
print(f"Warn: T is None,default T = [0, 1, 2, ..., {N - 1}]")
|
|
68
|
+
|
|
69
|
+
else:
|
|
70
|
+
if not isinstance(T, np.ndarray):
|
|
71
|
+
T = np.array(T)
|
|
72
|
+
|
|
73
|
+
if len(T) != len(S):
|
|
74
|
+
raise ValueError("The length of T must be equal to Signal.")
|
|
75
|
+
|
|
76
|
+
if not np.all(np.diff(T) > 0):
|
|
77
|
+
raise ValueError("T should be monotonic increasing!")
|
|
78
|
+
|
|
79
|
+
if np.any(np.allclose(np.diff(np.diff(T)))):
|
|
80
|
+
warn("The T is not uniform! Some error may happen.")
|
|
81
|
+
|
|
82
|
+
if max_imf is None:
|
|
83
|
+
max_imf = int(np.log2(len(S))) + 2
|
|
84
|
+
|
|
85
|
+
Res = S
|
|
86
|
+
IMFs = []
|
|
87
|
+
|
|
88
|
+
# std_dev = np.std(S)
|
|
89
|
+
|
|
90
|
+
count = 0
|
|
91
|
+
dead_cycle = 0
|
|
92
|
+
while True:
|
|
93
|
+
std_dev = np.std(Res)
|
|
94
|
+
|
|
95
|
+
_IMFs = []
|
|
96
|
+
for n in range(N_whitenoise):
|
|
97
|
+
white_noise = np.random.normal(0, std_dev * beta, N)
|
|
98
|
+
S_plus = Res + white_noise
|
|
99
|
+
S_minus = Res - white_noise
|
|
100
|
+
_IMFs_plus, _ = emd(S_plus, T, max_imf=1)
|
|
101
|
+
_IMFs_minus, _ = emd(S_minus, T, max_imf=1)
|
|
102
|
+
|
|
103
|
+
if not _IMFs_plus or not _IMFs_minus:
|
|
104
|
+
continue
|
|
105
|
+
|
|
106
|
+
_IMFs.append((_IMFs_plus[0] + _IMFs_minus[0]) / 2.0)
|
|
107
|
+
|
|
108
|
+
if not _IMFs:
|
|
109
|
+
dead_cycle += 1
|
|
110
|
+
continue
|
|
111
|
+
|
|
112
|
+
if dead_cycle >= dead_line:
|
|
113
|
+
raise RuntimeError("Trapped in a vicious cycle")
|
|
114
|
+
else:
|
|
115
|
+
dead_cycle = 0
|
|
116
|
+
|
|
117
|
+
IMF = np.mean(_IMFs, axis=0)
|
|
118
|
+
|
|
119
|
+
IMFs.append(IMF)
|
|
120
|
+
|
|
121
|
+
Res -= IMF
|
|
122
|
+
count += 1
|
|
123
|
+
|
|
124
|
+
if verbose:
|
|
125
|
+
if count % 10 == 0:
|
|
126
|
+
print(f"has get {count} IMFs...")
|
|
127
|
+
sleep(1)
|
|
128
|
+
|
|
129
|
+
if max_imf != -1:
|
|
130
|
+
if count >= max_imf or monotonic_increasing(Res) or monotonic_decreasing(Res):
|
|
131
|
+
return np.array(IMFs), np.array(Res)
|
|
132
|
+
|
|
133
|
+
else:
|
|
134
|
+
if monotonic_increasing(Res) or monotonic_decreasing(Res):
|
|
135
|
+
return np.array(IMFs), np.array(Res)
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
from typing import Union, Tuple
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
def ceemdan \
|
|
5
|
+
(
|
|
6
|
+
S: Union[list, np.ndarray], T: Union[list, np.ndarray] = None,
|
|
7
|
+
max_imf: int = -1,
|
|
8
|
+
trials=10,
|
|
9
|
+
noise_width=0.05, # default: 0.05-0.3
|
|
10
|
+
noise_seed=42, # seed
|
|
11
|
+
spline_kind='cubic',
|
|
12
|
+
nbsym=2, # Number of boundary symmetry points
|
|
13
|
+
extrema_detection='parabol',
|
|
14
|
+
parallel=False,
|
|
15
|
+
processes=None, # None = auto, int >= 1
|
|
16
|
+
random_state=42,
|
|
17
|
+
noise_scale=1.0, # scale factor of noise
|
|
18
|
+
noise_kind='normal', # noise kind: 'normal', 'uniform'
|
|
19
|
+
range_thr=0.01, # Stop threshold
|
|
20
|
+
total_power_thr=0.005
|
|
21
|
+
) -> Tuple[np.ndarray, np.ndarray]:
|
|
22
|
+
|
|
23
|
+
"""
|
|
24
|
+
CEEMDAN: Complete Ensemble Empirical Mode Decomposition with Adaptive Noise
|
|
25
|
+
|
|
26
|
+
:param S: Signal (1-dim)
|
|
27
|
+
:param T: Time axis (1-dim). Default uniform, or input the Unix.
|
|
28
|
+
:param max_imf: the num of the decomposed IMFs. | -1 means all.
|
|
29
|
+
:param trials:
|
|
30
|
+
:param noise_width:
|
|
31
|
+
:param noise_seed:
|
|
32
|
+
:param spline_kind:
|
|
33
|
+
:param nbsym:
|
|
34
|
+
:param extrema_detection:
|
|
35
|
+
:param parallel:
|
|
36
|
+
:param processes:
|
|
37
|
+
:param random_state:
|
|
38
|
+
:param noise_scale:
|
|
39
|
+
:param noise_kind:
|
|
40
|
+
:param range_thr:
|
|
41
|
+
:param total_power_thr:
|
|
42
|
+
:return: IMFs (n_IMFs, N), Res (N,)
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
from PyEMD import CEEMDAN
|
|
46
|
+
|
|
47
|
+
if not isinstance(S, np.ndarray):
|
|
48
|
+
S = np.array(S)
|
|
49
|
+
|
|
50
|
+
if S.ndim == 0:
|
|
51
|
+
raise ValueError("The dim of the S must be 1-dim, not 0")
|
|
52
|
+
|
|
53
|
+
elif S.ndim > 1:
|
|
54
|
+
if 1 in S.shape:
|
|
55
|
+
S = S.reshape(-1)
|
|
56
|
+
|
|
57
|
+
else:
|
|
58
|
+
raise ValueError(f"The dim of S must be 1-dim, not {S.ndim}")
|
|
59
|
+
|
|
60
|
+
N = len(S)
|
|
61
|
+
|
|
62
|
+
if T is None: # if T is None, default generate uniform T-axis.
|
|
63
|
+
T = np.arange(N) # default fs = 1
|
|
64
|
+
print(f"Warn: T is None,default T = [0, 1, 2, ..., {N - 1}]")
|
|
65
|
+
|
|
66
|
+
else:
|
|
67
|
+
if not isinstance(T, np.ndarray):
|
|
68
|
+
T = np.array(T)
|
|
69
|
+
|
|
70
|
+
CEEMDAN = CEEMDAN \
|
|
71
|
+
(
|
|
72
|
+
trials=trials,
|
|
73
|
+
noise_width=noise_width,
|
|
74
|
+
noise_seed=noise_seed,
|
|
75
|
+
spline_kind=spline_kind,
|
|
76
|
+
nbsym=nbsym,
|
|
77
|
+
extrema_detection=extrema_detection,
|
|
78
|
+
parallel=parallel,
|
|
79
|
+
processes=processes,
|
|
80
|
+
random_state=random_state,
|
|
81
|
+
noise_scale=noise_scale,
|
|
82
|
+
noise_kind=noise_kind,
|
|
83
|
+
range_thr=range_thr,
|
|
84
|
+
total_power_thr=total_power_thr
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
IMF_Residue = CEEMDAN.ceemdan(S, T, max_imf)
|
|
88
|
+
|
|
89
|
+
IMFs = IMF_Residue[:-1, :] # shape [n_imfs, len(S)]
|
|
90
|
+
Res = IMF_Residue[-1, :]
|
|
91
|
+
|
|
92
|
+
return IMFs, Res
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Python version: (must)
|
|
3
|
+
3.10.11
|
|
4
|
+
|
|
5
|
+
Lib and Version: (if None write None)
|
|
6
|
+
EMD-S - 1.9.0
|
|
7
|
+
|
|
8
|
+
Only accessed by: (must)
|
|
9
|
+
Only __init__.py
|
|
10
|
+
|
|
11
|
+
Description: (if None write None)
|
|
12
|
+
Realize the EEMD.
|
|
13
|
+
|
|
14
|
+
Modify: (must)
|
|
15
|
+
2026.3.25 - Create.
|
|
16
|
+
2026.3.27 - Change the EEMD class' usage. EEMD(parallel=True) (default) -> EEMD(parallel=False). Now, it will not use parallel default.
|
|
17
|
+
2026.4.2 - Finish the Optimization of the EEMD. Correct the logic.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import numpy as np
|
|
21
|
+
from typing import Union, Tuple, Optional
|
|
22
|
+
from .Utils import monotonic_increasing, monotonic_decreasing
|
|
23
|
+
from .Error import CycleError
|
|
24
|
+
from time import sleep
|
|
25
|
+
from warnings import warn
|
|
26
|
+
from .EMD import emd
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def eemd(S: Union[list, np.ndarray], T: Union[list, np.ndarray]=None, N_whitenoise=300, beta=0.3, max_imf: Optional[int]=None, dead_line: int=10, verbose: bool=False) \
|
|
30
|
+
-> Tuple[np.ndarray, np.ndarray]:
|
|
31
|
+
"""
|
|
32
|
+
EEMD: Ensemble Empirical Mode Decomposition
|
|
33
|
+
|
|
34
|
+
:param S: Signal (1-dim)
|
|
35
|
+
:param T: the time axis.
|
|
36
|
+
:param N_whitenoise: the num of the added whitenoise.
|
|
37
|
+
:param beta:
|
|
38
|
+
:param max_imf: -1, None or other int | -1 means decompose completely, None means give a int auto, other int means the num of the IMFs
|
|
39
|
+
:param dead_line: Sometime it'll be in unuseful cycle, when the average of the N's sequence with added whitenoise is empty([]). It'll be forced exit when the time of the cycle above the deadline.
|
|
40
|
+
:param verbose:
|
|
41
|
+
:return: IMFs (n_IMFs, N), Res (N,)
|
|
42
|
+
"""
|
|
43
|
+
if beta <= 0:
|
|
44
|
+
raise ValueError("The beta should > 0")
|
|
45
|
+
|
|
46
|
+
if N_whitenoise <= 0 or not isinstance(N_whitenoise, int):
|
|
47
|
+
raise TypeError("N_whitenoise must be int type or > 0")
|
|
48
|
+
|
|
49
|
+
if not isinstance(S, np.ndarray):
|
|
50
|
+
S = np.array(S)
|
|
51
|
+
|
|
52
|
+
if S.ndim == 0:
|
|
53
|
+
raise ValueError("The dim of the S must be 1-dim, not 0")
|
|
54
|
+
|
|
55
|
+
elif S.ndim > 1:
|
|
56
|
+
if 1 in S.shape:
|
|
57
|
+
S = S.reshape(-1)
|
|
58
|
+
|
|
59
|
+
else:
|
|
60
|
+
raise ValueError(f"The dim of S must be 1-dim, not {S.ndim}")
|
|
61
|
+
|
|
62
|
+
N = len(S)
|
|
63
|
+
|
|
64
|
+
if T is None: # if T is None, default generate uniform T-axis.
|
|
65
|
+
T = np.arange(N) # default fs = 1
|
|
66
|
+
print(f"Warn: T is None,default T = [0, 1, 2, ..., {N - 1}]")
|
|
67
|
+
|
|
68
|
+
else:
|
|
69
|
+
if not isinstance(T, np.ndarray):
|
|
70
|
+
T = np.array(T)
|
|
71
|
+
|
|
72
|
+
if len(T) != len(S):
|
|
73
|
+
raise ValueError("The length of T must be equal to Signal.")
|
|
74
|
+
|
|
75
|
+
if not np.all(np.diff(T) > 0):
|
|
76
|
+
raise ValueError("T should be monotonic increasing!")
|
|
77
|
+
|
|
78
|
+
if np.any(np.allclose(np.diff(np.diff(T)))):
|
|
79
|
+
warn("The T is not uniform! Some error may happen.")
|
|
80
|
+
|
|
81
|
+
if max_imf is None:
|
|
82
|
+
max_imf = int(np.log2(len(S))) + 2
|
|
83
|
+
|
|
84
|
+
Res = S
|
|
85
|
+
IMFs = []
|
|
86
|
+
|
|
87
|
+
# std_dev = np.std(S)
|
|
88
|
+
|
|
89
|
+
count = 0
|
|
90
|
+
dead_cycle = 0
|
|
91
|
+
while True:
|
|
92
|
+
std_dev = np.std(Res)
|
|
93
|
+
|
|
94
|
+
_IMFs = []
|
|
95
|
+
for n in range(N_whitenoise):
|
|
96
|
+
white_noise = np.random.normal(0, std_dev * beta, N)
|
|
97
|
+
_S = Res + white_noise
|
|
98
|
+
_IMFs_, _ = emd(_S, T, max_imf=1)
|
|
99
|
+
if not _IMFs_:
|
|
100
|
+
continue
|
|
101
|
+
_IMFs.append(_IMFs_[0])
|
|
102
|
+
|
|
103
|
+
if not _IMFs:
|
|
104
|
+
dead_cycle += 1
|
|
105
|
+
continue
|
|
106
|
+
|
|
107
|
+
if dead_cycle >= dead_line:
|
|
108
|
+
raise RuntimeError("Trapped in a vicious cycle")
|
|
109
|
+
else:
|
|
110
|
+
dead_cycle = 0
|
|
111
|
+
|
|
112
|
+
IMF = np.mean(_IMFs, axis=0)
|
|
113
|
+
|
|
114
|
+
IMFs.append(IMF)
|
|
115
|
+
|
|
116
|
+
Res -= IMF
|
|
117
|
+
count += 1
|
|
118
|
+
|
|
119
|
+
if verbose:
|
|
120
|
+
if count % 10 == 0:
|
|
121
|
+
print(f"has get {count} IMFs...")
|
|
122
|
+
sleep(1)
|
|
123
|
+
|
|
124
|
+
if max_imf != -1:
|
|
125
|
+
if count >= max_imf or monotonic_increasing(Res) or monotonic_decreasing(Res):
|
|
126
|
+
return np.array(IMFs), np.array(Res)
|
|
127
|
+
|
|
128
|
+
else:
|
|
129
|
+
if monotonic_increasing(Res) or monotonic_decreasing(Res):
|
|
130
|
+
return np.array(IMFs), np.array(Res)
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Python version: (must)
|
|
3
|
+
3.10.11
|
|
4
|
+
|
|
5
|
+
Lib and Version: (if None write None)
|
|
6
|
+
numpy - 2.2.6
|
|
7
|
+
scipy - 1.15.3
|
|
8
|
+
matplotlib - 3.10.8
|
|
9
|
+
|
|
10
|
+
Only accessed by: (must)
|
|
11
|
+
Only __init__.py
|
|
12
|
+
|
|
13
|
+
Description: (if None write None)
|
|
14
|
+
Realize the EFD.
|
|
15
|
+
Optimize the use of scipy.S
|
|
16
|
+
|
|
17
|
+
Modify: (must)
|
|
18
|
+
2026.3.25 - Create
|
|
19
|
+
2026.4.2 - Finish the Optimization of the EFD. Del the origin efd function.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import numpy as np
|
|
23
|
+
from .COLOR import printc
|
|
24
|
+
from typing import Union, Tuple
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def efd(S: Union[list, np.ndarray], T: Union[list, np.ndarray]=None, max_IMFs: int=-1) -> Tuple[np.ndarray, np.ndarray]:
|
|
28
|
+
"""
|
|
29
|
+
EFD: Empirical Fourier Decomposition
|
|
30
|
+
|
|
31
|
+
:param S: Signal (1-dim)
|
|
32
|
+
:param T: Time axis (1-dim)
|
|
33
|
+
:param max_IMFs: the num of the IMFs. -1 means return all IMFs
|
|
34
|
+
:return: IMFs (n_IMFs, N), Res: (N,)
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
from scipy.signal import argrelmax
|
|
38
|
+
|
|
39
|
+
if not isinstance(max_IMFs, int):
|
|
40
|
+
raise TypeError("The type of the max_IMFs must be int!")
|
|
41
|
+
|
|
42
|
+
if max_IMFs != -1 and max_IMFs <= 0:
|
|
43
|
+
if max_IMFs <= 0:
|
|
44
|
+
raise ValueError("Invalid value! Do you want use -1?")
|
|
45
|
+
|
|
46
|
+
if not isinstance(S, np.ndarray):
|
|
47
|
+
S = np.array(S)
|
|
48
|
+
|
|
49
|
+
if S.ndim == 0:
|
|
50
|
+
raise ValueError("The dim of the S must be 1-dim, not 0")
|
|
51
|
+
|
|
52
|
+
elif S.ndim > 1:
|
|
53
|
+
if 1 in S.shape:
|
|
54
|
+
S = S.reshape(-1)
|
|
55
|
+
|
|
56
|
+
else:
|
|
57
|
+
raise ValueError(f"The dim of S must be 1-dim, not {S.ndim}")
|
|
58
|
+
|
|
59
|
+
N = len(S)
|
|
60
|
+
|
|
61
|
+
if T is None: # if T is None, default generate uniform T-axis.
|
|
62
|
+
T = np.arange(N) # default fs = 1
|
|
63
|
+
print(f"Warn: T is None,default T = [0, 1, 2, ..., {N - 1}]")
|
|
64
|
+
|
|
65
|
+
else:
|
|
66
|
+
if not isinstance(T, np.ndarray):
|
|
67
|
+
T = np.array(T)
|
|
68
|
+
|
|
69
|
+
if len(T) != N:
|
|
70
|
+
raise ValueError(f"len of T: ({len(T)}) doesn't match ({N})")
|
|
71
|
+
|
|
72
|
+
dt = np.diff(T)
|
|
73
|
+
if not np.allclose(dt, dt[0], rtol=1e-10, atol=1e-14):
|
|
74
|
+
raise ValueError("Time series should be uniform intervals")
|
|
75
|
+
|
|
76
|
+
# make seq 0-mean value
|
|
77
|
+
MEAN = np.mean(S)
|
|
78
|
+
S: np.ndarray = S - np.mean(S)
|
|
79
|
+
|
|
80
|
+
F = np.fft.fft(S)
|
|
81
|
+
# fs = np.fft.fftfreq(N // 2 + 1, d=T[1]-T[0]) # only positive freq arr
|
|
82
|
+
magnitude = np.abs(F)
|
|
83
|
+
phase = np.angle(F)
|
|
84
|
+
|
|
85
|
+
edge_magnitude = magnitude[: N // 2 + 1].copy()[1:] * 2 # filter the dc(loc[0])
|
|
86
|
+
uniform_freq = np.linspace(0, np.pi, N // 2)
|
|
87
|
+
freq_N = len(uniform_freq)
|
|
88
|
+
|
|
89
|
+
local_maximum_points = argrelmax(edge_magnitude)
|
|
90
|
+
local_maximum = edge_magnitude[local_maximum_points]
|
|
91
|
+
|
|
92
|
+
if max_IMFs != -1:
|
|
93
|
+
local_maximum_zip = [(point, value) for point, value in zip(local_maximum_points, local_maximum)]
|
|
94
|
+
|
|
95
|
+
local_maximum_zip = sorted(local_maximum_zip, reverse=True, key=lambda x: x[1])
|
|
96
|
+
|
|
97
|
+
local_maximum_points = list(map(lambda x: x[0], local_maximum_zip[:max_IMFs]))
|
|
98
|
+
|
|
99
|
+
local_maximum_points = np.concatenate(([0], local_maximum_points, [freq_N - 1]))
|
|
100
|
+
|
|
101
|
+
local_maximum_points = np.unique(local_maximum_points)
|
|
102
|
+
local_maximum_points = np.sort(local_maximum_points)
|
|
103
|
+
|
|
104
|
+
wn = [] # the zero phase filter
|
|
105
|
+
for p in range(len(local_maximum_points) - 1):
|
|
106
|
+
next_point = local_maximum_points[p + 1]
|
|
107
|
+
current_point = local_maximum_points[p]
|
|
108
|
+
|
|
109
|
+
if edge_magnitude[current_point] == edge_magnitude[next_point]:
|
|
110
|
+
wn.append(current_point)
|
|
111
|
+
|
|
112
|
+
else:
|
|
113
|
+
# wn.append(np.argmin(edge_magnitude[current_point:next_point + 1]))
|
|
114
|
+
wn.append(current_point + np.argmin(edge_magnitude[current_point:next_point + 1]))
|
|
115
|
+
wn = np.concatenate(([0], wn, freq_N - 1))
|
|
116
|
+
|
|
117
|
+
filters_arr = []
|
|
118
|
+
for edge in range(1, len(wn)):
|
|
119
|
+
filter_single = np.zeros(freq_N)
|
|
120
|
+
|
|
121
|
+
current_point = wn[edge]
|
|
122
|
+
last_point = wn[edge - 1]
|
|
123
|
+
|
|
124
|
+
filter_single[last_point: current_point + 1] = 1
|
|
125
|
+
filters_arr.append(filter_single)
|
|
126
|
+
|
|
127
|
+
IMFs = []
|
|
128
|
+
M = N // 2 + 1
|
|
129
|
+
# the spectrum is symmetry, so we just need positive, and next, only need concentrate symmetrically.
|
|
130
|
+
for _filter in filters_arr:
|
|
131
|
+
filtered_magnitude = np.zeros(M, dtype=float)
|
|
132
|
+
filtered_magnitude[1:] = magnitude[1:M] * _filter
|
|
133
|
+
temp_spectrum = filtered_magnitude * np.exp(1j * phase[:M])
|
|
134
|
+
|
|
135
|
+
full_spectrum = np.zeros(N, dtype=complex)
|
|
136
|
+
full_spectrum[:M] = temp_spectrum # complex temp_spectrum
|
|
137
|
+
for i in range(1, M - 1):
|
|
138
|
+
full_spectrum[N - i] = np.conj(temp_spectrum[i])
|
|
139
|
+
|
|
140
|
+
IMFs.append(np.fft.ifft(full_spectrum).real)
|
|
141
|
+
|
|
142
|
+
IMFs = np.array(IMFs)
|
|
143
|
+
return IMFs, S - np.sum(IMFs, axis=0) + MEAN
|