Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge 0.6.1 into main #59

Merged
merged 20 commits into from
Oct 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
on:
push:
branches: ["main", "dev", "v*"]
pull_request:

name: build

Expand All @@ -9,14 +10,17 @@ jobs:
strategy:
matrix:
python: ["3.7", "3.10"]
extras: ["", "[RANSAC, qrandom, plots]"]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python }}
- run: pip install .
- run: pip install ".${{ matrix.extras }}"
- run: pip install codecov .
- run: coverage run -m unittest nolds.test_measures
- run: codecov
if: ${{ matrix.python == '3.10' }}
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
if: ${{ matrix.python == '3.10' && matrix.extras != '' }}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ _build

# virtual environment
.venv
.venv*
15 changes: 14 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,18 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
### Changed
### Fixed

## [0.6.1]

### Added

* Regression tests for all major algorithms that check for small changes in the main output value.

### Changed

* Nolds now supports numpy 2.x as well as 1.x.

### Fixed

## [0.6.0]

### Added
Expand Down Expand Up @@ -221,7 +233,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Hurst exponent (`hurst_rs`)
- detrended fluctuation analysis (DFA) (`dfa`)

[Unreleased]: https://github.com/CSchoel/nolds/compare/0.6.0..HEAD
[Unreleased]: https://github.com/CSchoel/nolds/compare/0.6.1..HEAD
[0.6.1]: https://github.com/CSchoel/nolds/compare/0.6.0..0.6.1
[0.6.0]: https://github.com/CSchoel/nolds/compare/0.5.2..0.6.0
[0.5.2]: https://github.com/CSchoel/nolds/compare/0.5.1..0.5.2
[0.5.1]: https://github.com/CSchoel/nolds/compare/0.5.0..0.5.1
Expand Down
2 changes: 1 addition & 1 deletion doc/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
# The short X.Y version.
version = '0.6'
# The full version, including alpha/beta/rc tags.
release = '0.6.0'
release = '0.6.1'

# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
Expand Down
12 changes: 8 additions & 4 deletions nolds/datasets.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,19 @@ def lorenz_euler(length, sigma, rho, beta, dt=0.01, start=[1,1,1]):
"""
def lorenz(state, sigma, rho, beta):
x, y, z = state
# NOTE: Numpy 1.x stores intermediate results as float64
# => to achieve consistency between numpy versions, we have to use
# float32 for all values that enter the formula to simulate numpy 1.x
# behavior with numpy 2.x.
return np.array([
sigma * (y - x),
rho * x - y - x * z,
x * y - beta * z
np.float32(sigma) * (y - x),
np.float32(rho) * x - y - x * z,
x * y - np.float32(beta) * z
], dtype="float32")
trajectory = np.zeros((length, 3), dtype="float32")
trajectory[0] = start
for i in range(1, length):
t = i * dt
# t = i * dt
trajectory[i] = trajectory[i-1] + lorenz(trajectory[i-1], sigma, rho, beta) * dt
return trajectory

Expand Down
2 changes: 1 addition & 1 deletion nolds/measures.py
Original file line number Diff line number Diff line change
Expand Up @@ -1442,7 +1442,7 @@ def mfhurst_b(data, qvals=None, dists=None, fit='poly',
Essentially, we can calculate c_q(d) of a discrete evenly sampled time
series Y = [y_0, y_1, y_2, ... y_(N-1)] by taking the absolute differences
[|y_0 - y_d|, |y_1 - y_(d+1)|, ... , |y_(N-d-1) - y_(N-1)|] raising them to
[\|y_0 - y_d\|, \|y_1 - y_(d+1)\|, ... , \|y_(N-d-1) - y_(N-1)\|] raising them to
the qth power and taking the mean.
Now we take the logarithm on both sides of our relation c_q(d) ~ d^(q H_q)
Expand Down
131 changes: 129 additions & 2 deletions nolds/test_measures.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def assert_array_equals(self, expected, actual, print_arrays=False):
print("==")
print(expected)
print()
self.assertTrue(np.alltrue(actual == expected))
self.assertTrue(np.all(actual == expected))

def test_delay_embed_lag2(self):
data = np.arange(10, dtype="float32")
Expand Down Expand Up @@ -459,7 +459,7 @@ def test_lorenz(self):
x = data[discard:,1]
rvals = nolds.logarithmic_r(1, np.e, 1.1) # determined experimentally
cd = nolds.corr_dim(x, emb_dim, fit="poly", rvals=rvals, lag=lag)
self.assertAlmostEqual(cd, 2.05, delta=0.1)
self.assertAlmostEqual(cd, 2.05, delta=0.2)

def test_logistic(self):
# TODO replicate tests with logistic map from grassberger-procaccia
Expand Down Expand Up @@ -544,5 +544,132 @@ def test_sampen_lorenz(self):
self.assertAlmostEqual(0.25, sz, delta=0.05)


class RegressionTests(unittest.TestCase):
"""Regression tests for main algorithms.
These tests are here to safeguard against accidental algorithmic changes such
as updates to core dependencies such as numpy or the Python standard library.
"""

def test_sampen(self):
"""Test hypothesis: The exact output of sampen() on random data hasn't changed since the last version."""
data = datasets.load_qrandom()[:1000]
se = nolds.sampen(data, emb_dim=2, tolerance=None, lag=1, dist=nolds.rowwise_chebyshev, closed=False)
self.assertAlmostEqual(2.1876999522832743, se, places=15)

