antropy 0.2.2__tar.gz → 0.2.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: antropy
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: AntroPy: entropy and complexity of time-series in Python
5
5
  Author-email: Raphael Vallat <raphaelvallat9@gmail.com>
6
6
  Maintainer-email: Raphael Vallat <raphaelvallat9@gmail.com>
@@ -3,4 +3,4 @@ from .entropy import *
3
3
  from .fractal import *
4
4
  from .utils import *
5
5
 
6
- __version__ = "0.2.2"
6
+ __version__ = "0.2.3"
@@ -31,12 +31,11 @@ __all__ = [
31
31
  # the ordinal index from a small pre-computed table. This avoids the O(m
32
32
  # log m) sort and replaces it with O(m) bitwise operations.
33
33
  #
34
- # Note: equal values (ties) are encoded as False by '<', which may assign
35
- # them to a different ordinal pattern than argsort would. For the typical
36
- # case of continuous data this never occurs in practice.
34
+ # Comparisons use '<=' because the left column always has the earlier
35
+ # position. This breaks ties by position, matching stable argsort.
37
36
  # ---------------------------------------------------------------------------
38
37
 
39
- # order=3 — key bits: bit2=(a<b), bit1=(a<c), bit0=(b<c)
38
+ # order=3 — key bits: bit2=(a<=b), bit1=(a<=c), bit0=(b<=c)
40
39
  # The 8-entry _PE3_LOOKUP maps the 3-bit key to the hashmult hash value
41
40
  # (sum of argsort * [1,3,9]) used by the general path; _PE3_REMAP then
42
41
  # compresses those 6 sparse hash values to the dense range 0..5.
@@ -52,8 +51,8 @@ for _i, _v in enumerate([5, 7, 11, 15, 19, 21]):
52
51
  _PE3_REMAP[_v] = _i
53
52
  del _i, _v
54
53
 
55
- # order=4 — key bits: bit5=(a<b), bit4=(a<c), bit3=(a<d), bit2=(b<c),
56
- # bit1=(b<d), bit0=(c<d)
54
+ # order=4 — key bits: bit5=(a<=b), bit4=(a<=c), bit3=(a<=d), bit2=(b<=c),
55
+ # bit1=(b<=d), bit0=(c<=d)
57
56
  # The 64-entry table directly maps each valid 6-bit key to ordinal 0..23.
58
57
  # Invalid keys (impossible comparison patterns) keep the sentinel value -1.
59
58
  _PE4_LOOKUP = np.full(64, -1, dtype=np.int64)
@@ -87,24 +86,26 @@ def _perm_entropy_fast(x, order, delay, normalize):
87
86
  n, m = x.shape
88
87
  n_embed = m - (order - 1) * delay
89
88
 
90
- # Apply a positional epsilon jitter — adding i*eps to column i — so that
91
- # any tied values are broken by column index, exactly matching argsort's
92
- # behaviour. This handles integer signals, quantized data, zero-padded
93
- # epochs, and any other input with exact duplicate values.
94
- # eps is scaled to the signal magnitude so the jitter never affects the
95
- # ordering of distinct values.
96
- eps = np.finfo(np.float64).eps * (float(np.abs(x).max()) + 1)
97
- cols = [
98
- x[:, i * delay : i * delay + n_embed].astype(np.float64) + i * eps for i in range(order)
99
- ]
89
+ # NaN comparisons do not define an ordering and can produce invalid keys.
90
+ # Process valid epochs separately so a missing sample cannot affect them.
91
+ # Self-comparison detects NaN without requiring a float dtype.
92
+ valid_epochs = (x == x).all(axis=1)
93
+ if not valid_epochs.all():
94
+ result = np.full(n, np.nan)
95
+ result[valid_epochs] = _perm_entropy_fast(x[valid_epochs], order, delay, normalize)
96
+ return float(result[0]) if is_1d else result
97
+
98
+ # Keep the original values: a numeric jitter can reorder close values,
99
+ # and conversion to float64 can lose differences between large integers.
100
+ cols = [x[:, i * delay : i * delay + n_embed] for i in range(order)]
100
101
 
101
102
  if order == 3:
102
103
  col0, col1, col2 = cols
103
104
  # Encode the 3 pairwise comparisons as bits of an integer key
104
105
  bit_key = (
105
- ((col0 < col1).astype(np.uint8) << 2)
106
- | ((col0 < col2).astype(np.uint8) << 1)
107
- | (col1 < col2).astype(np.uint8)
106
+ ((col0 <= col1).astype(np.uint8) << 2)
107
+ | ((col0 <= col2).astype(np.uint8) << 1)
108
+ | (col1 <= col2).astype(np.uint8)
108
109
  )
109
110
  keys = _PE3_REMAP[_PE3_LOOKUP[bit_key]] # ordinal indices 0..5
110
111
  n_perms = 6
@@ -113,12 +114,12 @@ def _perm_entropy_fast(x, order, delay, normalize):
113
114
  col0, col1, col2, col3 = cols
114
115
  # Encode the 6 pairwise comparisons as bits of an integer key
115
116
  bit_key = (
116
- ((col0 < col1).astype(np.uint8) << 5)
117
- | ((col0 < col2).astype(np.uint8) << 4)
118
- | ((col0 < col3).astype(np.uint8) << 3)
119
- | ((col1 < col2).astype(np.uint8) << 2)
120
- | ((col1 < col3).astype(np.uint8) << 1)
121
- | (col2 < col3).astype(np.uint8)
117
+ ((col0 <= col1).astype(np.uint8) << 5)
118
+ | ((col0 <= col2).astype(np.uint8) << 4)
119
+ | ((col0 <= col3).astype(np.uint8) << 3)
120
+ | ((col1 <= col2).astype(np.uint8) << 2)
121
+ | ((col1 <= col3).astype(np.uint8) << 1)
122
+ | (col2 <= col3).astype(np.uint8)
122
123
  )
123
124
  keys = _PE4_LOOKUP[bit_key] # ordinal indices 0..23
124
125
  n_perms = 24
@@ -150,10 +151,11 @@ def perm_entropy(x, order=3, delay=1, normalize=False):
150
151
  2D input is only supported for ``order=3`` or ``order=4``.
151
152
  order : int
152
153
  Order of permutation entropy. Default is 3.
153
- delay : int, list, np.ndarray or range
154
+ delay : int, list, tuple, np.ndarray or range
154
155
  Time delay (lag). Default is 1. If multiple values are passed
155
156
  (e.g. ``[1, 2, 3]``), AntroPy will calculate the average permutation
156
- entropy across all these delays.
157
+ entropy across all these delays. For 2D input, the average is taken
158
+ separately for each epoch. Delay sequences must be nonempty and 1D.
157
159
  normalize : bool
158
160
  If True, divide by log2(order!) to normalize the entropy between 0
159
161
  and 1. Otherwise, return the permutation entropy in bits.
@@ -162,7 +164,8 @@ def perm_entropy(x, order=3, delay=1, normalize=False):
162
164
  -------
163
165
  pe : float or np.array
164
166
  Permutation entropy. Returns a scalar for 1D input, or an array of
165
- shape ``(n_epochs,)`` for 2D input.
167
+ shape ``(n_epochs,)`` for 2D input. For orders 3 and 4, an epoch
168
+ containing NaN returns NaN without affecting other epochs.
166
169
 
167
170
  Notes
168
171
  -----
@@ -245,15 +248,32 @@ def perm_entropy(x, order=3, delay=1, normalize=False):
245
248
  (4,)
246
249
  >>> print(np.round(pe, 4))
247
250
  [0.9997 0.9991 0.9988 0.9988]
251
+
252
+ Multiple delays retain one average per epoch:
253
+
254
+ >>> pe_delays = ant.perm_entropy(x2d, delay=(1, 2, 3), normalize=True)
255
+ >>> pe_delays.shape
256
+ (4,)
257
+ >>> single_delays = [ant.perm_entropy(x2d, delay=d, normalize=True) for d in (1, 2, 3)]
258
+ >>> np.allclose(pe_delays, np.mean(single_delays, axis=0))
259
+ True
248
260
  """
249
- # If multiple delays are passed, return the average across all of them
250
- if isinstance(delay, (list, np.ndarray, range)):
251
- return np.mean([perm_entropy(x, order=order, delay=d, normalize=normalize) for d in delay])
252
261
  x = np.asarray(x)
253
262
  if x.ndim not in (1, 2):
254
263
  raise ValueError("x must be 1D or 2D.")
255
264
  if order < 2:
256
265
  raise ValueError("Order has to be at least 2.")
266
+ # If multiple delays are passed, return the average across all of them
267
+ delays = np.asarray(delay)
268
+ if delays.ndim > 1:
269
+ raise ValueError("delay must be a scalar or a 1D sequence.")
270
+ if delays.ndim == 1:
271
+ if not delays.size:
272
+ raise ValueError("delay sequence must not be empty.")
273
+ return np.mean(
274
+ [perm_entropy(x, order=order, delay=d, normalize=normalize) for d in delays], axis=0
275
+ )
276
+ delay = delays.item()
257
277
  if delay < 1:
258
278
  raise ValueError("delay must be greater than zero.")
259
279
  delay = int(delay)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: antropy
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: AntroPy: entropy and complexity of time-series in Python
5
5
  Author-email: Raphael Vallat <raphaelvallat9@gmail.com>
6
6
  Maintainer-email: Raphael Vallat <raphaelvallat9@gmail.com>
@@ -4,7 +4,7 @@ import unittest
4
4
 
5
5
  import numpy as np
6
6
  from numpy import apply_along_axis as aal
7
- from numpy.testing import assert_equal
7
+ from numpy.testing import assert_allclose, assert_equal
8
8
  from utils import ARANGE, NORMAL_TS, PURE_SINE, RANDOM_TS, RANDOM_TS_LONG, TEST_DTYPES
9
9
 
10
10
  from antropy import (
@@ -27,8 +27,8 @@ def _perm_entropy_orig(x, order=3, delay=1, normalize=False):
27
27
  """Original argsort-based implementation with no fast path, used as reference.
28
28
 
29
29
  Uses kind='stable' so that ties are broken by position (earlier index ranks
30
- lower), matching the behaviour of the positional epsilon jitter in the fast
31
- path. For tie-free data stable and quicksort give identical results.
30
+ lower), matching the position-based comparisons in the fast path.
31
+ For tie-free data stable and quicksort give identical results.
32
32
  """
33
33
  from math import factorial
34
34
 
@@ -82,8 +82,7 @@ class TestEntropy(unittest.TestCase):
82
82
  def test_perm_entropy_ties(self):
83
83
  """Fast path must agree with the original argsort implementation on signals with ties.
