complexeva 0.2.2__tar.gz → 0.3.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.
- complexeva-0.3.0/.gitignore +12 -0
- complexeva-0.3.0/CHANGELOG.md +14 -0
- complexeva-0.3.0/LICENSE +21 -0
- complexeva-0.3.0/PKG-INFO +63 -0
- complexeva-0.3.0/README.md +42 -0
- complexeva-0.3.0/complexeva/__init__.py +6 -0
- complexeva-0.3.0/complexeva/compression.py +9 -0
- complexeva-0.3.0/complexeva/higuchi.py +40 -0
- complexeva-0.3.0/complexeva/lempel_ziv.py +77 -0
- complexeva-0.3.0/complexeva/lyapunov_exponent.py +74 -0
- complexeva-0.3.0/pyproject.toml +36 -0
- complexeva-0.3.0/tests/test_higuchi.py +47 -0
- complexeva-0.3.0/tests/test_lempel_ziv.py +80 -0
- complexeva-0.3.0/tests/test_lyapunov.py +49 -0
- complexeva-0.2.2/PKG-INFO +0 -28
- complexeva-0.2.2/README.md +0 -16
- complexeva-0.2.2/complexeva/__init__.py +0 -0
- complexeva-0.2.2/complexeva/sequence/__init__.py +0 -6
- complexeva-0.2.2/complexeva/sequence/compression.py +0 -18
- complexeva-0.2.2/complexeva/sequence/higuchi.py +0 -75
- complexeva-0.2.2/complexeva/sequence/lempel_ziv.py +0 -76
- complexeva-0.2.2/complexeva/sequence/lyapunov_exponent.py +0 -32
- complexeva-0.2.2/complexeva/validate.py +0 -117
- complexeva-0.2.2/complexeva.egg-info/PKG-INFO +0 -28
- complexeva-0.2.2/complexeva.egg-info/SOURCES.txt +0 -13
- complexeva-0.2.2/complexeva.egg-info/dependency_links.txt +0 -1
- complexeva-0.2.2/complexeva.egg-info/top_level.txt +0 -1
- complexeva-0.2.2/setup.cfg +0 -4
- complexeva-0.2.2/setup.py +0 -15
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# changelog
|
|
2
|
+
|
|
3
|
+
## 0.3.0 (unreleased)
|
|
4
|
+
|
|
5
|
+
Numerical results of three measures change. To reproduce earlier results, pin `complexeva==0.2.3`.
|
|
6
|
+
|
|
7
|
+
- Breaking: measures are imported from the top level (`from complexeva import hfd`); `complexeva.sequence` is removed.
|
|
8
|
+
- Breaking: removed unexported helpers (`hfd_hilbert_envelope/transform/frequency`, `lempel_ziv_hilbert_envelope/transform/frequency`, `time_series_to_binary`) and the unused `**kwargs` of `lempel_ziv`.
|
|
9
|
+
|
|
10
|
+
- `hfd`, `hfd_hilbert`: default `k_max` is now 10 (was 64), which gives lower error and better discrimination on series of a few hundred points.
|
|
11
|
+
- `hfd`: raises `ValueError` when `k_max` is outside 2 to len(X) // 2 (previously `ZeroDivisionError` or NaN on short series).
|
|
12
|
+
- `hfd`: now returns the standard Higuchi fractal dimension D; 0.2.3 returned D - 1 (missing `/k` in L_m(k)) and skipped the last increment of each subsequence.
|
|
13
|
+
- `lempel_ziv`: now counts the final unfinished component as in Kaspar & Schuster (1987); 0.2.3 undercounted by 1 on most inputs. New `normalize=True` option returns c*log2(n)/n.
|
|
14
|
+
- `lyapunov_exponent`: rewritten as the Rosenstein et al. (1993) largest Lyapunov exponent with a Theiler window; 0.2.3 averaged one-step log distances and depended on signal amplitude. New options `min_tsep`, `trajectory_len`, `fit_offset`.
|
complexeva-0.3.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024-2026 Don Yin
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: complexeva
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Complexity measures for 1D time series: Higuchi fractal dimension, Lempel-Ziv, compression, Lyapunov exponent
|
|
5
|
+
Project-URL: Homepage, https://github.com/Don-Yin/complexeva
|
|
6
|
+
Project-URL: Changelog, https://github.com/Don-Yin/complexeva/blob/main/CHANGELOG.md
|
|
7
|
+
Author-email: Don Yin <don_yin@outlook.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Intended Audience :: Science/Research
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: Scientific/Engineering
|
|
14
|
+
Requires-Python: >=3.14
|
|
15
|
+
Requires-Dist: numba
|
|
16
|
+
Requires-Dist: numpy
|
|
17
|
+
Requires-Dist: scipy
|
|
18
|
+
Provides-Extra: test
|
|
19
|
+
Requires-Dist: pytest; extra == 'test'
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# complexeva
|
|
23
|
+
|
|
24
|
+
Complexity measures for 1D time series, implemented with NumPy, SciPy and Numba.
|
|
25
|
+
|
|
26
|
+
## install
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install complexeva
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Requires Python 3.14 or later.
|
|
33
|
+
|
|
34
|
+
## example
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
import numpy as np
|
|
38
|
+
import complexeva
|
|
39
|
+
|
|
40
|
+
x = np.random.default_rng(0).standard_normal(2000) # white noise
|
|
41
|
+
|
|
42
|
+
complexeva.hfd(x) # about 2.0
|
|
43
|
+
complexeva.lempel_ziv(x, normalize=True) # about 1.0
|
|
44
|
+
complexeva.compression_complexity(x) # about 0.96
|
|
45
|
+
complexeva.lyapunov_exponent(x) # largest Lyapunov exponent
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## measures
|
|
49
|
+
|
|
50
|
+
| function | what it returns | main parameters | reference |
|
|
51
|
+
|---|---|---|---|
|
|
52
|
+
| `hfd(X, k_max=10)` | Higuchi fractal dimension, 1 (smooth) to 2 (white noise) | `k_max`: largest time interval, 2 to len(X) // 2; report it with results | Higuchi 1988, Physica D 31:277 |
|
|
53
|
+
| `hfd_hilbert(X, k_max=10)` | `hfd` of the Hilbert envelope (instantaneous amplitude) | as `hfd` | |
|
|
54
|
+
| `lempel_ziv(x, method="median", normalize=False)` | Lempel-Ziv complexity: integer count, or c·log2(n)/n (about 1 for random input) when `normalize=True` | `method`: binarise by `"median"`, `"mean"` or `"diff"` (rising step); a `"0101..."` string is used as is | Kaspar & Schuster 1987, Phys. Rev. A 36:842 |
|
|
55
|
+
| `lempel_ziv_hilbert(x, method="median", normalize=False)` | `lempel_ziv` of the Hilbert envelope | as `lempel_ziv` | |
|
|
56
|
+
| `compression_complexity(data)` | zlib-compressed size / raw size of the array bytes, 0 to about 1 | depends on the array dtype | |
|
|
57
|
+
| `lyapunov_exponent(data, tau=1, m=2, dt=1.0, min_tsep=None, trajectory_len=20, fit_offset=0)` | largest Lyapunov exponent in units of 1/`dt`; positive suggests chaos, about 0 for periodic signals | `tau`, `m`: delay embedding; `min_tsep`: Theiler window (default: mean period); `trajectory_len`: steps fitted | Rosenstein et al. 1993, Physica D 65:117 |
|
|
58
|
+
|
|
59
|
+
All functions take a 1D array. Results changed in 0.3.0; see [CHANGELOG.md](CHANGELOG.md).
|
|
60
|
+
|
|
61
|
+
## licence
|
|
62
|
+
|
|
63
|
+
MIT
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# complexeva
|
|
2
|
+
|
|
3
|
+
Complexity measures for 1D time series, implemented with NumPy, SciPy and Numba.
|
|
4
|
+
|
|
5
|
+
## install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install complexeva
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Requires Python 3.14 or later.
|
|
12
|
+
|
|
13
|
+
## example
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
import numpy as np
|
|
17
|
+
import complexeva
|
|
18
|
+
|
|
19
|
+
x = np.random.default_rng(0).standard_normal(2000) # white noise
|
|
20
|
+
|
|
21
|
+
complexeva.hfd(x) # about 2.0
|
|
22
|
+
complexeva.lempel_ziv(x, normalize=True) # about 1.0
|
|
23
|
+
complexeva.compression_complexity(x) # about 0.96
|
|
24
|
+
complexeva.lyapunov_exponent(x) # largest Lyapunov exponent
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## measures
|
|
28
|
+
|
|
29
|
+
| function | what it returns | main parameters | reference |
|
|
30
|
+
|---|---|---|---|
|
|
31
|
+
| `hfd(X, k_max=10)` | Higuchi fractal dimension, 1 (smooth) to 2 (white noise) | `k_max`: largest time interval, 2 to len(X) // 2; report it with results | Higuchi 1988, Physica D 31:277 |
|
|
32
|
+
| `hfd_hilbert(X, k_max=10)` | `hfd` of the Hilbert envelope (instantaneous amplitude) | as `hfd` | |
|
|
33
|
+
| `lempel_ziv(x, method="median", normalize=False)` | Lempel-Ziv complexity: integer count, or c·log2(n)/n (about 1 for random input) when `normalize=True` | `method`: binarise by `"median"`, `"mean"` or `"diff"` (rising step); a `"0101..."` string is used as is | Kaspar & Schuster 1987, Phys. Rev. A 36:842 |
|
|
34
|
+
| `lempel_ziv_hilbert(x, method="median", normalize=False)` | `lempel_ziv` of the Hilbert envelope | as `lempel_ziv` | |
|
|
35
|
+
| `compression_complexity(data)` | zlib-compressed size / raw size of the array bytes, 0 to about 1 | depends on the array dtype | |
|
|
36
|
+
| `lyapunov_exponent(data, tau=1, m=2, dt=1.0, min_tsep=None, trajectory_len=20, fit_offset=0)` | largest Lyapunov exponent in units of 1/`dt`; positive suggests chaos, about 0 for periodic signals | `tau`, `m`: delay embedding; `min_tsep`: Theiler window (default: mean period); `trajectory_len`: steps fitted | Rosenstein et al. 1993, Physica D 65:117 |
|
|
37
|
+
|
|
38
|
+
All functions take a 1D array. Results changed in 0.3.0; see [CHANGELOG.md](CHANGELOG.md).
|
|
39
|
+
|
|
40
|
+
## licence
|
|
41
|
+
|
|
42
|
+
MIT
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
from .compression import compression_complexity
|
|
2
|
+
from .higuchi import hfd, hfd_hilbert
|
|
3
|
+
from .lempel_ziv import lempel_ziv, lempel_ziv_hilbert
|
|
4
|
+
from .lyapunov_exponent import lyapunov_exponent
|
|
5
|
+
|
|
6
|
+
__all__ = ["compression_complexity", "hfd", "hfd_hilbert", "lempel_ziv", "lempel_ziv_hilbert", "lyapunov_exponent"]
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import numpy
|
|
2
|
+
from numba import njit
|
|
3
|
+
from scipy.signal import hilbert
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@njit
|
|
7
|
+
def compute_L_x(X, k_max):
|
|
8
|
+
N = len(X)
|
|
9
|
+
L = numpy.zeros(k_max)
|
|
10
|
+
x = numpy.zeros(k_max)
|
|
11
|
+
for k in range(1, k_max + 1):
|
|
12
|
+
Lk = numpy.zeros(k)
|
|
13
|
+
for m in range(k):
|
|
14
|
+
# Higuchi (1988): M = floor((N - m) / k) increments with 1-based m, i.e. (N - 1 - m) // k here
|
|
15
|
+
Lmk = 0.0
|
|
16
|
+
n_max = (N - 1 - m) // k
|
|
17
|
+
for i in range(1, n_max + 1):
|
|
18
|
+
Lmk += numpy.abs(X[m + i * k] - X[m + (i - 1) * k])
|
|
19
|
+
Lmk *= (N - 1) / (n_max * k) # normalisation factor
|
|
20
|
+
Lk[m] = Lmk / k # curve length L_m(k); without /k the slope is D - 1
|
|
21
|
+
L[k - 1] = numpy.log(Lk.mean())
|
|
22
|
+
x[k - 1] = numpy.log(1.0 / k)
|
|
23
|
+
return x, L
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def hfd(X, k_max=10):
|
|
27
|
+
"""Higuchi fractal dimension of a 1D numpy array."""
|
|
28
|
+
assert X.ndim == 1, f"X must be a 1d numpy array, got shape {X.shape}"
|
|
29
|
+
if not 2 <= k_max <= len(X) // 2:
|
|
30
|
+
raise ValueError(f"k_max must be between 2 and len(X) // 2 = {len(X) // 2}, got {k_max}")
|
|
31
|
+
x, L = compute_L_x(X, k_max)
|
|
32
|
+
A = numpy.column_stack((x, numpy.ones_like(x)))
|
|
33
|
+
beta, _, _, _ = numpy.linalg.lstsq(A, L, rcond=None)
|
|
34
|
+
return beta[0]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def hfd_hilbert(X, k_max=10):
|
|
38
|
+
"""Higuchi fractal dimension of the Hilbert envelope (instantaneous amplitude) of X."""
|
|
39
|
+
assert X.ndim == 1, f"X must be a 1d numpy array, got shape {X.shape}"
|
|
40
|
+
return hfd(numpy.abs(numpy.asarray(hilbert(X))), k_max=k_max)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import numpy
|
|
2
|
+
from numba import njit
|
|
3
|
+
from scipy.signal import hilbert
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@njit
|
|
7
|
+
def lempel_ziv_core(binary_array):
|
|
8
|
+
"""Kaspar-Schuster count of components needed to reproduce a 0/1 integer array."""
|
|
9
|
+
history_pos = 0 # position in history we're comparing against
|
|
10
|
+
current_len = 1 # length of current pattern we're checking
|
|
11
|
+
max_pattern_len = 1 # length of longest matching pattern found
|
|
12
|
+
seq_pos = 1 # current position in sequence
|
|
13
|
+
complexity = 1 # start with 1 as first character is always a new pattern
|
|
14
|
+
seq_len = len(binary_array)
|
|
15
|
+
|
|
16
|
+
while seq_pos + current_len <= seq_len:
|
|
17
|
+
if binary_array[history_pos + current_len - 1] == binary_array[seq_pos + current_len - 1]:
|
|
18
|
+
current_len += 1
|
|
19
|
+
else:
|
|
20
|
+
max_pattern_len = max(max_pattern_len, current_len)
|
|
21
|
+
history_pos += 1
|
|
22
|
+
if history_pos == seq_pos:
|
|
23
|
+
complexity += 1
|
|
24
|
+
seq_pos += max_pattern_len
|
|
25
|
+
history_pos = 0
|
|
26
|
+
current_len = 1
|
|
27
|
+
max_pattern_len = 1
|
|
28
|
+
else:
|
|
29
|
+
current_len = 1
|
|
30
|
+
if current_len > 1: # sequence ended inside a copied component; Kaspar-Schuster counts it
|
|
31
|
+
complexity += 1
|
|
32
|
+
return complexity
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _binarize(time_series, method):
|
|
36
|
+
"""0/1 array: above median or mean, or increasing step for 'diff' (median follows doi:10.1101/2021.09.23.461002)."""
|
|
37
|
+
vector = numpy.asarray(time_series, dtype=numpy.float64)
|
|
38
|
+
if method == "diff":
|
|
39
|
+
bits = numpy.diff(vector) > 0
|
|
40
|
+
elif method == "mean":
|
|
41
|
+
bits = vector > numpy.mean(vector)
|
|
42
|
+
elif method == "median":
|
|
43
|
+
bits = vector > numpy.median(vector)
|
|
44
|
+
else:
|
|
45
|
+
raise ValueError("Method must be 'mean', 'median', or 'diff'.")
|
|
46
|
+
return bits.astype(numpy.int32)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def lempel_ziv(time_series, method="median", normalize=False):
|
|
50
|
+
"""
|
|
51
|
+
Lempel-Ziv complexity as described in Kaspar and Schuster (1987), Phys. Rev. A.
|
|
52
|
+
Counts the number of distinct patterns that need to be copied to reproduce the sequence.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
time_series: Either a binary string (e.g., "1001110") or numeric array to be binarised
|
|
56
|
+
method: Binarization method if time_series is numeric ('median', 'mean', 'diff')
|
|
57
|
+
normalize: If True, return c * log2(n) / n for the binary sequence of length n (about 1 for random input)
|
|
58
|
+
|
|
59
|
+
Returns: int (raw count) or float (normalized)
|
|
60
|
+
"""
|
|
61
|
+
if isinstance(time_series, str) and all(c in "01" for c in time_series):
|
|
62
|
+
binary_array = numpy.array([int(c) for c in time_series], dtype=numpy.int32)
|
|
63
|
+
else:
|
|
64
|
+
binary_array = _binarize(time_series, method)
|
|
65
|
+
complexity = int(lempel_ziv_core(binary_array))
|
|
66
|
+
if not normalize:
|
|
67
|
+
return complexity
|
|
68
|
+
n = len(binary_array)
|
|
69
|
+
return float(complexity * numpy.log2(n) / n) if n > 1 else 0.0
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def lempel_ziv_hilbert(time_series, method="median", normalize=False):
|
|
73
|
+
"""Lempel-Ziv complexity of the Hilbert envelope (instantaneous amplitude) of a 1D signal."""
|
|
74
|
+
time_series = numpy.asarray(time_series)
|
|
75
|
+
assert time_series.ndim == 1, f"time_series must be a 1d numpy array, got shape {time_series.shape}"
|
|
76
|
+
envelope = numpy.abs(numpy.asarray(hilbert(time_series)))
|
|
77
|
+
return lempel_ziv(envelope, method=method, normalize=normalize)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from numba import njit
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@njit(cache=True)
|
|
6
|
+
def _divergence(Y, min_tsep, trajectory_len):
|
|
7
|
+
"""mean log distance between each orbit point and its nearest neighbour after k steps (Rosenstein 1993)"""
|
|
8
|
+
n_traj = Y.shape[0] - trajectory_len + 1
|
|
9
|
+
neighbour = np.empty(n_traj, dtype=np.int64)
|
|
10
|
+
for i in range(n_traj):
|
|
11
|
+
best, best_j = np.inf, -1
|
|
12
|
+
for j in range(n_traj):
|
|
13
|
+
if abs(i - j) <= min_tsep: # Theiler window: skip temporally close points
|
|
14
|
+
continue
|
|
15
|
+
d = np.sum((Y[i] - Y[j]) ** 2)
|
|
16
|
+
if d < best:
|
|
17
|
+
best, best_j = d, j
|
|
18
|
+
neighbour[i] = best_j
|
|
19
|
+
log_div = np.full(trajectory_len, -np.inf)
|
|
20
|
+
for k in range(trajectory_len):
|
|
21
|
+
total, count = 0.0, 0
|
|
22
|
+
for i in range(n_traj):
|
|
23
|
+
d = np.sqrt(np.sum((Y[i + k] - Y[neighbour[i] + k]) ** 2))
|
|
24
|
+
if d > 0:
|
|
25
|
+
total += np.log(d)
|
|
26
|
+
count += 1
|
|
27
|
+
if count > 0:
|
|
28
|
+
log_div[k] = total / count
|
|
29
|
+
return log_div
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _mean_period(x):
|
|
33
|
+
"""1 / power-weighted mean frequency, the default Theiler window"""
|
|
34
|
+
psd = np.abs(np.fft.rfft(x - x.mean())) ** 2
|
|
35
|
+
freqs = np.fft.rfftfreq(len(x))
|
|
36
|
+
return int(np.ceil(np.sum(psd[1:]) / np.sum(freqs[1:] * psd[1:])))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def lyapunov_exponent(data, tau=1, m=2, dt=1.0, min_tsep=None, trajectory_len=20, fit_offset=0):
|
|
40
|
+
"""
|
|
41
|
+
Largest Lyapunov exponent by the Rosenstein et al. (1993) method.
|
|
42
|
+
|
|
43
|
+
The series is delay-embedded, each point is paired with its nearest neighbour outside a Theiler window,
|
|
44
|
+
and the exponent is the slope of the mean log divergence of those pairs over `trajectory_len` steps.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
data: 1D array-like time series
|
|
48
|
+
tau: embedding delay in samples
|
|
49
|
+
m: embedding dimension
|
|
50
|
+
dt: sampling interval; the result is in units of 1/dt
|
|
51
|
+
min_tsep: Theiler window in samples; defaults to the mean period of the signal
|
|
52
|
+
trajectory_len: number of steps over which divergence is followed and fitted
|
|
53
|
+
fit_offset: number of initial steps excluded from the line fit
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
float: largest Lyapunov exponent (positive suggests chaos, about 0 or below for regular signals)
|
|
57
|
+
"""
|
|
58
|
+
x = np.asarray(data, dtype=np.float64)
|
|
59
|
+
if x.ndim != 1:
|
|
60
|
+
raise ValueError(f"data must be 1D, got shape {x.shape}")
|
|
61
|
+
if min_tsep is None:
|
|
62
|
+
min_tsep = min(_mean_period(x), len(x) // 4)
|
|
63
|
+
n_vectors = len(x) - (m - 1) * tau
|
|
64
|
+
n_traj = n_vectors - trajectory_len + 1
|
|
65
|
+
if n_traj < 2 * min_tsep + 2:
|
|
66
|
+
raise ValueError(f"series too short: {n_traj} trajectories for min_tsep={min_tsep}")
|
|
67
|
+
Y = np.stack([x[i * tau : i * tau + n_vectors] for i in range(m)], axis=1)
|
|
68
|
+
log_div = _divergence(Y, min_tsep, trajectory_len)
|
|
69
|
+
ks = np.arange(trajectory_len)[fit_offset:]
|
|
70
|
+
log_div = log_div[fit_offset:]
|
|
71
|
+
finite = np.isfinite(log_div)
|
|
72
|
+
if finite.sum() < 2:
|
|
73
|
+
return -np.inf
|
|
74
|
+
return float(np.polyfit(ks[finite], log_div[finite], 1)[0] / dt)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "complexeva"
|
|
7
|
+
version = "0.3.0"
|
|
8
|
+
description = "Complexity measures for 1D time series: Higuchi fractal dimension, Lempel-Ziv, compression, Lyapunov exponent"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
authors = [{ name = "Don Yin", email = "don_yin@outlook.com" }]
|
|
13
|
+
requires-python = ">=3.14"
|
|
14
|
+
dependencies = ["numpy", "scipy", "numba"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
"Intended Audience :: Science/Research",
|
|
19
|
+
"Topic :: Scientific/Engineering",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[project.optional-dependencies]
|
|
23
|
+
test = ["pytest"]
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Homepage = "https://github.com/Don-Yin/complexeva"
|
|
27
|
+
Changelog = "https://github.com/Don-Yin/complexeva/blob/main/CHANGELOG.md"
|
|
28
|
+
|
|
29
|
+
[tool.hatch.build.targets.sdist]
|
|
30
|
+
include = ["/complexeva", "/tests", "/README.md", "/CHANGELOG.md", "/LICENSE"]
|
|
31
|
+
|
|
32
|
+
[tool.pytest.ini_options]
|
|
33
|
+
testpaths = ["tests"]
|
|
34
|
+
|
|
35
|
+
[tool.ruff]
|
|
36
|
+
line-length = 128
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import numpy
|
|
2
|
+
import pytest
|
|
3
|
+
|
|
4
|
+
from complexeva import hfd
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def reference_hfd(x, k_max):
|
|
8
|
+
"""Literal Higuchi (1988) formula with 1-based m, written for clarity rather than speed."""
|
|
9
|
+
N = len(x)
|
|
10
|
+
log_inv_k, log_L = [], []
|
|
11
|
+
for k in range(1, k_max + 1):
|
|
12
|
+
lengths = []
|
|
13
|
+
for m in range(1, k + 1):
|
|
14
|
+
M = (N - m) // k
|
|
15
|
+
total = sum(abs(x[m - 1 + i * k] - x[m - 1 + (i - 1) * k]) for i in range(1, M + 1))
|
|
16
|
+
lengths.append(total * (N - 1) / (M * k) / k)
|
|
17
|
+
log_inv_k.append(numpy.log(1.0 / k))
|
|
18
|
+
log_L.append(numpy.log(numpy.mean(lengths)))
|
|
19
|
+
return numpy.polyfit(log_inv_k, log_L, 1)[0]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@pytest.mark.parametrize("seed", [0, 1, 2])
|
|
23
|
+
def test_matches_literal_formula(seed):
|
|
24
|
+
x = numpy.random.default_rng(seed).standard_normal(500)
|
|
25
|
+
assert hfd(x, k_max=16) == pytest.approx(reference_hfd(x, 16), abs=1e-10)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_k_max_bounds():
|
|
29
|
+
x = numpy.random.default_rng(3).standard_normal(100)
|
|
30
|
+
assert numpy.isfinite(hfd(x, k_max=50))
|
|
31
|
+
for bad in (1, 51):
|
|
32
|
+
with pytest.raises(ValueError):
|
|
33
|
+
hfd(x, k_max=bad)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@pytest.mark.parametrize(
|
|
37
|
+
"name, expected, tol",
|
|
38
|
+
[("white", 2.0, 0.05), ("brown", 1.5, 0.08), ("sine", 1.0, 0.05)],
|
|
39
|
+
)
|
|
40
|
+
def test_known_dimensions(name, expected, tol):
|
|
41
|
+
rng = numpy.random.default_rng(0)
|
|
42
|
+
signals = {
|
|
43
|
+
"white": rng.standard_normal(8192),
|
|
44
|
+
"brown": numpy.cumsum(rng.standard_normal(8192)),
|
|
45
|
+
"sine": numpy.sin(numpy.linspace(0, 20 * numpy.pi, 8192)),
|
|
46
|
+
}
|
|
47
|
+
assert hfd(signals[name]) == pytest.approx(expected, abs=tol)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import numpy
|
|
2
|
+
import pytest
|
|
3
|
+
|
|
4
|
+
from complexeva import lempel_ziv, lempel_ziv_hilbert
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def reference_kaspar_schuster(s):
|
|
8
|
+
"""Kaspar & Schuster (1987) algorithm as transcribed on Wikipedia, for strings of length >= 2."""
|
|
9
|
+
i, k, start, c, k_max, n = 0, 1, 1, 1, 1, len(s)
|
|
10
|
+
while True:
|
|
11
|
+
if s[i + k - 1] == s[start + k - 1]:
|
|
12
|
+
k += 1
|
|
13
|
+
if start + k > n:
|
|
14
|
+
c += 1
|
|
15
|
+
break
|
|
16
|
+
else:
|
|
17
|
+
k_max = max(k, k_max)
|
|
18
|
+
i += 1
|
|
19
|
+
if i == start:
|
|
20
|
+
c += 1
|
|
21
|
+
start += k_max
|
|
22
|
+
if start + 1 > n:
|
|
23
|
+
break
|
|
24
|
+
i, k, k_max = 0, 1, 1
|
|
25
|
+
else:
|
|
26
|
+
k = 1
|
|
27
|
+
return c
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@pytest.mark.parametrize(
|
|
31
|
+
"s, expected",
|
|
32
|
+
[("1", 1), ("0000000000", 2), ("0101010101", 3), ("0001101001000101", 6), ("1001111011000010", 6)],
|
|
33
|
+
)
|
|
34
|
+
def test_known_strings(s, expected):
|
|
35
|
+
assert lempel_ziv(s) == expected
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_matches_reference_on_random_strings():
|
|
39
|
+
rng = numpy.random.default_rng(0)
|
|
40
|
+
for _ in range(3000):
|
|
41
|
+
s = "".join(rng.choice(["0", "1"], size=int(rng.integers(2, 300))))
|
|
42
|
+
assert lempel_ziv(s) == reference_kaspar_schuster(s), s
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@pytest.mark.parametrize("method", ["median", "mean", "diff"])
|
|
46
|
+
def test_numeric_input_equals_binarised_string(method):
|
|
47
|
+
x = numpy.random.default_rng(1).standard_normal(400)
|
|
48
|
+
if method == "diff":
|
|
49
|
+
bits = numpy.diff(x) > 0
|
|
50
|
+
else:
|
|
51
|
+
bits = x > (numpy.median(x) if method == "median" else numpy.mean(x))
|
|
52
|
+
assert lempel_ziv(x, method=method) == reference_kaspar_schuster("".join("1" if b else "0" for b in bits))
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_normalize_default_off_and_formula():
|
|
56
|
+
s = "1001111011000010"
|
|
57
|
+
assert lempel_ziv(s) == 6 and isinstance(lempel_ziv(s), int)
|
|
58
|
+
assert lempel_ziv(s, normalize=True) == pytest.approx(6 * numpy.log2(16) / 16)
|
|
59
|
+
assert lempel_ziv("1", normalize=True) == 0.0
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_normalized_random_near_one_and_regular_low():
|
|
63
|
+
rng = numpy.random.default_rng(4)
|
|
64
|
+
assert lempel_ziv(rng.standard_normal(20000), normalize=True) == pytest.approx(1.0, abs=0.1)
|
|
65
|
+
assert lempel_ziv(numpy.sin(numpy.linspace(0, 40 * numpy.pi, 20000)), normalize=True) < 0.05
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_normalize_uses_binary_length_for_diff():
|
|
69
|
+
x = numpy.random.default_rng(5).standard_normal(300)
|
|
70
|
+
raw = lempel_ziv(x, method="diff")
|
|
71
|
+
assert lempel_ziv(x, method="diff", normalize=True) == pytest.approx(raw * numpy.log2(299) / 299)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_hilbert_variant_forwards_normalize():
|
|
75
|
+
x = numpy.random.default_rng(6).standard_normal(500)
|
|
76
|
+
assert 0 < lempel_ziv_hilbert(x, normalize=True) < 2
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_hilbert_variant_runs():
|
|
80
|
+
assert lempel_ziv_hilbert(numpy.random.default_rng(2).standard_normal(500)) > 1
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import numpy
|
|
2
|
+
import pytest
|
|
3
|
+
|
|
4
|
+
from complexeva import lyapunov_exponent
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def logistic(n, x0=0.3):
|
|
8
|
+
x = numpy.empty(n)
|
|
9
|
+
x[0] = x0
|
|
10
|
+
for i in range(1, n):
|
|
11
|
+
x[i] = 4 * x[i - 1] * (1 - x[i - 1])
|
|
12
|
+
return x
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def henon(n):
|
|
16
|
+
x, y, out = 0.1, 0.1, numpy.empty(n)
|
|
17
|
+
for i in range(n):
|
|
18
|
+
x, y = 1 - 1.4 * x * x + y, 0.3 * x
|
|
19
|
+
out[i] = x
|
|
20
|
+
return out
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_logistic_map_is_ln2():
|
|
24
|
+
assert lyapunov_exponent(logistic(3000), m=1, trajectory_len=8) == pytest.approx(numpy.log(2), abs=0.03)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_henon_map_known_value():
|
|
28
|
+
assert lyapunov_exponent(henon(3000), m=2, trajectory_len=8) == pytest.approx(0.419, abs=0.03)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_regular_signal_is_about_zero():
|
|
32
|
+
x = numpy.sin(numpy.linspace(0, 40 * numpy.pi, 3000))
|
|
33
|
+
assert abs(lyapunov_exponent(x)) < 0.01
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_scale_and_offset_invariant():
|
|
37
|
+
x = logistic(2000)
|
|
38
|
+
a = lyapunov_exponent(x, m=1, trajectory_len=8)
|
|
39
|
+
assert lyapunov_exponent(1000 * x + 5, m=1, trajectory_len=8) == pytest.approx(a, rel=1e-9)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_dt_scales_result():
|
|
43
|
+
x = logistic(2000)
|
|
44
|
+
assert lyapunov_exponent(x, m=1, trajectory_len=8, dt=0.5) == pytest.approx(2 * lyapunov_exponent(x, m=1, trajectory_len=8))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_too_short_raises():
|
|
48
|
+
with pytest.raises(ValueError):
|
|
49
|
+
lyapunov_exponent(numpy.arange(10.0), min_tsep=5)
|
complexeva-0.2.2/PKG-INFO
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: complexeva
|
|
3
|
-
Version: 0.2.2
|
|
4
|
-
Home-page:
|
|
5
|
-
Author: Don Yin
|
|
6
|
-
Author-email: don_yin@outlook.com
|
|
7
|
-
Description-Content-Type: text/markdown
|
|
8
|
-
Dynamic: author
|
|
9
|
-
Dynamic: author-email
|
|
10
|
-
Dynamic: description
|
|
11
|
-
Dynamic: description-content-type
|
|
12
|
-
|
|
13
|
-
# overview
|
|
14
|
-
implementations of complexity measures in 1D sequences
|
|
15
|
-
|
|
16
|
-
# examples
|
|
17
|
-
```python
|
|
18
|
-
from complexeva.sequence import lyapunov_exponent, hfd, lempel_ziv, compression_complexity
|
|
19
|
-
|
|
20
|
-
signal = numpy.random.rand(1000) # shape: (1000,)
|
|
21
|
-
complexity = hfd(signal)
|
|
22
|
-
print(complexity)
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
# install
|
|
26
|
-
```bash
|
|
27
|
-
pip install complexeva
|
|
28
|
-
```
|
complexeva-0.2.2/README.md
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
# overview
|
|
2
|
-
implementations of complexity measures in 1D sequences
|
|
3
|
-
|
|
4
|
-
# examples
|
|
5
|
-
```python
|
|
6
|
-
from complexeva.sequence import lyapunov_exponent, hfd, lempel_ziv, compression_complexity
|
|
7
|
-
|
|
8
|
-
signal = numpy.random.rand(1000) # shape: (1000,)
|
|
9
|
-
complexity = hfd(signal)
|
|
10
|
-
print(complexity)
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
# install
|
|
14
|
-
```bash
|
|
15
|
-
pip install complexeva
|
|
16
|
-
```
|
|
File without changes
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
from .higuchi import hfd, hfd_hilbert
|
|
2
|
-
from .lempel_ziv import lempel_ziv
|
|
3
|
-
from .compression import compression_complexity
|
|
4
|
-
from .lyapunov_exponent import lyapunov_exponent
|
|
5
|
-
|
|
6
|
-
__all__ = ["compression_complexity", "lyapunov_exponent", "hfd", "hfd_hilbert", "lempel_ziv"]
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
#!/Users/donyin/miniconda3/envs/rotation-1/bin/python
|
|
2
|
-
|
|
3
|
-
import zlib
|
|
4
|
-
import numpy as np
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
def compression_complexity(data: np.ndarray) -> float:
|
|
8
|
-
"""
|
|
9
|
-
calculate compression complexity using zlib compression -> float: compression complexity score
|
|
10
|
-
"""
|
|
11
|
-
data = data.tobytes()
|
|
12
|
-
compressed = zlib.compress(data)
|
|
13
|
-
return len(compressed) / len(data) # Normalized complexity score
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
if __name__ == "__main__":
|
|
17
|
-
signal = np.random.rand(1000) # shape: (1000,)
|
|
18
|
-
print(compression_complexity(signal))
|
|
@@ -1,75 +0,0 @@
|
|
|
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=16):
|
|
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=16):
|
|
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(X, k_max=16):
|
|
39
|
-
"""
|
|
40
|
-
Calculate Higuchi Fractal Dimension on the instantaneous frequency
|
|
41
|
-
obtained from Hilbert transform of the input signal.
|
|
42
|
-
|
|
43
|
-
Args:
|
|
44
|
-
X (numpy.ndarray): Input 1D signal
|
|
45
|
-
k_max (int): Maximum k value for HFD calculation
|
|
46
|
-
|
|
47
|
-
Returns:
|
|
48
|
-
float: Higuchi Fractal Dimension of the instantaneous frequency
|
|
49
|
-
"""
|
|
50
|
-
assert X.ndim == 1, f"X must be a 1d numpy array, got shape {X.shape}"
|
|
51
|
-
analytic_signal = hilbert(X)
|
|
52
|
-
instantaneous_phase = numpy.angle(analytic_signal)
|
|
53
|
-
instantaneous_freq = numpy.diff(instantaneous_phase)
|
|
54
|
-
instantaneous_freq = numpy.append(instantaneous_freq, instantaneous_freq[-1])
|
|
55
|
-
return hfd(instantaneous_freq, k_max=k_max)
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if __name__ == "__main__":
|
|
59
|
-
import time
|
|
60
|
-
|
|
61
|
-
signal = numpy.random.rand(1000) # shape: (1000,)
|
|
62
|
-
|
|
63
|
-
# Test plain HFD speed
|
|
64
|
-
start_time = time.time()
|
|
65
|
-
complexity_plain = hfd(signal)
|
|
66
|
-
plain_time = time.time() - start_time
|
|
67
|
-
|
|
68
|
-
# Test Hilbert HFD speed
|
|
69
|
-
start_time = time.time()
|
|
70
|
-
complexity_hilbert = hfd_hilbert(signal)
|
|
71
|
-
hilbert_time = time.time() - start_time
|
|
72
|
-
|
|
73
|
-
print(f"Plain HFD: {complexity_plain:.6f}, Time: {plain_time:.6f}s")
|
|
74
|
-
print(f"Hilbert HFD: {complexity_hilbert:.6f}, Time: {hilbert_time:.6f}s")
|
|
75
|
-
print(f"Speed ratio (Hilbert/Plain): {hilbert_time/plain_time:.2f}x")
|
|
@@ -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))
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import numpy as np
|
|
2
|
-
from scipy.spatial.distance import pdist, squareform
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
def lyapunov_exponent(data, tau=1, m=2, dt=1):
|
|
6
|
-
"""
|
|
7
|
-
Calculate the largest Lyapunov exponent for a time series.
|
|
8
|
-
|
|
9
|
-
Args:
|
|
10
|
-
data: numpy array or list of numerical values
|
|
11
|
-
tau: time delay (default=1)
|
|
12
|
-
m: embedding dimension (default=2)
|
|
13
|
-
dt: time step (default=1)
|
|
14
|
-
|
|
15
|
-
Returns:
|
|
16
|
-
float: largest Lyapunov exponent
|
|
17
|
-
"""
|
|
18
|
-
x = np.array(data)
|
|
19
|
-
N = len(x)
|
|
20
|
-
Y = np.zeros((N - (m - 1) * tau, m))
|
|
21
|
-
for i in range(m):
|
|
22
|
-
Y[:, i] = x[i * tau : N - (m - 1) * tau + i * tau]
|
|
23
|
-
D = squareform(pdist(Y))
|
|
24
|
-
np.fill_diagonal(D, np.inf) # Exclude self-matches
|
|
25
|
-
nearest = np.argmin(D, axis=1)
|
|
26
|
-
div = np.zeros(N - (m - 1) * tau - 1)
|
|
27
|
-
for i in range(len(div)):
|
|
28
|
-
div[i] = np.abs(x[i + 1] - x[nearest[i] + 1])
|
|
29
|
-
positive_div = div[div > 0] # Avoid log(0)
|
|
30
|
-
if len(positive_div) == 0:
|
|
31
|
-
return 0.0
|
|
32
|
-
return -np.mean(np.log(positive_div)) / dt
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
#!/Users/donyin/miniconda3/envs/rotation-1/bin/python
|
|
2
|
-
|
|
3
|
-
"""
|
|
4
|
-
- validating measures of complexity is actually working using synthetic data on various Hurst exponents
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
import numpy
|
|
8
|
-
import matplotlib.pyplot as plt
|
|
9
|
-
from complexeva.sequence.higuchi import hfd
|
|
10
|
-
from complexeva.sequence.lempel_ziv import lempel_ziv
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class ComplexityMeasuresValidator2D:
|
|
14
|
-
def __init__(self, funcs, timeseries_len=1000, num_hurst=200):
|
|
15
|
-
self.funcs, self.num_hurst, self.timeseries_len = funcs, num_hurst, timeseries_len
|
|
16
|
-
|
|
17
|
-
def fbm(self, hurst):
|
|
18
|
-
"""
|
|
19
|
-
Generate fractional Brownian motion (fBm) time series.
|
|
20
|
-
|
|
21
|
-
This method uses the spectral synthesis method to generate a fBm time series
|
|
22
|
-
with a specified Hurst exponent.
|
|
23
|
-
|
|
24
|
-
Parameters:
|
|
25
|
-
-----------
|
|
26
|
-
hurst : float
|
|
27
|
-
The Hurst exponent, a value between 0 and 1 that characterizes
|
|
28
|
-
the fractal properties of the time series.
|
|
29
|
-
|
|
30
|
-
Returns:
|
|
31
|
-
--------
|
|
32
|
-
numpy.ndarray
|
|
33
|
-
A 1D array representing the generated fBm time series.
|
|
34
|
-
|
|
35
|
-
Notes:
|
|
36
|
-
------
|
|
37
|
-
- The length of the generated time series is determined by self.timeseries_len.
|
|
38
|
-
- This method uses the Fourier filtering method to generate fBm.
|
|
39
|
-
- The first frequency component is set to a small non-zero value (1e-6) to avoid division by zero.
|
|
40
|
-
"""
|
|
41
|
-
|
|
42
|
-
n = self.timeseries_len
|
|
43
|
-
f = numpy.fft.fftfreq(n)
|
|
44
|
-
f[0] = 1e-6 # avoid division by zero
|
|
45
|
-
psd = numpy.abs(f) ** (-2 * hurst - 1)
|
|
46
|
-
gaussian = numpy.random.randn(n) + 1j * numpy.random.randn(n)
|
|
47
|
-
fBm = numpy.fft.ifft(gaussian * numpy.sqrt(psd)).real
|
|
48
|
-
return fBm
|
|
49
|
-
|
|
50
|
-
def plot_vs_hurst(self):
|
|
51
|
-
hurst_exponent = numpy.linspace(0.001, 1, self.num_hurst)
|
|
52
|
-
fBm_series = [self.fbm(h) for h in hurst_exponent]
|
|
53
|
-
expected_hfd = [2 - h for h in hurst_exponent]
|
|
54
|
-
hfd_results = []
|
|
55
|
-
|
|
56
|
-
for func in self.funcs:
|
|
57
|
-
hfd_results.append([func(series) for series in fBm_series])
|
|
58
|
-
|
|
59
|
-
# ---- plotting ----
|
|
60
|
-
fig, axs = plt.subplots(3, 1, figsize=(9, 10))
|
|
61
|
-
|
|
62
|
-
axs[0].plot(fBm_series[0])
|
|
63
|
-
axs[0].set_title(f"fBm series with H = {hurst_exponent[0]:.3f}", fontsize=21)
|
|
64
|
-
mid_index = len(hurst_exponent) // 2
|
|
65
|
-
axs[1].plot(fBm_series[mid_index])
|
|
66
|
-
axs[1].set_title(f"fBm series with H = {hurst_exponent[mid_index]:.3f}", fontsize=21)
|
|
67
|
-
axs[2].plot(fBm_series[-1])
|
|
68
|
-
axs[2].set_title(f"fBm series with H = {hurst_exponent[-1]:.3f}", fontsize=21)
|
|
69
|
-
|
|
70
|
-
for ax in axs:
|
|
71
|
-
ax.set_xlabel("Time", fontsize=21)
|
|
72
|
-
ax.set_ylabel("Value", fontsize=21)
|
|
73
|
-
ax.tick_params(axis="both", which="major", labelsize=12)
|
|
74
|
-
ax.grid(True)
|
|
75
|
-
|
|
76
|
-
plt.tight_layout()
|
|
77
|
-
plt.savefig("fBm_series_examples.png", dpi=320, bbox_inches="tight")
|
|
78
|
-
|
|
79
|
-
# Create figure with two subplots side by side
|
|
80
|
-
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(18, 7))
|
|
81
|
-
|
|
82
|
-
# Original HFD vs Hurst plot
|
|
83
|
-
ax1.plot(hurst_exponent, expected_hfd, label="Expected FD")
|
|
84
|
-
for i, func in enumerate(self.funcs):
|
|
85
|
-
ax1.plot(hurst_exponent, hfd_results[i], label=f"HFD ({func.__name__})")
|
|
86
|
-
ax1.set_xlabel("Hurst Exponent (H)", fontsize=14)
|
|
87
|
-
ax1.set_ylabel("Raw Complexity", fontsize=14)
|
|
88
|
-
ax1.set_title("Before Normalization", fontsize=16)
|
|
89
|
-
ax1.grid(True)
|
|
90
|
-
ax1.tick_params(axis="both", labelsize=12)
|
|
91
|
-
ax1.legend(fontsize=12)
|
|
92
|
-
|
|
93
|
-
# Normalized HFD vs Hurst plot
|
|
94
|
-
ax2.plot(hurst_exponent, expected_hfd, label="Expected FD")
|
|
95
|
-
for i, func in enumerate(self.funcs):
|
|
96
|
-
# Normalize results between 0 and 1
|
|
97
|
-
normalized_results = (hfd_results[i] - numpy.min(hfd_results[i])) / (
|
|
98
|
-
numpy.max(hfd_results[i]) - numpy.min(hfd_results[i])
|
|
99
|
-
)
|
|
100
|
-
ax2.plot(hurst_exponent, normalized_results, label=f"HFD ({func.__name__})")
|
|
101
|
-
ax2.set_xlabel("Hurst Exponent (H)", fontsize=14)
|
|
102
|
-
ax2.set_ylabel("Normalized Complexity", fontsize=14)
|
|
103
|
-
ax2.set_title("After Normalization (0-1)", fontsize=16)
|
|
104
|
-
ax2.grid(True)
|
|
105
|
-
ax2.tick_params(axis="both", labelsize=12)
|
|
106
|
-
ax2.legend(fontsize=12)
|
|
107
|
-
|
|
108
|
-
plt.tight_layout()
|
|
109
|
-
plt.savefig("hfd_vs_hurst_comparison.png", dpi=320, bbox_inches="tight")
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if __name__ == "__main__":
|
|
113
|
-
validator = ComplexityMeasuresValidator2D([hfd])
|
|
114
|
-
validator.plot_vs_hurst()
|
|
115
|
-
|
|
116
|
-
validator = ComplexityMeasuresValidator2D([lempel_ziv])
|
|
117
|
-
validator.plot_vs_hurst()
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: complexeva
|
|
3
|
-
Version: 0.2.2
|
|
4
|
-
Home-page:
|
|
5
|
-
Author: Don Yin
|
|
6
|
-
Author-email: don_yin@outlook.com
|
|
7
|
-
Description-Content-Type: text/markdown
|
|
8
|
-
Dynamic: author
|
|
9
|
-
Dynamic: author-email
|
|
10
|
-
Dynamic: description
|
|
11
|
-
Dynamic: description-content-type
|
|
12
|
-
|
|
13
|
-
# overview
|
|
14
|
-
implementations of complexity measures in 1D sequences
|
|
15
|
-
|
|
16
|
-
# examples
|
|
17
|
-
```python
|
|
18
|
-
from complexeva.sequence import lyapunov_exponent, hfd, lempel_ziv, compression_complexity
|
|
19
|
-
|
|
20
|
-
signal = numpy.random.rand(1000) # shape: (1000,)
|
|
21
|
-
complexity = hfd(signal)
|
|
22
|
-
print(complexity)
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
# install
|
|
26
|
-
```bash
|
|
27
|
-
pip install complexeva
|
|
28
|
-
```
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
README.md
|
|
2
|
-
setup.py
|
|
3
|
-
complexeva/__init__.py
|
|
4
|
-
complexeva/validate.py
|
|
5
|
-
complexeva.egg-info/PKG-INFO
|
|
6
|
-
complexeva.egg-info/SOURCES.txt
|
|
7
|
-
complexeva.egg-info/dependency_links.txt
|
|
8
|
-
complexeva.egg-info/top_level.txt
|
|
9
|
-
complexeva/sequence/__init__.py
|
|
10
|
-
complexeva/sequence/compression.py
|
|
11
|
-
complexeva/sequence/higuchi.py
|
|
12
|
-
complexeva/sequence/lempel_ziv.py
|
|
13
|
-
complexeva/sequence/lyapunov_exponent.py
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
complexeva
|
complexeva-0.2.2/setup.cfg
DELETED
complexeva-0.2.2/setup.py
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
from setuptools import setup, find_packages
|
|
2
|
-
|
|
3
|
-
setup(
|
|
4
|
-
name="complexeva",
|
|
5
|
-
version="0.2.2",
|
|
6
|
-
packages=find_packages(),
|
|
7
|
-
install_requires=[],
|
|
8
|
-
author="Don Yin",
|
|
9
|
-
author_email="don_yin@outlook.com",
|
|
10
|
-
description="",
|
|
11
|
-
long_description=open("README.md").read(),
|
|
12
|
-
long_description_content_type="text/markdown",
|
|
13
|
-
url="",
|
|
14
|
-
pyhon_requires=">=3.12",
|
|
15
|
-
)
|