def test_corr_dim(self):
"""Test hypothesis: The exact output of corr_dim() with `fit=poly` on random data hasn't changed since the last version."""
data = datasets.load_qrandom()[:1000]
cd = nolds.corr_dim(data, emb_dim=5, lag=1, rvals=None, dist=nolds.rowwise_euclidean, fit="poly")
self.assertAlmostEqual(1.303252839255068, cd, places=15)

@unittest.skipUnless(SCIPY_AVAILABLE, "Tests with RANSAC require scipy.")
def test_corr_dim_RANSAC(self):
"""Test hypothesis: The exact output of corr_dim() with `fit=RANSAC` on random data hasn't changed since the last version."""
data = datasets.load_qrandom()[:1000]
sd = np.std(data, ddof=1)
# fix seed
np.random.seed(42)
# usa a too wide range for rvals to give RANSAC something to do ;)
rvals = nolds.logarithmic_r(0.01 * sd, 2 * sd, 1.03)
cd = nolds.corr_dim(data, emb_dim=5, lag=1, rvals=rvals, dist=nolds.rowwise_euclidean, fit="RANSAC")
self.assertAlmostEqual(0.44745494643404665, cd, places=15)

def test_lyap_e(self):
"""Test hypothesis: The exact output of lyap_e() on random data hasn't changed since the last version."""
data = datasets.load_qrandom()[:1000]
le = nolds.lyap_e(data, emb_dim=10, matrix_dim=4, min_nb=10, min_tsep=1, tau=1)
expected = np.array([ 0.03779942603329712, -0.014314012551504982, -0.08436867977030214, -0.22316730257003717])
for i in range(le.shape[0]):
self.assertAlmostEqual(expected[i], le[i], places=15, msg=f"{i+1}th Lyapunov exponent doesn't match")

def test_lyap_r(self):
"""Test hypothesis: The exact output of lyap_r() with `fit=poly` on random data hasn't changed since the last version."""
data = datasets.load_qrandom()[:1000]
le = nolds.lyap_r(data, emb_dim=10, lag=1, min_tsep=1, tau=1, min_neighbors=10, trajectory_len=10, fit="poly")
expected = 0.094715945307378
self.assertAlmostEqual(expected, le, places=15)

@unittest.skipUnless(SCIPY_AVAILABLE, "Tests with RANSAC require scipy.")
def test_lyap_r_RANSAC(self):
"""Test hypothesis: The exact output of lyap_r() with `fit=RANSAC` on random data hasn't changed since the last version."""
data = datasets.load_qrandom()[:1000]
np.random.seed(42)
# set lag to 2 for weird duplicate lines
# set trajectory_len to 100 to get many datapoints for RANSAC to choose from
le = nolds.lyap_r(data, emb_dim=10, lag=2, min_tsep=1, tau=1, min_neighbors=10, trajectory_len=100, fit="RANSAC")
expected = 0.0003401212353253564
self.assertAlmostEqual(expected, le, places=15)

def test_hurst_rs(self):
"""Test hypothesis: The exact output of hurst_rs() with `fit=poly` on random data hasn't changed since the last version."""
data = datasets.load_qrandom()[:1000]
rs = nolds.hurst_rs(data, nvals=None, fit="poly", corrected=True, unbiased=True)
expected = 0.5123887964986258
self.assertAlmostEqual(expected, rs, places=15)

