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.
Files changed (50) hide show
  1. {modal_decomposition-0.0.4 → modal_decomposition-0.1.0}/PKG-INFO +3 -1
  2. {modal_decomposition-0.0.4 → modal_decomposition-0.1.0}/README.md +1 -0
  3. {modal_decomposition-0.0.4 → modal_decomposition-0.1.0}/pyproject.toml +3 -2
  4. modal_decomposition-0.1.0/src/Modal_Decomposition/CEEFD.py +132 -0
  5. modal_decomposition-0.1.0/src/Modal_Decomposition/CEEMD.py +135 -0
  6. modal_decomposition-0.1.0/src/Modal_Decomposition/CEEMDAN.py +92 -0
  7. modal_decomposition-0.1.0/src/Modal_Decomposition/EEMD.py +130 -0
  8. modal_decomposition-0.1.0/src/Modal_Decomposition/EFD.py +143 -0
  9. {modal_decomposition-0.0.4 → modal_decomposition-0.1.0}/src/Modal_Decomposition/EMD.py +23 -12
  10. {modal_decomposition-0.0.4 → modal_decomposition-0.1.0}/src/Modal_Decomposition/EWT.py +12 -0
  11. modal_decomposition-0.1.0/src/Modal_Decomposition/Error/CycleError.py +0 -0
  12. modal_decomposition-0.1.0/src/Modal_Decomposition/FMD.py +476 -0
  13. modal_decomposition-0.1.0/src/Modal_Decomposition/ICEEMDAN.py +329 -0
  14. modal_decomposition-0.1.0/src/Modal_Decomposition/LMD.py +178 -0
  15. {modal_decomposition-0.0.4 → modal_decomposition-0.1.0}/src/Modal_Decomposition/MEMD.py +5 -2
  16. modal_decomposition-0.1.0/src/Modal_Decomposition/RPSEMD.py +97 -0
  17. modal_decomposition-0.1.0/src/Modal_Decomposition/SSA.py +225 -0
  18. modal_decomposition-0.1.0/src/Modal_Decomposition/SVMD.py +324 -0
  19. modal_decomposition-0.1.0/src/Modal_Decomposition/Utils/EnvironmentMemory.py +20 -0
  20. modal_decomposition-0.1.0/src/Modal_Decomposition/Utils/LazyImport.py +21 -0
  21. modal_decomposition-0.1.0/src/Modal_Decomposition/Utils/Monotonicity.py +173 -0
  22. modal_decomposition-0.1.0/src/Modal_Decomposition/Utils/NumpyNdarray_MemoryCalculator.py +38 -0
  23. modal_decomposition-0.1.0/src/Modal_Decomposition/Utils/OneDimArray.py +27 -0
  24. modal_decomposition-0.1.0/src/Modal_Decomposition/Utils/__init__.py +30 -0
  25. {modal_decomposition-0.0.4 → modal_decomposition-0.1.0}/src/Modal_Decomposition/VMD.py +2 -0
  26. modal_decomposition-0.1.0/src/Modal_Decomposition/__init__.py +141 -0
  27. {modal_decomposition-0.0.4 → modal_decomposition-0.1.0}/src/Modal_Decomposition.egg-info/PKG-INFO +3 -1
  28. {modal_decomposition-0.0.4 → modal_decomposition-0.1.0}/src/Modal_Decomposition.egg-info/SOURCES.txt +8 -0
  29. {modal_decomposition-0.0.4 → modal_decomposition-0.1.0}/src/Modal_Decomposition.egg-info/requires.txt +1 -0
  30. modal_decomposition-0.1.0/tests/test.py +77 -0
  31. modal_decomposition-0.0.4/src/Modal_Decomposition/CEEFD.py +0 -166
  32. modal_decomposition-0.0.4/src/Modal_Decomposition/CEEMD.py +0 -130
  33. modal_decomposition-0.0.4/src/Modal_Decomposition/EEMD.py +0 -36
  34. modal_decomposition-0.0.4/src/Modal_Decomposition/EFD.py +0 -101
  35. modal_decomposition-0.0.4/src/Modal_Decomposition/FMD.py +0 -70
  36. modal_decomposition-0.0.4/src/Modal_Decomposition/ICEEMDAN.py +0 -166
  37. modal_decomposition-0.0.4/src/Modal_Decomposition/LMD.py +0 -159
  38. modal_decomposition-0.0.4/src/Modal_Decomposition/RPSEMD.py +0 -84
  39. modal_decomposition-0.0.4/src/Modal_Decomposition/SSA.py +0 -113
  40. modal_decomposition-0.0.4/src/Modal_Decomposition/SVMD.py +0 -59
  41. modal_decomposition-0.0.4/src/Modal_Decomposition/__init__.py +0 -86
  42. modal_decomposition-0.0.4/tests/test.py +0 -12
  43. {modal_decomposition-0.0.4 → modal_decomposition-0.1.0}/LICENSE +0 -0
  44. {modal_decomposition-0.0.4 → modal_decomposition-0.1.0}/setup.cfg +0 -0
  45. {modal_decomposition-0.0.4 → modal_decomposition-0.1.0}/src/Modal_Decomposition/COLOR/__init__.py +0 -0
  46. {modal_decomposition-0.0.4 → modal_decomposition-0.1.0}/src/Modal_Decomposition/COLOR/color_define.py +0 -0
  47. {modal_decomposition-0.0.4 → modal_decomposition-0.1.0}/src/Modal_Decomposition/COLOR/colorful_print.py +0 -0
  48. {modal_decomposition-0.0.4 → modal_decomposition-0.1.0}/src/Modal_Decomposition/help_function.py +0 -0
  49. {modal_decomposition-0.0.4 → modal_decomposition-0.1.0}/src/Modal_Decomposition.egg-info/dependency_links.txt +0 -0
  50. {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.4
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.4"
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