complexeva 0.2.1__tar.gz → 0.2.3__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.
- {complexeva-0.2.1 → complexeva-0.2.3}/PKG-INFO +1 -1
- {complexeva-0.2.1 → complexeva-0.2.3}/complexeva/sequence/__init__.py +3 -3
- complexeva-0.2.3/complexeva/sequence/higuchi.py +122 -0
- complexeva-0.2.3/complexeva/sequence/lempel_ziv.py +266 -0
- {complexeva-0.2.1 → complexeva-0.2.3}/complexeva.egg-info/PKG-INFO +1 -1
- {complexeva-0.2.1 → complexeva-0.2.3}/setup.py +1 -1
- complexeva-0.2.1/complexeva/sequence/higuchi.py +0 -39
- complexeva-0.2.1/complexeva/sequence/lempel_ziv.py +0 -76
- {complexeva-0.2.1 → complexeva-0.2.3}/README.md +0 -0
- {complexeva-0.2.1 → complexeva-0.2.3}/complexeva/__init__.py +0 -0
- {complexeva-0.2.1 → complexeva-0.2.3}/complexeva/sequence/compression.py +0 -0
- {complexeva-0.2.1 → complexeva-0.2.3}/complexeva/sequence/lyapunov_exponent.py +0 -0
- {complexeva-0.2.1 → complexeva-0.2.3}/complexeva/validate.py +0 -0
- {complexeva-0.2.1 → complexeva-0.2.3}/complexeva.egg-info/SOURCES.txt +0 -0
- {complexeva-0.2.1 → complexeva-0.2.3}/complexeva.egg-info/dependency_links.txt +0 -0
- {complexeva-0.2.1 → complexeva-0.2.3}/complexeva.egg-info/top_level.txt +0 -0
- {complexeva-0.2.1 → complexeva-0.2.3}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
from .higuchi import hfd
|
|
2
|
-
from .lempel_ziv import lempel_ziv
|
|
1
|
+
from .higuchi import hfd, hfd_hilbert
|
|
2
|
+
from .lempel_ziv import lempel_ziv, lempel_ziv_hilbert
|
|
3
3
|
from .compression import compression_complexity
|
|
4
4
|
from .lyapunov_exponent import lyapunov_exponent
|
|
5
5
|
|
|
6
|
-
__all__ = ["compression_complexity", "lyapunov_exponent", "hfd", "lempel_ziv"]
|
|
6
|
+
__all__ = ["compression_complexity", "lyapunov_exponent", "hfd", "hfd_hilbert", "lempel_ziv", "lempel_ziv_hilbert"]
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
#!/Users/donyin/miniconda3/envs/rotation-1/bin/python
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
import numpy
|
|
5
|
+
from numba import njit
|
|
6
|
+
from scipy.signal import hilbert
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@njit
|
|
10
|
+
def compute_L_x(X, k_max=64):
|
|
11
|
+
k_max = min(k_max, len(X) // 2) if k_max is None else k_max
|
|
12
|
+
N = len(X)
|
|
13
|
+
L = numpy.zeros(k_max)
|
|
14
|
+
x = numpy.zeros(k_max)
|
|
15
|
+
for k in range(1, k_max + 1):
|
|
16
|
+
Lk = numpy.zeros(k)
|
|
17
|
+
for m in range(k):
|
|
18
|
+
Lmk = 0.0
|
|
19
|
+
n_max = (N - m) // k
|
|
20
|
+
for i in range(1, n_max):
|
|
21
|
+
Lmk += numpy.abs(X[m + i * k] - X[m + (i - 1) * k])
|
|
22
|
+
Lmk *= (N - 1) / (n_max * k)
|
|
23
|
+
Lk[m] = Lmk
|
|
24
|
+
L[k - 1] = numpy.log(Lk.mean())
|
|
25
|
+
x[k - 1] = numpy.log(1.0 / k)
|
|
26
|
+
return x, L
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def hfd(X, k_max=64):
|
|
30
|
+
"""expect a 1d numpy array as input"""
|
|
31
|
+
assert X.ndim == 1, f"X must be a 1d numpy array, got shape {X.shape}"
|
|
32
|
+
x, L = compute_L_x(X, k_max)
|
|
33
|
+
A = numpy.column_stack((x, numpy.ones_like(x)))
|
|
34
|
+
beta, _, _, _ = numpy.linalg.lstsq(A, L, rcond=None)
|
|
35
|
+
return beta[0]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def hfd_hilbert_envelope(X, k_max=64):
|
|
39
|
+
"""
|
|
40
|
+
Calculate HFD on the envelope (instantaneous amplitude) of the Hilbert transform.
|
|
41
|
+
This measures complexity of amplitude modulations.
|
|
42
|
+
"""
|
|
43
|
+
assert X.ndim == 1, f"X must be a 1d numpy array, got shape {X.shape}"
|
|
44
|
+
analytic_signal = hilbert(X)
|
|
45
|
+
envelope = numpy.abs(analytic_signal)
|
|
46
|
+
return hfd(envelope, k_max=k_max)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def hfd_hilbert_transform(X, k_max=64):
|
|
50
|
+
"""
|
|
51
|
+
Calculate HFD on the actual Hilbert transform (imaginary part of analytic signal).
|
|
52
|
+
This applies fractal analysis to the 90-degree phase-shifted version of the signal.
|
|
53
|
+
"""
|
|
54
|
+
assert X.ndim == 1, f"X must be a 1d numpy array, got shape {X.shape}"
|
|
55
|
+
analytic_signal = hilbert(X)
|
|
56
|
+
hilbert_transform = numpy.imag(analytic_signal)
|
|
57
|
+
return hfd(hilbert_transform, k_max=k_max)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def hfd_hilbert_frequency(X, k_max=64):
|
|
61
|
+
"""
|
|
62
|
+
Calculate HFD on the instantaneous frequency from Hilbert transform.
|
|
63
|
+
This measures complexity of frequency modulations (your original implementation).
|
|
64
|
+
"""
|
|
65
|
+
assert X.ndim == 1, f"X must be a 1d numpy array, got shape {X.shape}"
|
|
66
|
+
analytic_signal = hilbert(X)
|
|
67
|
+
instantaneous_phase = numpy.angle(analytic_signal)
|
|
68
|
+
instantaneous_freq = numpy.diff(instantaneous_phase)
|
|
69
|
+
instantaneous_freq = numpy.append(instantaneous_freq, instantaneous_freq[-1])
|
|
70
|
+
return hfd(instantaneous_freq, k_max=k_max)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# Keep the original as an alias for backward compatibility
|
|
74
|
+
def hfd_hilbert(X, k_max=64):
|
|
75
|
+
"""
|
|
76
|
+
Calculate Higuchi Fractal Dimension on the Hilbert-transformed BOLD time-series.
|
|
77
|
+
|
|
78
|
+
Based on research literature context, this applies HFD to the envelope
|
|
79
|
+
(instantaneous amplitude) of the Hilbert transform, which captures temporal
|
|
80
|
+
brain activity patterns in the BOLD signal amplitude over time.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
X (numpy.ndarray): Input 1D BOLD time-series
|
|
84
|
+
k_max (int): Maximum k value for HFD calculation
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
float: Higuchi Fractal Dimension of the Hilbert-transformed signal
|
|
88
|
+
"""
|
|
89
|
+
return hfd_hilbert_envelope(X, k_max=k_max)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
if __name__ == "__main__":
|
|
93
|
+
import time
|
|
94
|
+
|
|
95
|
+
signal = numpy.random.rand(1000) # shape: (1000,)
|
|
96
|
+
|
|
97
|
+
# Test plain HFD speed
|
|
98
|
+
start_time = time.time()
|
|
99
|
+
complexity_plain = hfd(signal)
|
|
100
|
+
plain_time = time.time() - start_time
|
|
101
|
+
|
|
102
|
+
# Test Hilbert HFD variants
|
|
103
|
+
start_time = time.time()
|
|
104
|
+
complexity_envelope = hfd_hilbert_envelope(signal)
|
|
105
|
+
envelope_time = time.time() - start_time
|
|
106
|
+
|
|
107
|
+
start_time = time.time()
|
|
108
|
+
complexity_transform = hfd_hilbert_transform(signal)
|
|
109
|
+
transform_time = time.time() - start_time
|
|
110
|
+
|
|
111
|
+
start_time = time.time()
|
|
112
|
+
complexity_frequency = hfd_hilbert_frequency(signal)
|
|
113
|
+
frequency_time = time.time() - start_time
|
|
114
|
+
|
|
115
|
+
print(f"Plain HFD: {complexity_plain:.6f}, Time: {plain_time:.6f}s")
|
|
116
|
+
print(f"Hilbert Envelope HFD: {complexity_envelope:.6f}, Time: {envelope_time:.6f}s")
|
|
117
|
+
print(f"Hilbert Transform HFD: {complexity_transform:.6f}, Time: {transform_time:.6f}s")
|
|
118
|
+
print(f"Hilbert Frequency HFD: {complexity_frequency:.6f}, Time: {frequency_time:.6f}s")
|
|
119
|
+
print(f"\nHilbert/Plain speed ratios:")
|
|
120
|
+
print(f" Envelope: {envelope_time/plain_time:.2f}x")
|
|
121
|
+
print(f" Transform: {transform_time/plain_time:.2f}x")
|
|
122
|
+
print(f" Frequency: {frequency_time/plain_time:.2f}x")
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
#!/Users/donyin/miniconda3/envs/rotation-1/bin/python
|
|
2
|
+
|
|
3
|
+
import numpy
|
|
4
|
+
from scipy.signal import hilbert
|
|
5
|
+
import time
|
|
6
|
+
from numba import njit
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@njit
|
|
10
|
+
def lempel_ziv_core(binary_array):
|
|
11
|
+
"""
|
|
12
|
+
Core Lempel-Ziv complexity algorithm optimized with numba.
|
|
13
|
+
Works on binary arrays of 0s and 1s (integers).
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
binary_array: numpy array of 0s and 1s (integers)
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
int: The number of distinct patterns (complexity measure)
|
|
20
|
+
"""
|
|
21
|
+
history_pos = 0 # position in history we're comparing against
|
|
22
|
+
current_len = 1 # length of current pattern we're checking
|
|
23
|
+
max_pattern_len = 1 # length of longest matching pattern found
|
|
24
|
+
seq_pos = 1 # current position in sequence
|
|
25
|
+
complexity = 1 # start with 1 as first character is always a new pattern
|
|
26
|
+
seq_len = len(binary_array)
|
|
27
|
+
|
|
28
|
+
while seq_pos + current_len <= seq_len:
|
|
29
|
+
if binary_array[history_pos + current_len - 1] == binary_array[seq_pos + current_len - 1]:
|
|
30
|
+
current_len += 1
|
|
31
|
+
else:
|
|
32
|
+
if current_len > max_pattern_len:
|
|
33
|
+
max_pattern_len = current_len
|
|
34
|
+
history_pos += 1
|
|
35
|
+
if history_pos == seq_pos:
|
|
36
|
+
complexity += 1
|
|
37
|
+
seq_pos += max_pattern_len
|
|
38
|
+
history_pos = 0
|
|
39
|
+
current_len = 1
|
|
40
|
+
max_pattern_len = 1
|
|
41
|
+
else:
|
|
42
|
+
current_len = 1
|
|
43
|
+
return complexity
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@njit
|
|
47
|
+
def time_series_to_binary_core(vector, threshold, method_idx):
|
|
48
|
+
"""
|
|
49
|
+
Core binarization function optimized with numba.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
vector: numpy array of numeric values
|
|
53
|
+
threshold: threshold value for binarization
|
|
54
|
+
method_idx: 0=mean, 1=median, 2=diff
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
numpy array of 0s and 1s (integers)
|
|
58
|
+
"""
|
|
59
|
+
if method_idx == 2: # diff method
|
|
60
|
+
diffs = numpy.diff(vector)
|
|
61
|
+
return (diffs > 0).astype(numpy.int32)
|
|
62
|
+
else: # mean or median method
|
|
63
|
+
return (vector > threshold).astype(numpy.int32)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def lempel_ziv(time_series, method="median", **kwargs):
|
|
67
|
+
"""
|
|
68
|
+
- compared to other implementations, this implementation is makes the most sense
|
|
69
|
+
|
|
70
|
+
Lempel-Ziv complexity as described in Kaspar and Schuster, Phys. Rev. A.
|
|
71
|
+
Counts the number of distinct patterns that need to be copied to reproduce the sequence.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
time_series: Either a binary string (e.g., "1001110") or numeric array to be converted
|
|
75
|
+
method: Binarization method if time_series is numeric ('median', 'mean', 'diff')
|
|
76
|
+
|
|
77
|
+
Returns: int: The number of distinct patterns (complexity measure)
|
|
78
|
+
"""
|
|
79
|
+
# Handle binary string input
|
|
80
|
+
if isinstance(time_series, str) and all(c in "01" for c in time_series):
|
|
81
|
+
binary_array = numpy.array([int(c) for c in time_series], dtype=numpy.int32)
|
|
82
|
+
return lempel_ziv_core(binary_array)
|
|
83
|
+
|
|
84
|
+
# Handle numeric input
|
|
85
|
+
vector = numpy.array(time_series, dtype=numpy.float64)
|
|
86
|
+
|
|
87
|
+
if method == "diff":
|
|
88
|
+
binary_array = time_series_to_binary_core(vector, 0.0, 2) # threshold not used for diff
|
|
89
|
+
elif method == "mean":
|
|
90
|
+
threshold = numpy.mean(vector)
|
|
91
|
+
binary_array = time_series_to_binary_core(vector, threshold, 0)
|
|
92
|
+
elif method == "median":
|
|
93
|
+
threshold = numpy.median(vector)
|
|
94
|
+
binary_array = time_series_to_binary_core(vector, threshold, 1)
|
|
95
|
+
else:
|
|
96
|
+
raise ValueError("Method must be 'mean', 'median', or 'diff'.")
|
|
97
|
+
|
|
98
|
+
return lempel_ziv_core(binary_array)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def time_series_to_binary(vector, method="median"):
|
|
102
|
+
"""
|
|
103
|
+
- convert a time series to a binary sequence based on a threshold.
|
|
104
|
+
- returns: str: A binary sequence as a string of '0's and '1's.
|
|
105
|
+
- https://doi.org/10.1101/2021.09.23.461002 this paper used median, making median the default method.
|
|
106
|
+
- diff also makes somewhat sense
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
# If input is already a binary string, return it as-is
|
|
110
|
+
if isinstance(vector, str) and all(c in "01" for c in vector):
|
|
111
|
+
return vector
|
|
112
|
+
|
|
113
|
+
vector = numpy.array(vector)
|
|
114
|
+
|
|
115
|
+
match method:
|
|
116
|
+
|
|
117
|
+
case "mean":
|
|
118
|
+
threshold = numpy.mean(vector)
|
|
119
|
+
binary_seq = "".join(["1" if x > threshold else "0" for x in vector])
|
|
120
|
+
|
|
121
|
+
case "median":
|
|
122
|
+
threshold = numpy.median(vector)
|
|
123
|
+
binary_seq = "".join(["1" if x > threshold else "0" for x in vector])
|
|
124
|
+
|
|
125
|
+
case "diff": # this measures whether the value is increasing or decreasing as compared to the previous value
|
|
126
|
+
diffs = numpy.diff(vector)
|
|
127
|
+
binary_seq = "".join(["1" if x > 0 else "0" for x in diffs])
|
|
128
|
+
|
|
129
|
+
case _:
|
|
130
|
+
raise ValueError("Method must be 'mean', 'median', or 'diff'.")
|
|
131
|
+
|
|
132
|
+
return binary_seq
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def lempel_ziv_hilbert_envelope(time_series, method="median", **kwargs):
|
|
136
|
+
"""
|
|
137
|
+
Calculate Lempel-Ziv complexity on the envelope (instantaneous amplitude) of the Hilbert transform.
|
|
138
|
+
This measures complexity of amplitude modulations.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
time_series (numpy.ndarray): Input 1D signal
|
|
142
|
+
method (str): Binarization method ('median', 'mean', 'diff')
|
|
143
|
+
**kwargs: Additional arguments
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
int: Lempel-Ziv complexity of the Hilbert-transformed signal
|
|
147
|
+
"""
|
|
148
|
+
time_series = numpy.array(time_series)
|
|
149
|
+
assert time_series.ndim == 1, f"time_series must be a 1d numpy array, got shape {time_series.shape}"
|
|
150
|
+
analytic_signal = hilbert(time_series)
|
|
151
|
+
envelope = numpy.abs(analytic_signal)
|
|
152
|
+
return lempel_ziv(envelope, method=method, **kwargs)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def lempel_ziv_hilbert_transform(time_series, method="median", **kwargs):
|
|
156
|
+
"""
|
|
157
|
+
Calculate Lempel-Ziv complexity on the actual Hilbert transform (imaginary part of analytic signal).
|
|
158
|
+
This applies complexity analysis to the 90-degree phase-shifted version of the signal.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
time_series (numpy.ndarray): Input 1D signal
|
|
162
|
+
method (str): Binarization method ('median', 'mean', 'diff')
|
|
163
|
+
**kwargs: Additional arguments
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
int: Lempel-Ziv complexity of the Hilbert transform
|
|
167
|
+
"""
|
|
168
|
+
time_series = numpy.array(time_series)
|
|
169
|
+
assert time_series.ndim == 1, f"time_series must be a 1d numpy array, got shape {time_series.shape}"
|
|
170
|
+
analytic_signal = hilbert(time_series)
|
|
171
|
+
hilbert_transform = numpy.imag(analytic_signal)
|
|
172
|
+
return lempel_ziv(hilbert_transform, method=method, **kwargs)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def lempel_ziv_hilbert_frequency(time_series, method="median", **kwargs):
|
|
176
|
+
"""
|
|
177
|
+
Calculate Lempel-Ziv complexity on the instantaneous frequency from Hilbert transform.
|
|
178
|
+
This measures complexity of frequency modulations.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
time_series (numpy.ndarray): Input 1D signal
|
|
182
|
+
method (str): Binarization method ('median', 'mean', 'diff')
|
|
183
|
+
**kwargs: Additional arguments
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
int: Lempel-Ziv complexity of the instantaneous frequency
|
|
187
|
+
"""
|
|
188
|
+
time_series = numpy.array(time_series)
|
|
189
|
+
assert time_series.ndim == 1, f"time_series must be a 1d numpy array, got shape {time_series.shape}"
|
|
190
|
+
analytic_signal = hilbert(time_series)
|
|
191
|
+
instantaneous_phase = numpy.angle(analytic_signal)
|
|
192
|
+
instantaneous_freq = numpy.diff(instantaneous_phase)
|
|
193
|
+
instantaneous_freq = numpy.append(instantaneous_freq, instantaneous_freq[-1])
|
|
194
|
+
return lempel_ziv(instantaneous_freq, method=method, **kwargs)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def lempel_ziv_hilbert(time_series, method="median", **kwargs):
|
|
198
|
+
"""
|
|
199
|
+
Calculate Lempel-Ziv complexity on the Hilbert-transformed BOLD time-series.
|
|
200
|
+
|
|
201
|
+
Based on research literature context, this applies LZ complexity to the envelope
|
|
202
|
+
(instantaneous amplitude) of the Hilbert transform, which captures temporal
|
|
203
|
+
brain activity patterns in the BOLD signal amplitude over time.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
time_series (numpy.ndarray): Input 1D BOLD time-series
|
|
207
|
+
method (str): Binarization method ('median', 'mean', 'diff')
|
|
208
|
+
**kwargs: Additional arguments
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
int: Lempel-Ziv complexity of the Hilbert-transformed signal
|
|
212
|
+
"""
|
|
213
|
+
return lempel_ziv_hilbert_envelope(time_series, method=method, **kwargs)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
if __name__ == "__main__":
|
|
217
|
+
# Test with synthetic signal
|
|
218
|
+
signal = numpy.random.rand(1000)
|
|
219
|
+
|
|
220
|
+
print("Testing Lempel-Ziv complexity methods:")
|
|
221
|
+
print("=" * 50)
|
|
222
|
+
|
|
223
|
+
# Test plain LZ
|
|
224
|
+
start_time = time.time()
|
|
225
|
+
lz_plain = lempel_ziv(signal, method="median")
|
|
226
|
+
plain_time = time.time() - start_time
|
|
227
|
+
|
|
228
|
+
# Test Hilbert LZ variants
|
|
229
|
+
start_time = time.time()
|
|
230
|
+
lz_envelope = lempel_ziv_hilbert_envelope(signal, method="median")
|
|
231
|
+
envelope_time = time.time() - start_time
|
|
232
|
+
|
|
233
|
+
start_time = time.time()
|
|
234
|
+
lz_transform = lempel_ziv_hilbert_transform(signal, method="median")
|
|
235
|
+
transform_time = time.time() - start_time
|
|
236
|
+
|
|
237
|
+
start_time = time.time()
|
|
238
|
+
lz_frequency = lempel_ziv_hilbert_frequency(signal, method="median")
|
|
239
|
+
frequency_time = time.time() - start_time
|
|
240
|
+
|
|
241
|
+
print(f"Plain LZ: {lz_plain}, Time: {plain_time:.6f}s")
|
|
242
|
+
print(f"Hilbert Envelope LZ: {lz_envelope}, Time: {envelope_time:.6f}s")
|
|
243
|
+
print(f"Hilbert Transform LZ: {lz_transform}, Time: {transform_time:.6f}s")
|
|
244
|
+
print(f"Hilbert Frequency LZ: {lz_frequency}, Time: {frequency_time:.6f}s")
|
|
245
|
+
print(f"\nHilbert/Plain speed ratios:")
|
|
246
|
+
print(f" Envelope: {envelope_time/plain_time:.2f}x")
|
|
247
|
+
print(f" Transform: {transform_time/plain_time:.2f}x")
|
|
248
|
+
print(f" Frequency: {frequency_time/plain_time:.2f}x")
|
|
249
|
+
|
|
250
|
+
# Test original functionality
|
|
251
|
+
print(f"\nOriginal test:")
|
|
252
|
+
sequence = "1001111011000010"
|
|
253
|
+
print(f"LZ of binary sequence '{sequence}': {lempel_ziv(sequence)}")
|
|
254
|
+
|
|
255
|
+
# Additional benchmark: test the same signal multiple times to see compiled performance
|
|
256
|
+
print(f"\nPerformance after compilation (running same signal 5 times):")
|
|
257
|
+
times = []
|
|
258
|
+
for i in range(5):
|
|
259
|
+
start_time = time.time()
|
|
260
|
+
lz_result = lempel_ziv(signal, method="median")
|
|
261
|
+
elapsed = time.time() - start_time
|
|
262
|
+
times.append(elapsed)
|
|
263
|
+
print(f" Run {i+1}: {lz_result}, Time: {elapsed:.6f}s")
|
|
264
|
+
|
|
265
|
+
print(f"Average compiled time: {numpy.mean(times):.6f}s")
|
|
266
|
+
print(f"Min compiled time: {numpy.min(times):.6f}s")
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
#!/Users/donyin/miniconda3/envs/rotation-1/bin/python
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import numpy
|
|
5
|
-
from numba import njit
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
@njit
|
|
9
|
-
def compute_L_x(X, k_max=16):
|
|
10
|
-
N = len(X)
|
|
11
|
-
L = numpy.zeros(k_max)
|
|
12
|
-
x = numpy.zeros(k_max)
|
|
13
|
-
for k in range(1, k_max + 1):
|
|
14
|
-
Lk = numpy.zeros(k)
|
|
15
|
-
for m in range(k):
|
|
16
|
-
Lmk = 0.0
|
|
17
|
-
n_max = (N - m) // k
|
|
18
|
-
for i in range(1, n_max):
|
|
19
|
-
Lmk += numpy.abs(X[m + i * k] - X[m + (i - 1) * k])
|
|
20
|
-
Lmk *= (N - 1) / (n_max * k)
|
|
21
|
-
Lk[m] = Lmk
|
|
22
|
-
L[k - 1] = numpy.log(Lk.mean())
|
|
23
|
-
x[k - 1] = numpy.log(1.0 / k)
|
|
24
|
-
return x, L
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def hfd(X, k_max=16):
|
|
28
|
-
"""expect a 1d numpy array as input"""
|
|
29
|
-
assert X.ndim == 1, f"X must be a 1d numpy array, got shape {X.shape}"
|
|
30
|
-
x, L = compute_L_x(X, k_max)
|
|
31
|
-
A = numpy.column_stack((x, numpy.ones_like(x)))
|
|
32
|
-
beta, _, _, _ = numpy.linalg.lstsq(A, L, rcond=None)
|
|
33
|
-
return beta[0]
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
if __name__ == "__main__":
|
|
37
|
-
signal = numpy.random.rand(1000) # shape: (1000,)
|
|
38
|
-
complexity = hfd(signal)
|
|
39
|
-
print(complexity)
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
#!/Users/donyin/miniconda3/envs/rotation-1/bin/python
|
|
2
|
-
|
|
3
|
-
import numpy
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
def lempel_ziv(time_series, method="median", **kwargs):
|
|
7
|
-
"""
|
|
8
|
-
- compared to other implementations, this implementation is makes the most sense
|
|
9
|
-
|
|
10
|
-
Lempel-Ziv complexity as described in Kaspar and Schuster, Phys. Rev. A.
|
|
11
|
-
Counts the number of distinct patterns that need to be copied to reproduce the sequence.
|
|
12
|
-
Returns: int: The number of distinct patterns (complexity measure)
|
|
13
|
-
"""
|
|
14
|
-
sequence = time_series_to_binary(time_series, method=method)
|
|
15
|
-
|
|
16
|
-
history_pos = 0 # position in history we're comparing against
|
|
17
|
-
current_len = 1 # length of current pattern we're checking
|
|
18
|
-
max_pattern_len = 1 # length of longest matching pattern found
|
|
19
|
-
seq_pos = 1 # current position in sequence
|
|
20
|
-
complexity = 1 # start with 1 as first character is always a new pattern
|
|
21
|
-
seq_len = len(sequence)
|
|
22
|
-
|
|
23
|
-
while seq_pos + current_len <= seq_len:
|
|
24
|
-
if sequence[history_pos + current_len - 1] == sequence[seq_pos + current_len - 1]:
|
|
25
|
-
current_len += 1
|
|
26
|
-
else:
|
|
27
|
-
if current_len > max_pattern_len:
|
|
28
|
-
max_pattern_len = current_len
|
|
29
|
-
history_pos += 1
|
|
30
|
-
if history_pos == seq_pos:
|
|
31
|
-
complexity += 1
|
|
32
|
-
seq_pos += max_pattern_len
|
|
33
|
-
history_pos = 0
|
|
34
|
-
current_len = 1
|
|
35
|
-
max_pattern_len = 1
|
|
36
|
-
else:
|
|
37
|
-
current_len = 1
|
|
38
|
-
return complexity
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def time_series_to_binary(vector, method="median"):
|
|
42
|
-
"""
|
|
43
|
-
- convert a time series to a binary sequence based on a threshold.
|
|
44
|
-
- returns: str: A binary sequence as a string of '0's and '1's.
|
|
45
|
-
- https://doi.org/10.1101/2021.09.23.461002 this paper used median, making median the default method.
|
|
46
|
-
- diff also makes somewhat sense
|
|
47
|
-
"""
|
|
48
|
-
|
|
49
|
-
vector = numpy.array(vector)
|
|
50
|
-
|
|
51
|
-
match method:
|
|
52
|
-
|
|
53
|
-
case "mean":
|
|
54
|
-
threshold = numpy.mean(vector)
|
|
55
|
-
binary_seq = "".join(["1" if x > threshold else "0" for x in vector])
|
|
56
|
-
|
|
57
|
-
case "median":
|
|
58
|
-
threshold = numpy.median(vector)
|
|
59
|
-
binary_seq = "".join(["1" if x > threshold else "0" for x in vector])
|
|
60
|
-
|
|
61
|
-
case "diff": # this measures whether the value is increasing or decreasing as compared to the previous value
|
|
62
|
-
diffs = numpy.diff(vector)
|
|
63
|
-
binary_seq = "".join(["1" if x > 0 else "0" for x in diffs])
|
|
64
|
-
|
|
65
|
-
case _:
|
|
66
|
-
raise ValueError("Method must be 'mean', 'median', or 'diff'.")
|
|
67
|
-
|
|
68
|
-
return binary_seq
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if __name__ == "__main__":
|
|
72
|
-
sequence = "1001111011000010"
|
|
73
|
-
print(lempel_ziv(sequence))
|
|
74
|
-
signal = numpy.random.rand(1000)
|
|
75
|
-
binary_sequence = time_series_to_binary(signal, method="median")
|
|
76
|
-
print(lempel_ziv(binary_sequence))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|