@unittest.skipUnless(SCIPY_AVAILABLE, "Tests with RANSAC require scipy.")
def test_hurst_rs_RANSAC(self):
"""Test hypothesis: The exact output of hurst_rs() with `fit=RANSAC` on random data hasn't changed since the last version."""
data = datasets.load_qrandom()[:1000]
np.random.seed(42)
# increase nsteps in nvals to have more data points for RANSAC to choose from
nvals = nolds.logmid_n(data.shape[0], ratio=1/4.0, nsteps=100)
rs = nolds.hurst_rs(data, nvals=nvals, fit="RANSAC", corrected=True, unbiased=True)
expected = 0.4805431939943321
self.assertAlmostEqual(expected, rs, places=15)

def test_dfa(self):
"""Test hypothesis: The exact output of dfa() with `fit_exp=poly` on random data hasn't changed since the last version."""
data = datasets.load_qrandom()[:1000]
h = nolds.dfa(data, nvals=None, overlap=True, order=1, fit_trend="poly", fit_exp="poly")
expected = 0.5450874638765073
self.assertAlmostEqual(expected, h, places=15)

@unittest.skipUnless(SCIPY_AVAILABLE, "Tests with RANSAC require scipy.")
def test_dfa_RANSAC(self):
"""Test hypothesis: The exact output of dfa() with `fit_exp=RANSAC` on random data hasn't changed since the last version."""
# adds trend to data to introduce a less clear line for fitting
data = datasets.load_qrandom()[:1000] + np.arange(1000) * 100
np.random.seed(42)
# adds more steps and higher values to nvals to introduce some scattering for RANSAC to have an effect on
nvals = nolds.logarithmic_n(10, 0.9 * data.shape[0], 1.1)
h = nolds.dfa(data, nvals=nvals, overlap=True, order=1, fit_trend="poly", fit_exp="RANSAC")
expected = 1.1372303125405405
self.assertAlmostEqual(expected, h, places=15)

def test_mfhurst_b(self):
"""Test hypothesis: The exact output of mfhurst_b() with `fit=poly` on random data hasn't changed since the last version."""
data = datasets.load_qrandom()[:1000]
h = nolds.mfhurst_b(data, qvals=[1], dists=None, fit="poly")
expected = [-0.00559398934417339]
self.assertAlmostEqual(expected[0], h[0], places=15)

@unittest.skipUnless(SCIPY_AVAILABLE, "Tests with RANSAC require scipy.")
def test_mfhurst_b_RANSAC(self):
"""Test hypothesis: The exact output of mfhurst_b() with `fit=RANSAC` on random data hasn't changed since the last version."""
data = datasets.load_qrandom()[:1000]
np.random.seed(42)
h = nolds.mfhurst_b(data, qvals=[1], dists=None, fit="RANSAC")
expected = [-0.009056463064211057]
self.assertAlmostEqual(expected[0], h[0], places=15)

def test_mfhurst_dm(self):
"""Test hypothesis: The exact output of mfhurst_dm() with `fit=poly` on random data hasn't changed since the last version."""
data = datasets.load_qrandom()[:1000]
h, _ = nolds.mfhurst_dm(data, qvals=[1], max_dists=range(5, 20), detrend=True, fit="poly")
expected = [0.008762803881203145]
self.assertAlmostEqual(expected[0], h[0], places=15)

@unittest.skipUnless(SCIPY_AVAILABLE, "Tests with RANSAC require scipy.")
def test_mfhurst_dm_RANSAC(self):
"""Test hypothesis: The exact output of mfhurst_dm() with `fit=RANSAC` on random data hasn't changed since the last version."""
data = datasets.load_qrandom()[:1000]
np.random.seed(42)
h, _ = nolds.mfhurst_dm(data, qvals=[1], max_dists=range(5, 20), detrend=True, fit="RANSAC")
expected = [0.005324834328837356]
self.assertAlmostEqual(expected[0], h[0], places=15)


if __name__ == "__main__":
unittest.main()
6 changes: 3 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def run(self):

with io.open("README.rst", "r", encoding="utf-8") as f:
readme = f.read()
version = '0.6.0'
version = '0.6.1'
setup(
name='nolds',
packages=['nolds'],
Expand Down Expand Up @@ -57,12 +57,12 @@ def run(self):
],
test_suite='nolds.test_measures',
install_requires=[
'numpy<2.0',
'numpy>1.0,<3.0',
'future',
'setuptools'
],
extras_require={
'RANSAC': ['sklearn>=0.19'],
'RANSAC': ['scikit-learn>=0.19'],
'qrandom': ['quantumrandom'],
'plots': ['matplotlib']
},
Expand Down