84
84
 
85
- A positional epsilon jitter is applied to all inputs so ties are broken
86
- by column index, exactly matching argsort's position-based tiebreaking.
85
+ Ties are broken by column index, matching stable argsort.
87
86
  This applies to integer arrays, float arrays with duplicate values,
88
87
  zero-padded signals, and quantized data stored as float.
89
88
  """
@@ -94,8 +93,7 @@ class TestEntropy(unittest.TestCase):
94
93
  perm_entropy(x_const, order=3), _perm_entropy_orig(x_const, order=3)
95
94
  )
96
95
 
97
- # Ties at different window positions: without jitter the fast path would
98
- # collapse both tied windows to the same pattern; with jitter it matches argsort.
96
+ # Ties at different window positions must remain distinct patterns.
99
97
  # x = [3, 2, 2, 1, 3]:
100
98
  # window [3,2,2] → argsort [1,2,0] (b<c<a, tie broken by position)
101
99
  # window [2,2,1] → argsort [2,0,1] (c<a<b, tie broken by position)
@@ -106,7 +104,7 @@ class TestEntropy(unittest.TestCase):
106
104
  atol=1e-12,
107
105
  )
108
106
 
109
- # Same signal stored as float — jitter must apply regardless of dtype
107
+ # Same signal stored as float: tie handling is independent of dtype.
110
108
  x_ties_float = x_ties.astype(float)
111
109
  np.testing.assert_allclose(
112
110
  perm_entropy(x_ties_float, order=3),
@@ -145,6 +143,66 @@ class TestEntropy(unittest.TestCase):
145
143
  err_msg=f"Mismatch on quantized float signal for order={order}",
146
144
  )
147
145
 
146
+ def test_perm_entropy_preserves_ordering(self):
147
+ x = np.array([2, 0, 3, 1, 4, 1, 5, 2, 3, 0, 1, 4], dtype=np.int64)
148
+ examples = [
149
+ x,
150
+ x + 2**60,
151
+ x.astype(np.uint64) + 2**63,
152
+ np.array([int(value) + 2**100 for value in x], dtype=object),
153
+ x * 1e-18,
154
+ 1.0 + x * np.finfo(float).eps,
155
+ ]
156
+ for order in [3, 4]:
157
+ for delay in [1, 2, 3]:
158
+ for normalize in [False, True]:
159
+ # All examples preserve x's ordinal patterns. The original
160
+ # embedding helper casts integers to float64, so use x as
161
+ # the reference before adding large offsets.
162
+ reference = _perm_entropy_orig(x, order, delay, normalize)
163
+ for values in examples:
164
+ np.testing.assert_allclose(
165
+ perm_entropy(values, order, delay, normalize),
166
+ reference,
167
+ rtol=0,
168
+ atol=1e-14,
169
+ )
170
+ batch = np.vstack([x, x + 2**60])
171
+ np.testing.assert_allclose(
172
+ perm_entropy(batch, order, delay, normalize),
173
+ [reference, reference],
174
+ rtol=0,
175
+ atol=1e-14,
176
+ )
177
+
178
+ def test_perm_entropy_nan_epoch_isolation(self):
179
+ values = np.array([1, 0, 2, 3, 5, 4, 1, 2.0])
180
+ missing = values.copy()
181
+ missing[1] = np.nan
182
+ for order in [3, 4]:
183
+ for normalize in [False, True]:
184
+ self.assertTrue(np.isnan(perm_entropy(missing, order, normalize=normalize)))
185
+ self.assertTrue(
186
+ np.isnan(perm_entropy(missing.astype(object), order, normalize=normalize))
187
+ )
188
+ self.assertTrue(np.isnan(perm_entropy(np.vstack([missing, missing]), order)).all())
189
+ expected = _perm_entropy_orig(values, order, normalize=normalize)
190
+ for position in [0, 1, 2]:
191
+ batch = np.vstack([values, values, values])
192
+ batch[position] = missing
193
+ before = batch.copy()
194
+ actual = perm_entropy(batch, order, normalize=normalize)
195
+ self.assertTrue(np.isnan(actual[position]))
196
+ np.testing.assert_allclose(np.delete(actual, position), expected)
197
+ np.testing.assert_array_equal(batch, before)
198
+
199
+ infinite = values.copy()
200
+ infinite[:2] = [-np.inf, np.inf]
201
+ np.testing.assert_allclose(
202
+ perm_entropy(infinite, order, normalize=normalize),
203
+ _perm_entropy_orig(infinite, order, normalize=normalize),
204
+ )
205
+
148
206
  def test_perm_entropy_fast_path(self):
149
207
  """Fast paths (order=3/4) must match the original argsort implementation for 1D input."""
150
208
  rng = np.random.default_rng(0)
@@ -186,6 +244,44 @@ class TestEntropy(unittest.TestCase):
186
244
  err_msg=f"2D mismatch: order={order}, delay={delay}, normalize={normalize}",
187
245
  )
188
246
 
247
+ def test_perm_entropy_delay_shapes(self):
248
+ batch = np.random.default_rng(0).random((3, 30))
249
+ for values in [batch[0], batch]:
250
+ np.testing.assert_allclose(
251
+ perm_entropy(values, delay=np.array(2)), perm_entropy(values, delay=2)
252
+ )
253
+ for empty in [[], (), range(0), np.array([])]:
254
+ with self.assertRaisesRegex(ValueError, "delay sequence must not be empty"):
255
+ perm_entropy(values, delay=empty)
256
+ with self.assertRaisesRegex(ValueError, "scalar or a 1D sequence"):
257
+ perm_entropy(values, delay=np.array([[1, 2]]))
258
+
259
+ def test_perm_entropy_2d_multiple_delays(self):
260
+ x = np.array(
261
+ [
262
+ [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5, 8],
263
+ [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
264
+ ]
265
+ )
266
+ for order in [3, 4]:
267
+ for normalize in [False, True]:
268
+ for delays in [[1, 2, 3], (1, 2, 3), np.array([1, 2, 3]), range(1, 4)]:
269
+ reference = [
270
+ np.mean(
271
+ [_perm_entropy_orig(row, order, delay, normalize) for delay in delays]
272
+ )
273
+ for row in x
274
+ ]
275
+ result = perm_entropy(x, order, delays, normalize)
276
+ self.assertEqual(result.shape, (2,))
277
+ np.testing.assert_allclose(result, reference)
278
+ single_epoch = perm_entropy(x[:1], order, delays, normalize)
279
+ self.assertEqual(single_epoch.shape, (1,))
280
+ np.testing.assert_allclose(single_epoch, reference[:1])
281
+ scalar = perm_entropy(x[0], order, delays, normalize)
282
+ self.assertEqual(np.ndim(scalar), 0)
283
+ np.testing.assert_allclose(scalar, reference[0])
284
+
189
285
  def test_spectral_entropy(self):
190
286
  spectral_entropy(RANDOM_TS, SF_TS, method="fft")
191
287
  spectral_entropy(RANDOM_TS, SF_TS, method="welch")
@@ -194,7 +290,7 @@ class TestEntropy(unittest.TestCase):
194
290
  self.assertEqual(np.round(spectral_entropy(PURE_SINE, 100), 2), 0.0)
195
291
  # 2D data
196
292
  params = dict(sf=SF_TS, normalize=True, method="welch", nperseg=100)
197
- assert_equal(
293
+ assert_allclose(
198
294
  aal(spectral_entropy, axis=1, arr=data, **params),
199
295
  spectral_entropy(data, **params),
200
296
  )
@@ -310,7 +406,7 @@ class TestEntropy(unittest.TestCase):
310
406
  assert mob_sine < mob
311
407
  assert com_sine < com
312
408
  # 2D data (avoid warning with flat line variance)
313
- assert_equal(
409
+ assert_allclose(
314
410
  aal(hjorth_params, axis=-1, arr=data[:-1, :]).T,
315
411
  hjorth_params(data[:-1, :], axis=-1),
316
412
  )
@@ -4,7 +4,7 @@ import unittest
4
4
 
5
5
  import numpy as np
6
6
  from numpy import apply_along_axis as aal
7
- from numpy.testing import assert_equal
7
+ from numpy.testing import assert_allclose
8
8
  from utils import ARANGE, NORMAL_TS, PURE_COSINE, PURE_SINE, RANDOM_TS, TEST_DTYPES
9
9
 
10
10
  from antropy import detrended_fluctuation, higuchi_fd, katz_fd, petrosian_fd
@@ -49,16 +49,16 @@ class TestEntropy(unittest.TestCase):
49
49
  petrosian_fd(list(RANDOM_TS))
50
50
  self.assertEqual(np.round(pfd, 3), 1.030)
51
51
  # 2D data
52
- assert_equal(aal(petrosian_fd, axis=1, arr=data), petrosian_fd(data))
53
- assert_equal(aal(petrosian_fd, axis=0, arr=data), petrosian_fd(data, axis=0))
52
+ assert_allclose(aal(petrosian_fd, axis=1, arr=data), petrosian_fd(data))
53
+ assert_allclose(aal(petrosian_fd, axis=0, arr=data), petrosian_fd(data, axis=0))
54
54
 
55
55
  def test_katz_fd(self):
56
56
  x_k = [0.0, 0.0, 2.0, -2.0, 0.0, -1.0, -1.0, 0.0]
57
57
  self.assertEqual(np.round(katz_fd(x_k), 3), 5.783)
58
58
  # 2D data
59
59
  data_kfd = np.vstack((RANDOM_TS, NORMAL_TS, PURE_SINE, PURE_COSINE, ARANGE))
60
- assert_equal(aal(katz_fd, axis=1, arr=data_kfd), katz_fd(data_kfd))
61
- assert_equal(aal(katz_fd, axis=0, arr=data_kfd), katz_fd(data_kfd, axis=0))
60
+ assert_allclose(aal(katz_fd, axis=1, arr=data_kfd), katz_fd(data_kfd))
61
+ assert_allclose(aal(katz_fd, axis=0, arr=data_kfd), katz_fd(data_kfd, axis=0))
62
62
 
63
63
  def test_higuchi_fd(self):
64
64
  """Test for function `higuchi_fd`.
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes