complexeva 0.2.3__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.
@@ -0,0 +1,12 @@
1
+ build
2
+ dist
3
+ complexeva.egg-info
4
+ _dev**
5
+ **.sh
6
+
7
+ __pycache__
8
+ .DS_Store
9
+ **test.py
10
+
11
+ candidate-measures.md
12
+ dev.py
@@ -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`.
@@ -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
@@ -1,6 +1,6 @@
1
+ from .compression import compression_complexity
1
2
  from .higuchi import hfd, hfd_hilbert
2
3
  from .lempel_ziv import lempel_ziv, lempel_ziv_hilbert
3
- from .compression import compression_complexity
4
4
  from .lyapunov_exponent import lyapunov_exponent
5
5
 
6
- __all__ = ["compression_complexity", "lyapunov_exponent", "hfd", "hfd_hilbert", "lempel_ziv", "lempel_ziv_hilbert"]
6
+ __all__ = ["compression_complexity", "hfd", "hfd_hilbert", "lempel_ziv", "lempel_ziv_hilbert", "lyapunov_exponent"]
@@ -0,0 +1,9 @@
1
+ import zlib
2
+
3
+ import numpy as np
4
+
5
+
6
+ def compression_complexity(data: np.ndarray) -> float:
7
+ """zlib-compressed size divided by raw size of the array bytes."""
8
+ raw = data.tobytes()
9
+ return len(zlib.compress(raw)) / len(raw)
@@ -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.3/PKG-INFO DELETED
@@ -1,28 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: complexeva
3
- Version: 0.2.3
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,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,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,122 +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=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")
@@ -1,266 +0,0 @@
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,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.3
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
- complexeva
@@ -1,4 +0,0 @@
1
- [egg_info]
2
- tag_build =
3
- tag_date = 0
4
-
complexeva-0.2.3/setup.py DELETED
@@ -1,15 +0,0 @@
1
- from setuptools import setup, find_packages
2
-
3
- setup(
4
- name="complexeva",
5
- version="0.2.3",
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
- )