Skip to content

Commit 951d35a

Browse files
Merge pull request #27 from Digiratory/23-kasdin-generator
Kasdin generator
2 parents 0fd496a + a7cd0b1 commit 951d35a

File tree

7 files changed

+207
-47
lines changed

7 files changed

+207
-47
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## 1.7.1
44

5+
* [GH-23](https://github.com/Digiratory/StatTools/issues/23) feat: add Kasdin generator. fix: change first arg in lfilter in LBFBm generator.
56
* [GH-15](https://github.com/Digiratory/StatTools/issues/15) feat&fix: LBFBm generator update: generate with input value and return an increment instead of the absolute value of the signal.
67

78
## 1.7.0

README.md

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ from StatTools.filters import FilteredArray
3030

3131
h = 0.8 # choose Hurst parameter
3232
total_vectors = 1000 # total number of vectors in output
33-
vectors_length = 1440 # each vector's length
33+
vectors_length = 1440 # each vector's length
3434
t = 8 # threads in use during computation
3535

3636
correlated_vectors = Filter(h, vectors_length).generate(n_vectors=total_vectors,
@@ -49,13 +49,32 @@ base = 1.2 # the basis for the filter optimization algorithm
4949
target_len = 4000 # number of generation iterations
5050

5151
generator = LBFBmGenerator(h, filter_len, base)
52-
trajectory = []
53-
for value in islice(generator, target_len):
54-
trajectory.append(value)
52+
signal = []
53+
for value in islice(generator, target_len):
54+
signal.append(value)
5555
```
5656

5757
For more information and generator validation, see [lbfbm_generator.ipynb](/research/lbfbm_generator.ipynb).
5858

59+
It is also possible to use the method of generating increments with a given H using `KasdinGenerator`.
60+
61+
```python
62+
from StatTools.generators.kasdin_generator import KasdinGenerator
63+
h = 0.8 # choose Hurst parameter
64+
target_len = 4000 # number of generation iterations
65+
66+
generator = KasdinGenerator(h, length=target_len)
67+
68+
# the first option
69+
signal = generator.get_full_sequence()
70+
71+
# the second option
72+
signal_list = []
73+
for sample in generator:
74+
signal_list.append(sample)
75+
```
76+
For more information see Kasdin, N. J. (1995). Discrete simulation of colored noise and stochastic processes and 1/f/sup /spl alpha// power law noise generation. doi:10.1109/5.381848.
77+
5978
### Fluctuational Analysis
6079

6180
1. Example of Detrended Fluctuational Analysis (DFA)
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
from itertools import islice
2+
from typing import Iterator, Optional
3+
4+
import numpy as np
5+
from scipy.signal import lfilter
6+
7+
8+
class KasdinGenerator:
9+
"""
10+
Generates a sequence of numbers according to the Kasdin model.
11+
Based on the method proposed in the article Kasdin, N. J. (1995).
12+
Discrete simulation of colored noise and stochastic processes and 1/f/sup /spl alpha// power law noise generation.
13+
doi:10.1109/5.381848
14+
15+
Args:
16+
h (float): Hurst exponent (0 < H < 2)
17+
length (int): Maximum length of the sequence.
18+
random_generator (Iterator[float], optional): Iterator providing random values.
19+
Defaults is iter(np.random.randn(), None).
20+
Raises:
21+
ValueError: If length is less than 1
22+
StopIteration('Sequence exhausted') : If maximum sequence length has been reached.
23+
24+
Example usage:
25+
>>> generator = KasdinGenerator(h, length)
26+
>>> trj = list(generator)
27+
"""
28+
29+
def __init__(
30+
self,
31+
h: float,
32+
length: int,
33+
random_generator: Optional[Iterator[float]] = iter(np.random.randn, None),
34+
) -> None:
35+
if length is not None and length < 1:
36+
raise ValueError("Length must be more than 1")
37+
self.h = h
38+
self.length = length
39+
self.random_generator = random_generator
40+
41+
# init filter coefficients
42+
beta = 2 * self.h - 1
43+
self.filter_coefficients = np.zeros(self.length, dtype=np.float64)
44+
self.filter_coefficients[0] = 1.0
45+
k = np.arange(1, self.length)
46+
self.filter_coefficients[1:] = np.cumprod((k - 1 - beta / 2) / k)
47+
48+
# generate the sequence
49+
random_sequence = np.fromiter(
50+
islice(random_generator, self.length), dtype=np.float64
51+
)
52+
self.sequence = lfilter(1, self.filter_coefficients, random_sequence)
53+
self.current_index = 0
54+
55+
def get_filter_coefficients(self):
56+
"""Returns the filter coefficients."""
57+
return self.filter_coefficients
58+
59+
def __iter__(self) -> "KasdinGenerator":
60+
return self
61+
62+
def __next__(self) -> float:
63+
"""Return next value in sequence"""
64+
if self.current_index >= self.length:
65+
raise StopIteration("Sequence exhausted")
66+
self.current_index += 1
67+
return self.sequence[self.current_index - 1]
68+
69+
def get_full_sequence(self) -> np.ndarray:
70+
"""Return full generated sequence."""
71+
return self.sequence

StatTools/generators/lbfbm_generator.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import math
22
import warnings
3-
from typing import List, Iterator, Optional
3+
from typing import Iterator, List, Optional
4+
45
import numpy as np
5-
from scipy.signal import lfilter
66
from numpy.typing import NDArray
7+
from scipy.signal import lfilter
78

89

910
def signed_power(base: float, degree: float) -> float:
@@ -176,8 +177,7 @@ def _find_filter_len(self, base, length):
176177

177178
def _calculate_step(self) -> float:
178179
"""Applies a filter."""
179-
res = lfilter(np.ones(self.filter_len), self.matrix_a, self.bins[::-1])
180-
return res[-1] - res[-2]
180+
return lfilter(1, self.matrix_a, self.bins[::-1])[-1]
181181

182182
def __iter__(self) -> "LBFBmGenerator":
183183
return self

tests/test_filter.py

Lines changed: 50 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import numpy as np
22
import pytest
33

4-
from StatTools.generators.base_filter import Filter
54
from StatTools.analysis.dfa import DFA
5+
from StatTools.generators.base_filter import Filter
66

77
testdata = {
88
"target_mean": [0.5, 0.7, 0.9],
@@ -24,21 +24,25 @@ def test_filter_generator(h, length, target_std, target_mean):
2424
Test that the generated data has the specified mean and standard deviation.
2525
"""
2626
generator = Filter(h, length, set_mean=target_mean, set_std=target_std)
27-
trajectory = list(generator)
28-
29-
actual_mean = np.mean(trajectory)
30-
actual_std = np.std(trajectory, ddof=1)
31-
actual_h = DFA(trajectory).find_h()
27+
mean_difference_mean = 0
28+
mean_difference_std = 0
29+
mean_difference_h = 0
30+
times = 5
31+
for _ in range(times):
32+
trajectory = list(generator)
33+
mean_difference_mean += np.mean(trajectory)
34+
mean_difference_std += np.std(trajectory, ddof=1)
35+
mean_difference_h += DFA(trajectory).find_h()
3236

3337
assert (
34-
abs(actual_mean - target_mean) < 0.001
35-
), f"Mean deviation too large: expected {target_mean}, got {actual_mean}"
38+
abs(mean_difference_mean / times - target_mean) < 0.001
39+
), f"Mean deviation too large: expected {target_mean}, got {mean_difference_mean}"
3640
assert (
37-
abs(actual_std - target_std) < 0.001
38-
), f"Std deviation too large: expected {target_std}, got {actual_std}"
39-
assert abs(actual_h - h) < (
41+
abs(mean_difference_std / times - target_std) < 0.001
42+
), f"Std deviation too large: expected {target_std}, got {mean_difference_std}"
43+
assert abs(mean_difference_h / times - h) < (
4044
h * 0.15
41-
), f"Hurst deviation too large: expected {h}, got {actual_h}"
45+
), f"Hurst deviation too large: expected {h}, got {mean_difference_h}"
4246

4347

4448
@pytest.mark.parametrize("h", testdata["h"])
@@ -50,21 +54,25 @@ def test_filter(h, length, target_std, target_mean):
5054
Test that the generated data has the specified mean and standard deviation.
5155
"""
5256
generator = Filter(h, length, set_mean=target_mean, set_std=target_std)
53-
trajectory = generator.generate(n_vectors=1)
54-
55-
actual_mean = np.mean(trajectory)
56-
actual_std = np.std(trajectory, ddof=1)
57-
actual_h = DFA(trajectory).find_h()
57+
mean_difference_mean = 0
58+
mean_difference_std = 0
59+
mean_difference_h = 0
60+
times = 3
61+
for _ in range(times):
62+
trajectory = generator.generate(n_vectors=1)
63+
mean_difference_mean += np.mean(trajectory)
64+
mean_difference_std += np.std(trajectory, ddof=1)
65+
mean_difference_h += DFA(trajectory).find_h()
5866

5967
assert (
60-
abs(actual_mean - target_mean) < 0.001
61-
), f"Mean deviation too large: expected {target_mean}, got {actual_mean}"
68+
abs(mean_difference_mean / times - target_mean) < 0.001
69+
), f"Mean deviation too large: expected {target_mean}, got {mean_difference_mean}"
6270
assert (
63-
abs(actual_std - target_std) < 0.001
64-
), f"Std deviation too large: expected {target_std}, got {actual_std}"
65-
assert abs(actual_h - h) < (
71+
abs(mean_difference_std / times - target_std) < 0.001
72+
), f"Std deviation too large: expected {target_std}, got {mean_difference_std}"
73+
assert abs(mean_difference_h / times - h) < (
6674
h * 0.15
67-
), f"Hurst deviation too large: expected {h}, got {actual_h}"
75+
), f"Hurst deviation too large: expected {h}, got {mean_difference_h}"
6876

6977

7078
@pytest.mark.parametrize("h", testdata["h"])
@@ -75,22 +83,27 @@ def test_filter_2d(h, length, target_std, target_mean):
7583
"""
7684
Test that the generated data has the specified mean and standard deviation.
7785
"""
86+
count = 3
7887
generator = Filter(h, length, set_mean=target_mean, set_std=target_std)
79-
trajectories = generator.generate(n_vectors=3)
88+
trajectories = generator.generate(n_vectors=count)
8089

81-
for i in range(3):
90+
mean_difference_mean = 0
91+
mean_difference_std = 0
92+
mean_difference_h = 0
93+
94+
for i in range(count):
8295
trajectory = trajectories[i]
8396

84-
actual_mean = np.mean(trajectory)
85-
actual_std = np.std(trajectory, ddof=1)
86-
actual_h = DFA(trajectory).find_h()
97+
mean_difference_mean += np.mean(trajectory)
98+
mean_difference_std += np.std(trajectory, ddof=1)
99+
mean_difference_h += DFA(trajectory).find_h()
87100

88-
assert (
89-
abs(actual_mean - target_mean) < 0.001
90-
), f"Mean deviation too large: expected {target_mean}, got {actual_mean}"
91-
assert (
92-
abs(actual_std - target_std) < 0.001
93-
), f"Std deviation too large: expected {target_std}, got {actual_std}"
94-
assert abs(actual_h - h) < (
95-
h * 0.15
96-
), f"Hurst deviation too large: expected {h}, got {actual_h}"
101+
assert (
102+
abs(mean_difference_mean / count - target_mean) < 0.001
103+
), f"Mean deviation too large: expected {target_mean}, got {mean_difference_mean}"
104+
assert (
105+
abs(mean_difference_std / count - target_std) < 0.001
106+
), f"Std deviation too large: expected {target_std}, got {mean_difference_std}"
107+
assert abs(mean_difference_h / count - h) < (
108+
h * 0.15
109+
), f"Hurst deviation too large: expected {h}, got {mean_difference_h}"

tests/test_kasdin_generator.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import numpy as np
2+
import pytest
3+
4+
from StatTools.analysis.dfa import DFA
5+
from StatTools.generators.kasdin_generator import KasdinGenerator
6+
7+
testdata = {
8+
"h_list": [i * 0.01 for i in range(50, 200, 20)],
9+
"rate_list": [14],
10+
}
11+
12+
13+
def get_test_h(
14+
h: float,
15+
target_len: int,
16+
) -> float:
17+
"""
18+
Calculates the Hurst exponent for the generated trajectory.
19+
20+
Parameters:
21+
base: The base of the number system for bins
22+
filter_len: Filter length
23+
h: The specified Hurst exponent
24+
scales: Scales for analysis
25+
step: The step for analysis
26+
27+
Returns:
28+
Calculated Hurst exponent (h_gen)
29+
"""
30+
generator = KasdinGenerator(h, length=target_len)
31+
signal = generator.get_full_sequence()
32+
dfa = DFA(signal)
33+
return dfa.find_h()
34+
35+
36+
@pytest.mark.parametrize("h", testdata["h_list"])
37+
@pytest.mark.parametrize("rate", testdata["rate_list"])
38+
def test_kasdin_generator(h: float, rate: int):
39+
"""
40+
It tests the generator for compliance with the specified Hurst exponent.
41+
42+
Parameters:
43+
h: The specified Hurst exponent
44+
base: The base of the number system for bins
45+
"""
46+
threshold = 0.10
47+
times = 3
48+
mean_difference = 0
49+
length = 2**rate
50+
for _ in range(times):
51+
h_gen = get_test_h(h, length)
52+
mean_difference += abs(h_gen - h) / h
53+
mean_difference /= times
54+
assert (
55+
mean_difference <= threshold
56+
), f"Diff between h and h_gen exceeds {threshold * 100}%: h={h}, h_gen={h_gen}, mean diff={mean_difference * 100:.2f}%"

tests/test_lbfbm_generator.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
from scipy import stats
21
import numpy as np
32
import pytest
3+
from scipy import stats
44

55
from StatTools.analysis.dpcca import dpcca
66
from StatTools.generators.lbfbm_generator import LBFBmGenerator, normalize
@@ -79,7 +79,7 @@ def test_lbfbm_generator(h: float, base: float, rate: int):
7979
base: The base of the number system for bins
8080
"""
8181
threshold = 0.10
82-
times = 10
82+
times = 3
8383
mean_difference = 0
8484
length = 2**rate
8585
scales = np.array([2**i for i in range(3, rate - 3)])

0 commit comments

Comments
 (0)