diff --git a/README.md b/README.md
index f96d8d6..6a0639e 100644
--- a/README.md
+++ b/README.md
@@ -80,7 +80,7 @@ Note that individual operations can only be called directly on individual time-s
Time-series feature extraction is computationally intensive.
To speed up processing, pyhctsa allows you to distribute the workload across multiple CPU cores on your local machine using the `LocalDistributor`:
```Python
-from pyhctsa.distributed import LocalDistributor
+from pyhctsa.distribute import LocalDistributor
from pyhctsa.calculator import FeatureCalculator
# initialize the calculator
@@ -94,21 +94,6 @@ dist = LocalDistributor(n_workers=4)
res = calc.extract(data, distributor=dist)
```
-## ℹ️ Note for Windows users
-Some features require Java (JDK) to be installed. If you encounter a `JVM not found` error:
-
-1. Ensure Java Development Kit (JDK) is installed on your system
- - Download from [Oracle](https://www.oracle.com/java/technologies/downloads/) or use OpenJDK
- - Minimum version required: JDK 11
-
-2. Before importing pyhctsa, set the `JAVA_HOME` environment variable using the location of the JDK installation on your system:
-```Python
-import os
-os.environ['JAVA_HOME'] = "C:\Program Files\Java\jdk-11" # replace with relevant path
-from pyhctsa.calculator import FeatureCalculator
-# rest of your code...
-```
-
# 🔑 Licenses
## Internal licenses
@@ -119,7 +104,6 @@ While the majority of features in _pyhctsa_ rely on standard Python libraries, a
The following external time-series analysis code packages are provided with the software (in the `toolboxes` directory), and are used by our main feature-extraction calculator to compute meaningful structural features from time series:
-- Joseph T. Lizier's [Java Information Dynamics Toolkit (JIDT)](https://github.com/jlizier/jidt) for studying information-theoretic measures of computation in complex systems, version 1.3 (GPL license).
- Time-series analysis code developed by [Michael Small](https://github.com/m-small) (unlicensed).
- Max Little's [time-series analysis code](http://www.maxlittle.net/software/index.php) (GPL License).
- [TISEAN package for nonlinear time-series analysis](http://www.mpipks-dresden.mpg.de/~tisean/Tisean_3.0.1/index.html), version 3.0.1 (GPL license).
diff --git a/docs/source/usage/getting_started.rst b/docs/source/usage/getting_started.rst
index af78311..010ac9f 100644
--- a/docs/source/usage/getting_started.rst
+++ b/docs/source/usage/getting_started.rst
@@ -53,7 +53,7 @@ cores on your local machine using the `LocalDistributor`:
.. code-block:: python
- from pyhctsa.distributed import LocalDistributor
+ from pyhctsa.distribute import LocalDistributor
from pyhctsa.calculator import FeatureCalculator
# initialize the calculator
@@ -66,16 +66,4 @@ cores on your local machine using the `LocalDistributor`:
# pass the distributor to the .extract() method
res = calc.extract(data, distributor=dist)
-ℹ️ Note for Windows Users
--------------------------
-Some features require Java (JDK) to be installed. If you encounter a JVM not found error:
- 1. Ensure Java Development Kit (JDK) is installed on your system
- - Download from Oracle or use OpenJDK (Minimum version required: JDK 11)
- 2. Before importing `pyhctsa`, set the `JAVA_HOME` environment variable using the location of the JDK installation on your system:
-
- .. code-block:: python
-
- import os
- os.environ['JAVA_HOME'] = "C:\Program Files\Java\jdk-11" # replace with relevant path
- from pyhctsa.calculator import FeatureCalculator
- # rest of your code...
\ No newline at end of file
+
\ No newline at end of file
diff --git a/pyhctsa/calculator.py b/pyhctsa/calculator.py
index 131d397..9f6cdc9 100644
--- a/pyhctsa/calculator.py
+++ b/pyhctsa/calculator.py
@@ -6,6 +6,10 @@
from typing import Union, Any, Callable
import logging
+logger = logging.getLogger('pyhctsa')
+logger.setLevel(logging.CRITICAL) # only log critical warnings by default
+logger.addHandler(logging.NullHandler())
+
import numpy as np
import pandas as pd
import yaml
@@ -70,7 +74,7 @@ def wrapper(*args, **kwargs):
if isinstance(result, dict):
missing = [k for k in keys if k not in result] # log all of the missing keys
if missing:
- logging.info(f'Warning: time-series features for func {func} not found {missing}')
+ logger.info(f'Warning: time-series features for func {func} not found {missing}')
if keep:
return {k: result[k] for k in keys if k in result}
else:
@@ -87,7 +91,7 @@ def _standardise_inputs(data) -> list[np.ndarray]:
elif data.ndim == 2:
if data.shape[0] > data.shape[1]:
# notify the user to check that the shapes make sense
- logging.warning(f"Check that the shape of the 2D input is such "
+ logger.warning(f"Check that the shape of the 2D input is such "
f"that (n_series, n_samples). Got shape: {data.shape}")
return [np.asarray(row, dtype=float) for row in data]
else:
@@ -209,34 +213,36 @@ def _repr_html_(self):
return _build_repr_html(self.feature_funcs, self._skipped_functions, self.config, self.config_path)
def _check_deps(self, module_key, feature_name, config):
- raw_deps = config.get("dependencies")
+ raw_deps = config.get("dependencies", None)
if not raw_deps:
return True
deps_to_check = [raw_deps] if isinstance(raw_deps, str) else raw_deps
missing = [dep for dep in deps_to_check if not _check_optional_deps(dep)]
if missing:
full_name = f"{module_key}.{feature_name}"
- logging.info(f"Skipping function '{full_name}' - missing dependencies: {', '.join(missing)}")
+ logger.info(f"Skipping function '{full_name}' - missing dependencies: {', '.join(missing)}")
self._skipped_functions.append((full_name, missing))
return False
return True
def _build_feature_funcs(self):
feature_funcs = {}
- skipped_functions = []
+ self._skipped_functions = []
for module_key in self.config.keys():
try:
module = importlib.import_module(f"{self._operations_package}.{module_key}")
except ImportError as e:
- logging.warning(f"Failed to import module '{module_key}': {e}")
+ logger.warning(f"Failed to import module '{module_key}': {e}")
# Skip all functions in this module since we can't import it
for feature_name in self.config[module_key].keys():
- skipped_functions.append((f"{module_key}.{feature_name}", ["import_error"]))
+ self._skipped_functions.append((f"{module_key}.{feature_name}", ["import_error"]))
continue
# Process features from this module
for feature_name, feature_config in self.config[module_key].items():
+ if not self._check_deps(module_key, feature_name, feature_config):
+ continue
op_func = getattr(module, feature_name)
base_name = feature_config.get("base_name", feature_name)
ordered_args = feature_config.get("ordered_args", [])
@@ -270,11 +276,8 @@ def _build_feature_funcs(self):
feature_funcs[label] = final_func
- # store information about skipped functions for later reference
- self._skipped_functions = skipped_functions
- if skipped_functions:
- logging.info(f"Total functions skipped due to missing dependencies: {len(skipped_functions)}")
-
+ if self._skipped_functions:
+ logger.info(f"Total functions skipped due to missing dependencies: {len(self._skipped_functions)}")
return feature_funcs
def extract(self, data: Union[ArrayLike, list[ArrayLike]],
diff --git a/pyhctsa/configurations/hctsa.yaml b/pyhctsa/configurations/hctsa.yaml
index 3b9464f..d54e36b 100644
--- a/pyhctsa/configurations/hctsa.yaml
+++ b/pyhctsa/configurations/hctsa.yaml
@@ -77,7 +77,6 @@ correlation:
add_noise:
base_name: add_noise
depedencies:
- - jpype1
configs:
- {tau: 1, ami_method: 'quantiles', extra_param: 10, zscore: True}
- {tau: 1, ami_method: 'even', extra_param: 10, zscore: True}
@@ -440,38 +439,35 @@ information:
automutual_info_stats:
base_name: automutual_info_stats
dependencies:
- - jpype1
configs:
- {max_tau: 40, est_method: 'gaussian', zscore: True}
- {max_tau: 20, est_method: 'gaussian', zscore: True}
- - {max_tau: 40, est_method: 'kraskov1', extra_param: '4', zscore: True}
- - {max_tau: 20, est_method: 'kraskov1', extra_param: '4', zscore: True}
+ - {max_tau: 40, est_method: 'kraskov1', extra_param: 4, zscore: True}
+ - {max_tau: 20, est_method: 'kraskov1', extra_param: 4, zscore: True}
legacy_name: IN_AutoMutualInfoStats
ordered_args: ['max_tau', 'est_method', 'extra_param']
first_min:
base_name: first_min
dependencies:
- - jpype1
configs:
- {min_what: 'ac', zscore: True}
- {min_what: 'mi-gaussian', zscore: True}
- - {min_what: 'mi-kraskov2', extra_param: '4', zscore: True}
- - {min_what: 'mi-hist', extra_param: '5', zscore: True}
- - {min_what: 'mi-hist', extra_param: '10', zscore: True}
+ - {min_what: 'mi-kraskov2', extra_param: 4, zscore: True}
+ - {min_what: 'mi-hist', extra_param: 5, zscore: True}
+ - {min_what: 'mi-hist', extra_param: 10, zscore: True}
legacy_name: CO_FirstMin
ordered_args: ['min_what', 'extra_param']
first_max:
base_name: first_max
depedencies:
- - jpype1
configs:
- {max_what: 'ac', zscore: True}
- {max_what: 'mi-gaussian', zscore: True}
- - {max_what: 'mi-kraskov2', extra_param: '4', zscore: True}
- - {max_what: 'mi-hist', extra_param: '5', zscore: True}
- - {max_what: 'mi-hist', extra_param: '10', zscore: True}
+ - {max_what: 'mi-kraskov2', extra_param: 4, zscore: True}
+ - {max_what: 'mi-hist', extra_param: 5, zscore: True}
+ - {max_what: 'mi-hist', extra_param: 10, zscore: True}
legacy_name: CO_FirstMin
ordered_args: ['max_what', 'extra_param']
diff --git a/pyhctsa/configurations/module_configs/correlation.yaml b/pyhctsa/configurations/module_configs/correlation.yaml
index 6947d58..15e5f8d 100644
--- a/pyhctsa/configurations/module_configs/correlation.yaml
+++ b/pyhctsa/configurations/module_configs/correlation.yaml
@@ -11,7 +11,6 @@ correlation:
add_noise:
base_name: add_noise
depedencies:
- - jpype1
configs:
- {tau: 1, ami_method: 'quantiles', extra_param: 10, zscore: True}
- {tau: 1, ami_method: 'even', extra_param: 10, zscore: True}
diff --git a/pyhctsa/configurations/module_configs/information.yaml b/pyhctsa/configurations/module_configs/information.yaml
index 504187a..69cc196 100644
--- a/pyhctsa/configurations/module_configs/information.yaml
+++ b/pyhctsa/configurations/module_configs/information.yaml
@@ -2,38 +2,35 @@ information:
automutual_info_stats:
base_name: automutual_info_stats
dependencies:
- - jpype1
configs:
- {max_tau: 40, est_method: 'gaussian', zscore: True}
- {max_tau: 20, est_method: 'gaussian', zscore: True}
- - {max_tau: 40, est_method: 'kraskov1', extra_param: '4', zscore: True}
- - {max_tau: 20, est_method: 'kraskov1', extra_param: '4', zscore: True}
+ - {max_tau: 40, est_method: 'kraskov1', extra_param: 4, zscore: True}
+ - {max_tau: 20, est_method: 'kraskov1', extra_param: 4, zscore: True}
legacy_name: IN_AutoMutualInfoStats
ordered_args: ['max_tau', 'est_method', 'extra_param']
first_min:
base_name: first_min
dependencies:
- - jpype1
configs:
- {min_what: 'ac', zscore: True}
- {min_what: 'mi-gaussian', zscore: True}
- - {min_what: 'mi-kraskov2', extra_param: '4', zscore: True}
- - {min_what: 'mi-hist', extra_param: '5', zscore: True}
- - {min_what: 'mi-hist', extra_param: '10', zscore: True}
+ - {min_what: 'mi-kraskov2', extra_param: 4, zscore: True}
+ - {min_what: 'mi-hist', extra_param: 5, zscore: True}
+ - {min_what: 'mi-hist', extra_param: 10, zscore: True}
legacy_name: CO_FirstMin
ordered_args: ['min_what', 'extra_param']
first_max:
base_name: first_max
- depedencies:
- - jpype1
+ dependencies:
configs:
- {max_what: 'ac', zscore: True}
- {max_what: 'mi-gaussian', zscore: True}
- - {max_what: 'mi-kraskov2', extra_param: '4', zscore: True}
- - {max_what: 'mi-hist', extra_param: '5', zscore: True}
- - {max_what: 'mi-hist', extra_param: '10', zscore: True}
+ - {max_what: 'mi-kraskov2', extra_param: 4, zscore: True}
+ - {max_what: 'mi-hist', extra_param: 5, zscore: True}
+ - {max_what: 'mi-hist', extra_param: 10, zscore: True}
legacy_name: CO_FirstMin
ordered_args: ['max_what', 'extra_param']
diff --git a/pyhctsa/operations/correlation.py b/pyhctsa/operations/correlation.py
index d728b2e..8eef6e4 100644
--- a/pyhctsa/operations/correlation.py
+++ b/pyhctsa/operations/correlation.py
@@ -1,4 +1,5 @@
import logging
+logger = logging.getLogger('pyhctsa')
from typing import Union
import numpy as np
@@ -9,7 +10,7 @@
from scipy.stats import mode as smode
from statsmodels.tsa.stattools import pacf
-from ..operations.information import automutual_info, first_min
+from ..operations.information import first_min, automutual_info
from ..toolboxes.c22 import periodicity_wang_wrapper
from ..utils import bin_picker, make_mat_buffer, point_of_crossing, sign_change, z_score, histc
@@ -20,8 +21,8 @@ def add_noise(y: ArrayLike, tau: Union[int, str] = 1, ami_method: str = 'even',
Adds Gaussian-distributed noise to the time series with increasing standard deviation, eta,
across the range eta = 0, 0.1, ..., 2, and measures the mutual information at each point.
- Can be measured using histograms with extra_param bins or using the Information Dynamics
- Toolkit. The output is a set of statistics on the resulting set of automutual information
+ Can be measured using histograms with extra_param bins, or Kraskov estimators with k = extra_param.
+ The output is a set of statistics on the resulting set of automutual information
estimates, including a fit to an exponential decay, since the automutual information
decreases with the added white noise. This algorithm is quite different, but was based
on the idea in [1].
@@ -55,10 +56,9 @@ def add_noise(y: ArrayLike, tau: Union[int, str] = 1, ami_method: str = 'even',
- ``"quantiles"``
- ``"even"``
- JIDT-based estimators:
+ Alternative estimators:
- ``"gaussian"``
- - ``"kernel"``
- ``"kraskov1"``
- ``"kraskov2"``
@@ -68,7 +68,7 @@ def add_noise(y: ArrayLike, tau: Union[int, str] = 1, ami_method: str = 'even',
Additional parameter for the AMI estimator.
- For histogram methods: number of bins.
- - For JIDT methods: estimator-specific parameter.
+ - For alternative methods: estimator-specific parameter.
Default is ``10``.
@@ -86,9 +86,6 @@ def add_noise(y: ArrayLike, tau: Union[int, str] = 1, ami_method: str = 'even',
# Set tau to minimum of autocorrelation function if 'ac' or 'tau'
if tau in ['ac', 'tau']:
tau = first_crossing(y, 'ac', 0, 'discrete')
- if extra_param is None:
- # JIDT expects empty string for no extra params
- extra_param = ''
# Generate noise
if random_seed is not None:
np.random.seed(random_seed)
@@ -107,12 +104,14 @@ def add_noise(y: ArrayLike, tau: Union[int, str] = 1, ami_method: str = 'even',
for i in range(num_repeats):
amis[i] = histogram_ami(y + noise_range[i]*noise, tau, ami_method, extra_param)
if np.isnan(amis[i]):
- raise ValueError('Error computing AMI: Time series too short (?)')
- if ami_method in ['gaussian','kernel','kraskov1','kraskov2']:
+ logger.warning('Error computing AMI: Time series too short (?)')
+ return np.nan
+ if ami_method in ['gaussian','kraskov1','kraskov2']:
for i in range(num_repeats):
- amis[i] = automutual_info(y + noise_range[i]*noise, tau, ami_method, str(extra_param))
+ amis[i] = automutual_info(y + noise_range[i]*noise, tau, ami_method, extra_param)
if np.isnan(amis[i]):
- raise ValueError('Error computing AMI: Time series too short (?)')
+ logger.warning('Error computing AMI: Time series too short (?)')
+ return np.nan
# Output statistics
out = {}
# Proportion decreases
@@ -250,6 +249,9 @@ def time_rev_kaplan(y: ArrayLike, time_lag: int = 1) -> float:
The time reversal asymmetry statistic.
"""
embedded = _lag_embed(np.asarray(y), 3, time_lag)
+ if np.isscalar(embedded):
+ # a scalar (nan) has been returned instead of an array
+ return np.nan
a = embedded[:, 0]
b = embedded[:, 1]
c = embedded[:, 2]
@@ -262,7 +264,8 @@ def _lag_embed(x: ArrayLike, m: int, lag: int = 1) -> ArrayLike:
x = np.asarray(x).flatten()
lx = len(x)
if lx < lag * (m - 1) + 1:
- raise ValueError("Time series is too short for the given dimension and lag.")
+ logger.warning("Time series is too short for the given dimension and lag.")
+ return np.nan
new_size = lx - lag * (m - 1)
y = np.zeros((new_size, m))
for i in range(m):
@@ -314,7 +317,8 @@ def embed2_angle_tau(y: ArrayLike, max_tau: int) -> dict:
theta = np.arctan(theta)
if len(theta) == 0:
- raise ValueError(f'Time series (N={len(y)}) too short for embedding')
+ logger.warning(f'Time series (N={len(y)}) too short for embedding')
+ return np.nan
stats_store[0, i] = autocorr(theta, 1, 'Fourier')[0]
stats_store[1, i] = autocorr(theta, 2, 'Fourier')[0]
@@ -1280,7 +1284,7 @@ def embed2_shapes(y: ArrayLike, tau: Union[str, int, None] = 'tau',
counts -= 1 # ignore self counts
if np.all(counts == 0):
- logging.warning("embed2_shapes: no counts detected!")
+ logger.warning("embed2_shapes: no counts detected!")
return np.nan
# Return basic statistics on the counts
@@ -1497,9 +1501,9 @@ def autocorr(y: ArrayLike, tau: Union[int, list] = 1,
if tau:
# if list is not empty
if np.max(tau) > N - 1: # -1 because acf(1) is lag 0
- logging.warning(f"Time lag {np.max(tau)} is too long for time-series length {N}.")
+ logger.warning(f"Time lag {np.max(tau)} is too long for time-series length {N}.")
if np.any(np.array(tau) < 0):
- logging.warning('Negative time lags not applicable.')
+ logger.warning('Negative time lags not applicable.')
if method == 'Fourier':
n_fft = 2 ** (int(np.ceil(np.log2(N))) + 1)
F = np.fft.fft(y - np.mean(y), n_fft)
@@ -1535,7 +1539,7 @@ def acf_y(t):
for i, t in enumerate(tau):
if np.any(np.isnan(y)):
good_r = (~np.isnan(y[:N-t])) & (~np.isnan(y[t:]))
- logging.info(f'NaNs in time series, computing for {np.sum(good_r)}/{len(good_r)} pairs of points.')
+ logger.info(f'NaNs in time series, computing for {np.sum(good_r)}/{len(good_r)} pairs of points.')
y1 = y[:N-t]
y1n = y1[good_r] - np.mean(y1[good_r])
y2 = y[t:]
@@ -1716,7 +1720,7 @@ def _stat_av(y: ArrayLike, window_stat: str = 'mean', num_seg: int = 5, inc_move
y = np.asarray(y)
win_length = np.floor(len(y)/num_seg)
if win_length == 0:
- logging.warning(f"Time-series of length {len(y)} is too short for {num_seg} windows")
+ logger.warning(f"Time-series of length {len(y)} is too short for {num_seg} windows")
return np.nan
inc = np.floor(win_length/inc_move) # increment to move at each step
# if increment rounded down to zero, prop it up
@@ -1785,7 +1789,7 @@ def autocorr_shape(y: ArrayLike, stop_when: Union[int, str] = 'pos_drown') -> di
for i in range(1, N+1):
acf_val = autocorr(y, i-1, 'Fourier')[0]
if np.isnan(acf_val):
- logging.warning("Weird time series (constant?)")
+ logger.warning("Weird time series (constant?)")
out = np.nan
if acf_val < th:
# Ensure ACF is all positive
@@ -1949,7 +1953,8 @@ def trev(y: ArrayLike, tau: Union[int, str] = 'ac') -> dict:
# tau is the first minimum of the automutual information function
tau = first_min(y, 'mi')
if np.isnan(tau):
- raise ValueError("No valid setting for time delay. (Is the time series too short?)")
+ logger.warning("No valid setting for time delay. (Is the time series too short?)")
+ return np.nan
# Compute trev quantities
yn = y[:-tau]
@@ -2018,7 +2023,8 @@ def tc3(y: list, tau: Union[int, str, None] = 'ac') -> dict:
tau = first_min(y, 'mi')
if np.isnan(tau):
- raise ValueError("No valid setting for time delay (time series too short?)")
+ logger.warning("No valid setting for time delay (time series too short?)")
+ return np.nan
# Compute tc3 statistic
yn = y[:-2*tau]
diff --git a/pyhctsa/operations/distribution.py b/pyhctsa/operations/distribution.py
index ab5eb1e..870cbfb 100644
--- a/pyhctsa/operations/distribution.py
+++ b/pyhctsa/operations/distribution.py
@@ -1,4 +1,5 @@
import logging
+logger = logging.getLogger('pyhctsa')
from typing import Dict, Union
import numpy as np
@@ -73,11 +74,11 @@ def compare_ks_fit(x: ArrayLike, what_distn: str) -> dict:
elif what_distn == 'exp':
# Check positivity
if np.any(x < 0):
- logging.warning("The data contains negative values, but Exponential is a positive-only distribution.")
+ logger.warning("The data contains negative values, but Exponential is a positive-only distribution.")
return np.nan
# Check constant
if np.all(x == x[0]):
- logging.warning("Data are a constant.")
+ logger.warning("Data are a constant.")
return np.nan
# Fit Exponential distribution (equivalent to expfit in MATLAB)
_, lam = expon.fit(x, floc=0) # force support at 0
@@ -91,7 +92,7 @@ def compare_ks_fit(x: ArrayLike, what_distn: str) -> dict:
elif what_distn == 'logn':
# Check positivity
if np.any(x <= 0):
- logging.warning("The data are not positive, but Log-Normal is a positive-only distribution.")
+ logger.warning("The data are not positive, but Log-Normal is a positive-only distribution.")
return np.nan
# Fit log-normal distribution
shape, loc, scale = lognorm.fit(x, floc=0) # sigma, 0, exp(mu)
@@ -537,7 +538,7 @@ def cv(x: ArrayLike, k: int = 1) -> float:
The coefficient of variation of order :math:`k`.
"""
if not isinstance(k, int) or k < 0:
- logging.warning('k should probably be a positive integer')
+ logger.warning('k should probably be a positive integer')
# carry on with just this warning, though
# Compute the coefficient of variation (of order k) of the data
@@ -718,7 +719,8 @@ def outlier_include(y: ArrayLike, threshold_how: str = 'abs', inc: float = 0.01)
raise ValueError(f"Invalid thresholdHow: '{threshold_how}'. Must be 'abs', 'pos', or 'neg'.")
if len(thresholds) == 0:
- raise ValueError("Error setting increments through the time-series values")
+ logger.warning("Error setting increments through the time-series values")
+ return np.nan
# Initialize statistics matrix
# Columns: [mean_diff, std_err, percentage, median_pos, mean_pos, std_pos]
diff --git a/pyhctsa/operations/entropy.py b/pyhctsa/operations/entropy.py
index 54c6cf3..64d2fe8 100644
--- a/pyhctsa/operations/entropy.py
+++ b/pyhctsa/operations/entropy.py
@@ -1,6 +1,7 @@
from math import factorial
from typing import Optional, Union
import logging
+logger = logging.getLogger('pyhctsa')
import numpy as np
from numpy.typing import ArrayLike
@@ -266,7 +267,7 @@ def multi_scale_entropy(
pp_text = f"after {pre_process_how} pre-processing"
else:
pp_text = ""
- logging.warning(f"Not enough samples ({len(y)} {pp_text}) to compute sample entropy at multiple scales")
+ logger.warning(f"Not enough samples ({len(y)} {pp_text}) to compute sample entropy at multiple scales")
return {'out': np.nan}
# Output raw values
diff --git a/pyhctsa/operations/graph.py b/pyhctsa/operations/graph.py
index 2b9be3b..77dae3e 100644
--- a/pyhctsa/operations/graph.py
+++ b/pyhctsa/operations/graph.py
@@ -4,6 +4,7 @@
from scipy.stats import expon, norm
from ts2vg import NaturalVG
import logging
+logger = logging.getLogger('pyhctsa')
from pyhctsa.operations.correlation import autocorr, first_crossing
from pyhctsa.operations.entropy import distribution_entropy
@@ -88,7 +89,7 @@ def visibility_graph(y: ArrayLike, meth: str = 'horiz', max_l: int = 5000) -> di
N = len(y)
if N > max_l:
# too long to store in memory
- logging.info(f"Time series ({N} > {max_l}) is too long for visibility graph."
+ logger.info(f"Time series ({N} > {max_l}) is too long for visibility graph."
f"Analyzing the first {max_l} samples.")
y = y[:max_l]
N = len(y)
diff --git a/pyhctsa/operations/information.py b/pyhctsa/operations/information.py
index 3c6c37a..346eff2 100644
--- a/pyhctsa/operations/information.py
+++ b/pyhctsa/operations/information.py
@@ -1,13 +1,14 @@
import logging
+logger = logging.getLogger('pyhctsa')
import os
from typing import Any, Dict, List, Optional, Union, Callable
-import jpype as jp
import numpy as np
from numpy.typing import ArrayLike
from scipy import stats
from ..utils import sign_change
+from ..toolboxes.infotheory.mutual_info import KraskovMI, GaussianMI
def _get_corr_fn(y: np.ndarray, min_what: str, extra_param: Union[int, float, None]) -> Callable:
"""Helper to return the correct correlation function based on method type."""
@@ -22,8 +23,6 @@ def _get_corr_fn(y: np.ndarray, min_what: str, extra_param: Union[int, float, No
return lambda x: automutual_info(y, x, 'kraskov2', extra_param)
elif min_what == 'mi-kraskov1':
return lambda x: automutual_info(y, x, 'kraskov1', extra_param)
- elif min_what == 'mi-kernel':
- return lambda x: automutual_info(y, x, 'kernel', extra_param)
elif min_what in ['mi', 'mi-gaussian']:
return lambda x: automutual_info(y, x, 'gaussian', extra_param)
else:
@@ -51,10 +50,9 @@ def first_min(
Automutual information (AMI):
- - ``'mi'``: AMI using the JIDT Gaussian estimator (default for AMI).
- - ``'mi-kernel'``: AMI using the JIDT kernel estimator.
- - ``'mi-kraskov1'``: AMI using the JIDT Kraskov estimator (variant 1).
- - ``'mi-kraskov2'``: AMI using the JIDT Kraskov estimator (variant 2).
+ - ``'mi'``: AMI using the Gaussian estimator (default for AMI).
+ - ``'mi-kraskov1'``: AMI using the Kraskov estimator (variant 1).
+ - ``'mi-kraskov2'``: AMI using the Kraskov estimator (variant 2).
- ``'mi-hist'``: AMI using a histogram-based estimator.
Default is ``'mi-gaussian'``.
@@ -77,7 +75,7 @@ def first_min(
auto_corr[i - 1] = corrfn(i)
if np.isnan(auto_corr[i - 1]):
- logging.warning(f"No minimum in {min_what}: encountered NaN.")
+ logger.warning(f"No minimum in {min_what}: encountered NaN.")
return np.nan
# Check for minimum
@@ -109,10 +107,9 @@ def first_max(
Automutual information (AMI):
- - ``'mi'``: AMI using the JIDT Gaussian estimator (default for AMI).
- - ``'mi-kernel'``: AMI using the JIDT kernel estimator.
- - ``'mi-kraskov1'``: AMI using the JIDT Kraskov estimator (variant 1).
- - ``'mi-kraskov2'``: AMI using the JIDT Kraskov estimator (variant 2).
+ - ``'mi'``: AMI using the Gaussian estimator (default for AMI).
+ - ``'mi-kraskov1'``: AMI using the Kraskov estimator (variant 1).
+ - ``'mi-kraskov2'``: AMI using the Kraskov estimator (variant 2).
- ``'mi-hist'``: AMI using a histogram-based estimator.
Default is ``'mi'``.
@@ -134,7 +131,7 @@ def first_max(
auto_corr[i - 1] = corrfn(i)
if np.isnan(auto_corr[i - 1]):
- logging.warning(f"No maximum in {max_what}: encountered NaN.")
+ logger.warning(f"No maximum in {max_what}: encountered NaN.")
return np.nan
# Check for maximum
@@ -189,7 +186,7 @@ def _mi_bin(v1: ArrayLike, v2: ArrayLike, r1: Union[str, list] = 'range',
if np.any(mask):
mi = np.sum(p_ij[mask] * np.log(p_ij[mask] / p_ixp_j[mask]))
else:
- logging.warning("The histograms aren't catching any points. Perhaps due to an inappropriate custom range for binning the data.")
+ logger.warning("The histograms aren't catching any points. Perhaps due to an inappropriate custom range for binning the data.")
mi = np.nan
return mi
@@ -228,7 +225,7 @@ def automutual_info_stats(
max_tau : int, optional
Maximum time delay to investigate. If None, uses N/4 where N is the length
of the time series, but won't exceed N/2. Default is `None`.
- est_method : {'gaussian', 'kernel', 'kraskov1', 'kraskov2'}, optional
+ est_method : {'gaussian', 'kraskov1', 'kraskov2'}, optional
Method for estimating mutual information (passed to automutual_info).
Default is ``'kernel'``.
extra_param : int or str, optional
@@ -321,18 +318,17 @@ def automutual_info_stats(
out['amiac1'] = autocorr(ami, 1, 'Fourier')[0]
return out
-
+
def automutual_info(
- y: ArrayLike,
- time_delay: Union[int, str, List[int]] = 1,
- est_method: str = 'kernel',
- extra_param: Optional[Union[int, str]] = None
-) -> Union[float, Dict[str, float]]:
+ y: ArrayLike,
+ time_delay: Union[int, str, List[int]] = 1,
+ est_method: str = 'gaussian',
+ extra_param: Optional[Union[int, str]] = None) -> Any:
"""
Compute time-delayed automutual information of a time series.
Calculates the mutual information between a time series and its time-delayed version
- using various estimation methods from the JIDT (Java Information Dynamics Toolkit).
+ using various estimation methods.
References
----------
@@ -353,11 +349,10 @@ def automutual_info(
Default is 1.
- est_method : {'gaussian', 'kernel', 'kraskov1', 'kraskov2'}, optional
+ est_method : {'gaussian', 'kraskov1', 'kraskov2'}, optional
Method for estimating mutual information:
- 'gaussian': Assumes Gaussian variables
- - 'kernel': Kernel density estimation (default)
- 'kraskov1': Kraskov estimator 1 (KSG1)
- 'kraskov2': Kraskov estimator 2 (KSG2)
@@ -366,7 +361,7 @@ def automutual_info(
extra_param : int or str, optional
Extra parameter for the estimator. For Kraskov estimators,
this sets the number of nearest neighbors 'k'.
- Default is 3.
+ Default is 4.
Returns
-------
@@ -376,7 +371,7 @@ def automutual_info(
If multiple time_delay:
dict: Keys are f"ami{delay}", values are corresponding AMI values
"""
- from ..operations.distribution import first_crossing
+ from ..operations.distribution import first_crossing # zzzz
if isinstance(time_delay, str) and time_delay in ['ac', 'tau']:
time_delay = first_crossing(y, corr_fun='ac', threshold=0, what_out='discrete')
@@ -384,6 +379,9 @@ def automutual_info(
y = np.asarray(y).flatten()
n = len(y)
min_samples = 5 # minimum 5 samples to compute mutual information (could make higher?)
+ kval = 4 # default
+ if extra_param:
+ kval = extra_param
# Loop over time delays if a vector
if not isinstance(time_delay, list):
@@ -394,14 +392,17 @@ def automutual_info(
if num_time_delays > 1:
time_delay = np.sort(time_delay)
-
- # initialise the MI calculator object if using non-Gaussian estimator
- if est_method != 'gaussian':
- # assumes the JVM has already been started up
- mi_calc = _initialize_MI(est_method=est_method, extra_param=extra_param, add_noise=False) # NO ADDED NOISE!
-
+
+ if est_method == 'kraskov1':
+ mi_calc = KraskovMI(k=kval, algorithm=1, add_noise=False) # no added noise
+ elif est_method == 'kraskov2':
+ mi_calc = KraskovMI(k=kval, algorithm=2, add_noise=False)
+ elif est_method == 'gaussian':
+ mi_calc = GaussianMI()
+ else:
+ raise ValueError(f'Unknown estimator: {est_method}')
+
for k, delay in enumerate(time_delay):
- # check enough samples to compute automutual info
if delay > n - min_samples:
# time series too short - keep the remaining values as NaNs
break
@@ -410,186 +411,22 @@ def automutual_info(
y1 = y[:-delay]
y2 = y[delay:]
- if est_method == 'gaussian':
- r, _ = stats.pearsonr(y1, y2)
- amis[k] = -0.5 * np.log(1 - r**2)
- else:
- # Reinitialize for Kraskov:
- mi_calc.initialise(1, 1)
- # Set observations to time-delayed versions of the time series:
- y1_jp = jp.JArray(jp.JDouble)(y1) # convert observations to java double
- y2_jp = jp.JArray(jp.JDouble)(y2)
- mi_calc.setObservations(y1_jp, y2_jp)
- # compute
- amis[k] = mi_calc.computeAverageLocalOfObservations()
-
+ amis[k] = mi_calc.compute(y1, y2)
+
if np.isnan(amis).any():
- logging.warning(
+ logger.warning(
f"Time series (n={n}) is too short for automutual information calculations "
f"up to lags of {max(time_delay)}"
)
-
+
if num_time_delays == 1:
# return a scalar if only one time delay
return amis[0]
+
else:
# return a dict for multiple time delays
return {f"ami{delay}": ami for delay, ami in zip(time_delay, amis)}
-def mutual_info(
- y1: ArrayLike,
- y2: ArrayLike,
- est_method: str = 'kernel',
- extra_param: Optional[Union[int, str]] = None
-) -> float:
- """
- Compute mutual information between two time series using JIDT estimators.
-
- This function calculates the mutual information between two time series using
- various estimators from the Java Information Dynamics Toolkit (JIDT).
-
- Note: This function requires the infodynamics.jar Java library and JPype.
-
- References
- ----------
- .. [1] Kraskov, A., Stoegbauer, H., Grassberger, P. (2004). Estimating mutual information.
- Physical Review E, 69(6), 066138. https://doi.org/10.1103/PhysRevE.69.066138
-
- Parameters
- ----------
- y1 : array-like
- First input time series.
- y2 : array-like
- Second input time series.
- est_method : str, optional
- Estimation method to use:
-
- - 'gaussian': Assumes Gaussian variables
- - 'kernel': Kernel density estimation (default)
- - 'kraskov1': Kraskov estimator 1 (KSG1)
- - 'kraskov2': Kraskov estimator 2 (KSG2)
-
- Default is `kernel`.
-
- extra_param : Union[int, str], optional
- Extra parameter for the estimator:
-
- - For Kraskov estimators: number of nearest neighbors 'k' (default: 3)
- - For other methods: ignored
-
- Default is `None`.
-
- Returns
- -------
- float
- Estimated mutual information between the input time series
- """
- # Initialize miCalc object (don't add noise!):
- mi_calc = _initialize_MI(est_method=est_method, extra_param=extra_param, add_noise=False)
- # Set observations to two time series:
- y1_jp = jp.JArray(jp.JDouble)(y1) # convert observations to java double
- y2_jp = jp.JArray(jp.JDouble)(y2) # convert observations to java double
- mi_calc.setObservations(y1_jp, y2_jp)
-
- # Compute mutual information
- out = mi_calc.computeAverageLocalOfObservations()
-
- return out
-
-def _initialize_MI(
- est_method: str = 'gaussian',
- extra_param: Optional[Union[int, str]] = None,
- add_noise: bool = False,
- verbose: bool = False
-) -> Any: # Returns a Java object, use Any since we can't type hint JPype objects
- """
- Initialize a mutual information calculator from JIDT (Java Information Dynamics Toolkit).
-
- Creates and configures a mutual information estimator by starting the JVM if needed,
- loading the appropriate JIDT class, and setting up the calculation parameters.
-
- Parameters
- ----------
- est_method : str, optional
-
- Estimation method to use:
- - 'gaussian': Assumes Gaussian variables (simplest)
- - 'kernel': Kernel density estimation
- - 'kraskov1': Kraskov estimator 1 (KSG1)
- - 'kraskov2': Kraskov estimator 2 (KSG2)
-
- Default is ``'gaussian'``.
-
- extra_param : Union[int, str], optional
- Configuration parameter for the estimator:
-
- - For Kraskov methods: number of nearest neighbors 'k'
- - For other methods: ignored
-
- Default is `None` (uses k=3 for Kraskov).
-
- add_noise : bool, optional
- Whether to add small random noise for Kraskov estimators:
-
- - True: Add noise (helpful for deterministic signals)
- - False: No noise.
-
- Default is `False`.
-
- verbose : bool, optional
- Display JVM debug info. Default is `False`.
-
- Returns
- -------
- Any
- Initialized JIDT mutual information calculator object.
- Type varies by estimation method chosen.
- """
-
- if not jp.isJVMStarted():
- jarloc = (
- os.path.dirname(os.path.abspath(__file__)) + "/../toolboxes/infodynamics-dist/infodynamics.jar"
- )
- # change to debug info
- if verbose:
- logging.debug(f"Starting JVM with java class {jarloc}.")
- jp.startJVM(jp.getDefaultJVMPath(), "-ea", "-Djava.class.path=" + jarloc, interrupt=False)
-
-
- if est_method == 'gaussian':
- implementing_class = 'infodynamics.measures.continuous.gaussian'
- mi_calc = jp.JPackage(implementing_class).MutualInfoCalculatorMultiVariateGaussian()
- elif est_method == 'kernel':
- implementing_class = 'infodynamics.measures.continuous.kernel'
- mi_calc = jp.JPackage(implementing_class).MutualInfoCalculatorMultiVariateKernel()
- elif est_method == 'kraskov1':
- implementing_class = 'infodynamics.measures.continuous.kraskov'
- mi_calc = jp.JPackage(implementing_class).MutualInfoCalculatorMultiVariateKraskov1()
- elif est_method == 'kraskov2':
- implementing_class = 'infodynamics.measures.continuous.kraskov'
- mi_calc = jp.JPackage(implementing_class).MutualInfoCalculatorMultiVariateKraskov2()
- else:
- raise ValueError(f"Unknown mutual information estimation method '{est_method}'")
-
- # Add neighest neighbor option for KSG estimator
- if est_method in ['kraskov1', 'kraskov2']:
- if extra_param is not None:
- if isinstance(extra_param, int):
- logging.warning("Number of nearest neighbors needs to be a string. Setting this for you...")
- extra_param = str(extra_param)
- mi_calc.setProperty('k', extra_param) # 4th input specifies number of nearest neighbors for KSG estimator
- else:
- mi_calc.setProperty('k', '3') # use 3 nearest neighbors for KSG estimator as default
-
- # Make deterministic if kraskov1 or 2 (which adds a small amount of noise to the signal by default)
- if (est_method in ['kraskov1', 'kraskov2']) and (add_noise is False):
- mi_calc.setProperty('NOISE_LEVEL_TO_ADD','0')
-
- # Specify a univariate calculation
- mi_calc.initialise(1,1)
-
- return mi_calc
-
def rm_automutual_information(y: ArrayLike, tau: int = 1) -> float:
"""
Estimates the mutual information of two stationary signals with
@@ -689,23 +526,23 @@ def _rm_info(*args):
len_y = y_shape[0]
if len(x_shape) != 1: # makes sure x is a row vector
- logging.warning("Invalid dimension of x")
+ logger.warning("Invalid dimension of x")
return
if len(y_shape) != 1:
- logging.warning("Invalid dimension of y")
+ logger.warning("Invalid dimension of y")
return
if len_x != len_y: # makes sure x and y have the same amount of elements
- logging.warning("Unequal length of x and y")
+ logger.warning("Unequal length of x and y")
return
if n_args > 5:
- logging.warning("Too many arguments")
+ logger.warning("Too many arguments")
return
if n_args < 2:
- logging.warning("Not enough arguments")
+ logger.warning("Not enough arguments")
return
# setting up variables depending on amount of inputs
@@ -869,19 +706,19 @@ def _rm_histogram_2(*args):
leny = yshape[0]
if len(xshape) != 1: # makes sure x is a row vector
- logging.warning("Invalid dimension of x")
+ logger.warning("Invalid dimension of x")
return
if len(yshape) != 1:
- logging.warning("Invalid dimension of y")
+ logger.warning("Invalid dimension of y")
return
if lenx != leny: # makes sure x and y have the same amount of elements
- logging.warning("Unequal length of x and y")
+ logger.warning("Unequal length of x and y")
return
if nargin > 3:
- logging.warning("Too many arguments")
+ logger.warning("Too many arguments")
return
if nargin == 2:
@@ -909,16 +746,16 @@ def _rm_histogram_2(*args):
# checking descriptor to make sure it is valid, otherwise print an error
if ncellx < 1:
- logging.warning("Invalid number of cells in X dimension")
+ logger.warning("Invalid number of cells in X dimension")
if ncelly < 1:
- logging.warning("Invalid number of cells in Y dimension")
+ logger.warning("Invalid number of cells in Y dimension")
if upperx <= lowerx:
- logging.warning("Invalid bounds in X dimension")
+ logger.warning("Invalid bounds in X dimension")
if uppery <= lowery:
- logging.warning("Invalid bounds in Y dimension")
+ logger.warning("Invalid bounds in Y dimension")
result = np.zeros([int(ncellx), int(ncelly)],
dtype=int) # should do the same thing as matlab: result(1:ncellx,1:ncelly) = 0;
diff --git a/pyhctsa/operations/model_fit.py b/pyhctsa/operations/model_fit.py
index 6255588..4b602ec 100644
--- a/pyhctsa/operations/model_fit.py
+++ b/pyhctsa/operations/model_fit.py
@@ -10,6 +10,7 @@
from statsmodels.tsa.ar_model import AutoReg, ar_select_order
from lmfit.models import SineModel
import logging
+logger = logging.getLogger('pyhctsa')
from ..operations.correlation import autocorr, first_crossing
from ..operations.stationarity import sliding_window
@@ -320,7 +321,7 @@ def local_simple(y: ArrayLike, forecast_meth: str = 'mean',
lp = train_length
evalr = np.arange(lp, N) #range over which to evaluate the forecast
if np.size(evalr) == 0:
- logging.warning("This time series is too short for forecasting")
+ logger.warning("This time series is too short for forecasting")
return np.nan
res = np.zeros(len(evalr))
if forecast_meth == 'mean':
@@ -401,15 +402,15 @@ def exp_smoothing(x: ArrayLike, n_train: Union[None, int, float] = None,
min_train, max_train = 100, 1000
if n_train > max_train:
- logging.info(f"Training set size reduced from {n_train} to {max_train}.")
+ logger.info(f"Training set size reduced from {n_train} to {max_train}.")
n_train = max_train
if n_train < min_train:
- logging.info(f"Training set size increased from {n_train} to {min_train}.")
+ logger.info(f"Training set size increased from {n_train} to {min_train}.")
n_train = min_train
if N < n_train:
- logging.warning("Time series is too short for the specified training size.")
+ logger.warning("Time series is too short for the specified training size.")
return np.nan
# --- Find Optimal Alpha ---
@@ -428,7 +429,7 @@ def exp_smoothing(x: ArrayLike, n_train: Union[None, int, float] = None,
# Check for valid RMSEs before fitting
valid_indices = ~np.isnan(rmses)
if np.sum(valid_indices) < 3:
- logging.info("Not enough valid points for quadratic fit; choosing best alpha from search.")
+ logger.info("Not enough valid points for quadratic fit; choosing best alpha from search.")
alphamin = alphar[np.nanargmin(rmses)] if np.any(valid_indices) else 0.5
else:
# Fit quadratic to the 3 points with the lowest RMSE
@@ -464,7 +465,7 @@ def exp_smoothing(x: ArrayLike, n_train: Union[None, int, float] = None,
valid_ref = ~np.isnan(rmses_ref)
if not np.any(valid_ref):
- logging.info("Could not compute RMSE in refined search; using previous alpha.")
+ logger.info("Could not compute RMSE in refined search; using previous alpha.")
else:
p2 = np.polyfit(alphar_ref[valid_ref], rmses_ref[valid_ref], 2)
if p2[0] < 0: # Bad fit, fallback to best alpha in search
@@ -476,14 +477,15 @@ def exp_smoothing(x: ArrayLike, n_train: Union[None, int, float] = None,
out['alphamin'] = alpha
if np.isnan(alpha):
- raise ValueError("Alpha optimization failed, resulting in NaN.")
+ logger.warning("Alpha optimization failed, resulting in NaN.")
+ return np.nan
# --- Final Fit and Residual Analysis ---
y_fit = _fit_exp_smooth(x, alpha)
yp, xp = y_fit[2:], x[2:]
if len(yp) < 2:
- logging.warning("Not enough points to calculate residual statistics.")
+ logger.warning("Not enough points to calculate residual statistics.")
residout = {'mean': np.nan, 'std': np.nan, 'AC1': np.nan}
else:
residuals = yp - xp
@@ -664,7 +666,7 @@ def _get_criteria(sel, N, crit = "aic"):
se.pop(0)
keys = se.keys()
else:
- return ValueError(f"Unknown crtieria: {crit}!")
+ raise ValueError(f"Unknown criteria: {crit}!")
orlist = np.array([i[-1] for i in list(keys)])
ps_len = len(keys)
diff --git a/pyhctsa/operations/nonlinearity.py b/pyhctsa/operations/nonlinearity.py
index 728ae2b..a40c01f 100644
--- a/pyhctsa/operations/nonlinearity.py
+++ b/pyhctsa/operations/nonlinearity.py
@@ -3,6 +3,7 @@
import numpy as np
from numpy.typing import ArrayLike
import logging
+logger = logging.getLogger('pyhctsa')
from sklearn.decomposition import PCA
@@ -84,7 +85,7 @@ def _ms_embed(z, v, w):
lags = np.sort(lags)
dim = len(lags)
if n <= lags[-1]:
- logging.warning("Vector is too small to be embedded with the given lags.")
+ logger.warning("Vector is too small to be embedded with the given lags.")
return np.full((dim, 1), np.nan), None
w_win = lags[-1] - lags[0] # window width (renamed to avoid shadowing arg)
@@ -130,7 +131,8 @@ def _ms_nlpe(y: ArrayLike, de: int, tau: int) -> float:
y = y.squeeze() # (1, m) -> (m,)
if x is None or x.size == 0:
- raise ValueError("Error embedding the time series.")
+ logger.warning("Error embedding the time series.")
+ return np.nan
de_dim, n = x.shape
@@ -246,21 +248,25 @@ def nlpe(y: ArrayLike, de: int = 3, tau: Union[int, str] = 1, max_n: int = 5000)
raise ValueError("tau can be either 'mi' or 'ac'")
# check the tau
if np.isnan(tau):
- raise ValueError('Time series cannot be embedded (too short?)')
+ logger.warning('Time series cannot be embedded (too short?)')
+ return np.nan
#% nlpe can cause memory pains for long time series
#% Let's do this dirty cheat
if n > max_n:
# crop the time series to the first max_n samples
y = y[:max_n]
- logging.info(f"Michael Small's nlpe code is only being evaluated on the first {max_n} (/{n}) samples.")
+ logger.info(f"Michael Small's nlpe code is only being evaluated on the first {max_n} (/{n}) samples.")
n = max_n
if n < 20: # short time series cause problems
- logging.warning(f'Time series (N = {len(y)}) is too short.')
+ logger.warning(f'Time series (N = {len(y)}) is too short.')
return np.nan
# run the nonlinear prediction error code
res = _ms_nlpe(y, de, tau)
+ if np.isscalar(res) and np.isnan(res):
+ # a scalar nan has been returned instead of expected array
+ return np.nan
# compute outputs
out = {}
@@ -304,18 +310,18 @@ def embed_pca(y: ArrayLike, tau: Union[str, int] = 'ac', m: int = 3) -> dict:
if tau == 'ac':
tau = first_crossing(y, 'ac', 0, 'discrete')
if np.isnan(tau):
- logging.warning('Could not get time delay by ACF (time series too short?)')
+ logger.warning('Could not get time delay by ACF (time series too short?)')
return np.nan
elif tau == 'mi':
tau = first_min(y, 'mi')
if np.isnan(tau):
- logging.warning('Could not get time delay by mutual information (time series too short?)')
+ logger.warning('Could not get time delay by mutual information (time series too short?)')
return np.nan
else:
raise ValueError(f'Invalid time-delay method: {tau}. Choose either mi or ac.')
n_embed = n - (m-1)*tau
if n_embed <= 0:
- logging.warning(f'Time series (N = {n}) too short to embed with these embedding parameters.')
+ logger.warning(f'Time series (N = {n}) too short to embed with these embedding parameters.')
return np.nan
y_embed = np.zeros((n_embed, m))
diff --git a/pyhctsa/operations/pre_process.py b/pyhctsa/operations/pre_process.py
index ecab178..704f82a 100644
--- a/pyhctsa/operations/pre_process.py
+++ b/pyhctsa/operations/pre_process.py
@@ -3,6 +3,7 @@
from scipy.signal import lfilter, resample_poly
from statsmodels.tsa.tsatools import detrend
import logging
+logger = logging.getLogger('pyhctsa')
from ..operations.distribution import outlier_test
from ..operations.stationarity import sliding_window, stat_av
@@ -115,7 +116,7 @@ def preproc_compare(y: ArrayLike, detrend_meth: str = 'medianf') -> dict:
try:
order = int(order)
except ValueError:
- logging.warning(f"Could not convert order: `{order}' to integer.")
+ logger.warning(f"Could not convert order: `{order}' to integer.")
y_d = detrend(y, order=order, axis=0)
# 2) Differencing
@@ -127,7 +128,7 @@ def preproc_compare(y: ArrayLike, detrend_meth: str = 'medianf') -> dict:
try:
ndiff = int(ndiff)
except ValueError:
- logging.warning(f"Could not convert ndiff: `{ndiff}' to integer.")
+ logger.warning(f"Could not convert ndiff: `{ndiff}' to integer.")
y_d = np.diff(y, n=ndiff, axis=0)
# 3) Median filter
@@ -139,7 +140,7 @@ def preproc_compare(y: ArrayLike, detrend_meth: str = 'medianf') -> dict:
try:
med_ord = int(med_ord)
except ValueError:
- logging.warning(f"Could not convert median order: `{med_ord}' to integer.")
+ logger.warning(f"Could not convert median order: `{med_ord}' to integer.")
y_d = _med_filt_1d(y, med_ord)
# 4) Running average
@@ -151,7 +152,7 @@ def preproc_compare(y: ArrayLike, detrend_meth: str = 'medianf') -> dict:
try:
rav_wsize = int(rav_wsize)
except ValueError:
- logging.warning(f"Could not running average window size: `{rav_wsize}' to integer.")
+ logger.warning(f"Could not running average window size: `{rav_wsize}' to integer.")
y_d = lfilter(np.ones(rav_wsize)/rav_wsize, [1], y)
elif 'resample' in detrend_meth:
diff --git a/pyhctsa/operations/scaling.py b/pyhctsa/operations/scaling.py
index 3f62c32..66cfc8c 100644
--- a/pyhctsa/operations/scaling.py
+++ b/pyhctsa/operations/scaling.py
@@ -6,6 +6,7 @@
from scipy.interpolate import interp1d
import statsmodels.api as sm
import logging
+logger = logging.getLogger('pyhctsa')
from ..toolboxes.Max_Little import fastdfa
from ..utils import make_mat_buffer
@@ -124,7 +125,7 @@ def fluctuation_analysis(x: ArrayLike, q: Union[float, int] = 2,
taur = np.arange(5, np.floor(N/2) + 1, tau_step) # maybe increased??
ntau = len(taur) # analyze the time series across this many timescales
if ntau < 8: # fewer than 8 points
- logging.warning(f'This time series (N = {N}) is too short to analyze using this fluctuation analysis.')
+ logger.warning(f'This time series (N = {N}) is too short to analyze using this fluctuation analysis.')
out = np.nan
return out
diff --git a/pyhctsa/operations/stationarity.py b/pyhctsa/operations/stationarity.py
index a85bf52..4f8a71d 100644
--- a/pyhctsa/operations/stationarity.py
+++ b/pyhctsa/operations/stationarity.py
@@ -1,4 +1,5 @@
import logging
+logger = logging.getLogger('pyhctsa')
import warnings
from typing import Union
@@ -251,7 +252,8 @@ def moment_corr(x: ArrayLike, window_length: Union[None, float] = None,
points_per_window = np.size(x_buff, 0)
if points_per_window == 1:
- raise ValueError(f"This time series (N = {N}) is too short to extract {num_windows}")
+ logger.warning(f"This time series (N = {N}) is too short to extract {num_windows}")
+ return np.nan
# okay now we have the sliding window ('buffered') signal, x_buff
# first calculate the first moment in all the windows
@@ -264,7 +266,6 @@ def moment_corr(x: ArrayLike, window_length: Union[None, float] = None,
#out['R'] = R
out['absR'] = np.abs(rmat[0, 1])
out['density'] = np.ptp(M1) * np.ptp(M2) / N
- #out['mi'] = MutualInfo(M1, M2, 'gaussian')
return out
@@ -723,7 +724,7 @@ def local_global(y: ArrayLike, subset_how: str = 'l', n: Union[int, float, None]
if len(r) < 5:
# It's not really appropriate to compute statistics on less than 5 datapoints
- logging.warning(f"Time series (of length {N}) is too short")
+ logger.warning(f"Time series (of length {N}) is too short")
return np.nan
# Compare statistics of this subset to those obtained from the full time series
@@ -825,7 +826,8 @@ def std_nth_deriv(y: ArrayLike, ndr: int = 2) -> float:
y = np.asarray(y)
yd = np.diff(y, n=ndr)
if len(yd) == 0:
- raise ValueError(f"Time series (N = {len(y)}) too short to compute differences at n = {n}")
+ logger.warning(f"Time series (N = {len(y)}) too short to compute differences at n = {n}")
+ return np.nan
out = np.std(yd, ddof=1)
return float(out)
@@ -937,7 +939,7 @@ def stat_av(y: ArrayLike, what_type: str = 'seg', extra_param: int = 5) -> float
pn = int(np.floor(N / extra_param))
M = np.array([np.mean(y[j*extra_param:(j+1)*extra_param]) for j in range(pn)])
else:
- logging.warning(f"This time series (N = {N}) is too short for stat_av({what_type},'{extra_param}')")
+ logger.warning(f"This time series (N = {N}) is too short for stat_av({what_type},'{extra_param}')")
return np.nan
else:
raise ValueError(f"Error evaluating stat_av of type '{what_type}', please select either 'seg' or 'len'")
@@ -1014,7 +1016,7 @@ def sliding_window(y: ArrayLike, window_stat: str = 'mean', across_win_stat: str
y = np.asarray(y)
win_length = np.floor(len(y)/num_seg)
if win_length == 0:
- logging.warning(f"Time-series of length {len(y)} is too short for {num_seg} windows")
+ logger.warning(f"Time-series of length {len(y)} is too short for {num_seg} windows")
return np.nan
inc = np.floor(win_length/inc_move) # increment to move at each step
# if incrment rounded down to zero, prop it up
diff --git a/pyhctsa/operations/surrogates.py b/pyhctsa/operations/surrogates.py
index 74be9b6..83aa29c 100644
--- a/pyhctsa/operations/surrogates.py
+++ b/pyhctsa/operations/surrogates.py
@@ -1,12 +1,14 @@
import warnings
from typing import Union
+import logging
+logger = logging.getLogger('pyhctsa')
import numpy as np
from numpy.typing import ArrayLike
from scipy.stats import gaussian_kde, norm, zmap
from ..operations.correlation import tc3
-from ..operations.information import automutual_info, first_min
+from ..operations.information import automutual_info, first_min, automutual_info
warnings.filterwarnings("ignore", category=RuntimeWarning)
@@ -15,7 +17,8 @@ def sd_give_me_stats(stat_x: float, stat_surr: ArrayLike, left_right_both: str)
num_surrs = len(stat_surr)
out = {}
if np.isnan(stat_surr).any():
- raise ValueError("SDgivemestats failed")
+ logger.warning("SDgivemestats failed")
+ return np.nan
#% ASSUME GAUSSIAN DISTRIBUTION:
#% so can use 1/2-sided z-statistic
z_stat = zmap(np.atleast_1d(stat_x), stat_surr, ddof=1)[0]
@@ -276,7 +279,8 @@ def surrogate_test(
fmmi_surr[i] = np.nan
if np.isnan(fmmi_surr).any():
- raise ValueError("fmmi failed")
+ logger.warning("fmmi failed")
+ return np.nan
#% FMMI should be higher for signal than surrogates
some_stats = sd_give_me_stats(fmmi_x, fmmi_surr, 'right')
for (k, v) in zip(some_stats.keys(), some_stats.values()):
diff --git a/pyhctsa/operations/symbolic.py b/pyhctsa/operations/symbolic.py
index 06bd13c..3b56ca4 100644
--- a/pyhctsa/operations/symbolic.py
+++ b/pyhctsa/operations/symbolic.py
@@ -2,6 +2,7 @@
from typing import Union
from numpy.typing import ArrayLike
import logging
+logger = logging.getLogger('pyhctsa')
from scipy.stats import mstats
from scipy.signal import resample as ssre
@@ -185,7 +186,7 @@ def motif_two(y: ArrayLike, binarize_how: str = 'diff') -> dict:
N = len(y_bin)
if N < 5:
- logging.warning("Time series too short!")
+ logger.warning("Time series too short!")
return np.nan
# Binary sequences of length 1
r1 = (y_bin == 1) # 1
@@ -604,12 +605,14 @@ def transition_matrix(y: ArrayLike, how_to_cg: str = 'quantile',
# check inputs
y = np.asarray(y)
if num_groups < 2:
- raise ValueError("Too few groups for coarse-graining")
+ logger.warning("Too few groups for coarse-graining")
+ return np.nan
if tau == 'ac':
# determine the tau from first zero of the ACF
tau = first_crossing(y, 'ac', 0, 'discrete')
if np.isnan(tau):
- raise ValueError("Time series too short to estimate tau")
+ logger.warning("Time series too short to estimate tau")
+ return np.nan
if tau > 1: # calculate transition matrix at a non-unit lag
# downsample at rate 1:tau
y = ssre(y, int(np.ceil(len(y) / tau)))
diff --git a/pyhctsa/operations/wavelet.py b/pyhctsa/operations/wavelet.py
index 7efcf90..52280b3 100644
--- a/pyhctsa/operations/wavelet.py
+++ b/pyhctsa/operations/wavelet.py
@@ -8,6 +8,7 @@
from numpy.typing import ArrayLike
import pywt
import logging
+logger = logging.getLogger('pyhctsa')
from ..utils import sign_change
from pywt._extensions._pywt import (
@@ -252,9 +253,9 @@ def scal_2_freq(y: ArrayLike, w_name: str = 'db3', a_max: int = 5, delta: int =
if a_max == 'max':
a_max = max_level
if max_level < a_max:
- logging.info(f'Chosen level {a_max} is too large for this wavelet on this signal...')
+ logger.info(f'Chosen level {a_max} is too large for this wavelet on this signal...')
a_max = max_level # set to max allowed level
- logging.info(f'changed to maximum level computed with wmaxlev: {a_max}')
+ logger.info(f'changed to maximum level computed with wmaxlev: {a_max}')
# % Define scales.
scales = np.arange(1, a_max+1)
@@ -306,7 +307,7 @@ def dwt_coeff(y: ArrayLike, w_name: str = 'db3', level: int = 3) -> dict:
level = pywt.dwt_max_level(N, w_name)
max_level_allowed = pywt.dwt_max_level(N, w_name)
if max_level_allowed < level:
- logging.warning("Chosen level is too large for this wavelet on this signal....\n")
+ logger.warning("Chosen level is too large for this wavelet on this signal....\n")
#%% Perform Wavelet Decomposition
C, L = None, None
if max_level_allowed < level: # if level exceeds max level, just use max level instead
@@ -465,9 +466,9 @@ def detail_coeffs(y: ArrayLike, w_name: str = 'db3', max_level: Union[int, str]
if max_level == 'max':
max_level = pywt.dwt_max_level(N, w_name)
if pywt.dwt_max_level(N, w_name) < max_level:
- logging.info(f"Chosen wavelet level is too large for the {w_name} wavelet for this signal of length N = {N}")
+ logger.info(f"Chosen wavelet level is too large for the {w_name} wavelet for this signal of length N = {N}")
max_level = pywt.dwt_max_level(N, w_name)
- logging.info(f"Using a wavelet level of {max_level} instead.")
+ logger.info(f"Using a wavelet level of {max_level} instead.")
# Perform a single-level wavelet decomposition
means = np.zeros(max_level) # mean detail coefficient magnitude at each level
medians = np.zeros(max_level) # median detail coefficient magnitude at each level
@@ -553,10 +554,12 @@ def wl_coeffs(y: ArrayLike, w_name: str = 'db3', level: Union[int, str] = 3) ->
if level == 'max':
level = pywt.dwt_max_level(N, w_name)
if level == 0:
- raise ValueError("Cannot compute wavelet coefficients (short time series)")
+ logger.warning("Cannot compute wavelet coefficients (short time series)")
+ return np.nan
if pywt.dwt_max_level(N, w_name) < level:
- raise ValueError(f"Chosen level, {level}, is too large for this wavelet on this signal.")
+ logger.warning(f"Chosen level, {level}, is too large for this wavelet on this signal.")
+ return np.nan
C, L = wavedec(y, wavelet=w_name, level=level)
det = wrcoef(C, L, w_name, level)
diff --git a/pyhctsa/toolboxes/infodynamics-dist/build.xml b/pyhctsa/toolboxes/infodynamics-dist/build.xml
deleted file mode 100644
index a75538b..0000000
--- a/pyhctsa/toolboxes/infodynamics-dist/build.xml
+++ /dev/null
@@ -1,324 +0,0 @@
-
-
-
- Build file for the Java Information Dynamics Toolkit
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- .block ul li {list-style:disc; margin-left: 20px;}
- .block ol li {list-style:decimal; margin-left: 40px;}
- .block ol li ol li {list-style:lower-alpha; margin-left: 40px;}
- .block ol li ul li {list-style:disc; margin-left: 40px;}
- .block ol li ul li ol li {list-style:lower-alpha; margin-left: 40px;}
- .block ol li ol li ol li {list-style:lower-roman; margin-left: 40px;}
- .block ul li ol li {list-style:decimal; margin-left: 20px;}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/pyhctsa/toolboxes/infodynamics-dist/infodynamics.jar b/pyhctsa/toolboxes/infodynamics-dist/infodynamics.jar
deleted file mode 100755
index 3a6a6f2..0000000
Binary files a/pyhctsa/toolboxes/infodynamics-dist/infodynamics.jar and /dev/null differ
diff --git a/pyhctsa/toolboxes/infodynamics-dist/license-gplv3.txt b/pyhctsa/toolboxes/infodynamics-dist/license-gplv3.txt
deleted file mode 100644
index 94a9ed0..0000000
--- a/pyhctsa/toolboxes/infodynamics-dist/license-gplv3.txt
+++ /dev/null
@@ -1,674 +0,0 @@
- GNU GENERAL PUBLIC LICENSE
- Version 3, 29 June 2007
-
- Copyright (C) 2007 Free Software Foundation, Inc.
- Everyone is permitted to copy and distribute verbatim copies
- of this license document, but changing it is not allowed.
-
- Preamble
-
- The GNU General Public License is a free, copyleft license for
-software and other kinds of works.
-
- The licenses for most software and other practical works are designed
-to take away your freedom to share and change the works. By contrast,
-the GNU General Public License is intended to guarantee your freedom to
-share and change all versions of a program--to make sure it remains free
-software for all its users. We, the Free Software Foundation, use the
-GNU General Public License for most of our software; it applies also to
-any other work released this way by its authors. You can apply it to
-your programs, too.
-
- When we speak of free software, we are referring to freedom, not
-price. Our General Public Licenses are designed to make sure that you
-have the freedom to distribute copies of free software (and charge for
-them if you wish), that you receive source code or can get it if you
-want it, that you can change the software or use pieces of it in new
-free programs, and that you know you can do these things.
-
- To protect your rights, we need to prevent others from denying you
-these rights or asking you to surrender the rights. Therefore, you have
-certain responsibilities if you distribute copies of the software, or if
-you modify it: responsibilities to respect the freedom of others.
-
- For example, if you distribute copies of such a program, whether
-gratis or for a fee, you must pass on to the recipients the same
-freedoms that you received. You must make sure that they, too, receive
-or can get the source code. And you must show them these terms so they
-know their rights.
-
- Developers that use the GNU GPL protect your rights with two steps:
-(1) assert copyright on the software, and (2) offer you this License
-giving you legal permission to copy, distribute and/or modify it.
-
- For the developers' and authors' protection, the GPL clearly explains
-that there is no warranty for this free software. For both users' and
-authors' sake, the GPL requires that modified versions be marked as
-changed, so that their problems will not be attributed erroneously to
-authors of previous versions.
-
- Some devices are designed to deny users access to install or run
-modified versions of the software inside them, although the manufacturer
-can do so. This is fundamentally incompatible with the aim of
-protecting users' freedom to change the software. The systematic
-pattern of such abuse occurs in the area of products for individuals to
-use, which is precisely where it is most unacceptable. Therefore, we
-have designed this version of the GPL to prohibit the practice for those
-products. If such problems arise substantially in other domains, we
-stand ready to extend this provision to those domains in future versions
-of the GPL, as needed to protect the freedom of users.
-
- Finally, every program is threatened constantly by software patents.
-States should not allow patents to restrict development and use of
-software on general-purpose computers, but in those that do, we wish to
-avoid the special danger that patents applied to a free program could
-make it effectively proprietary. To prevent this, the GPL assures that
-patents cannot be used to render the program non-free.
-
- The precise terms and conditions for copying, distribution and
-modification follow.
-
- TERMS AND CONDITIONS
-
- 0. Definitions.
-
- "This License" refers to version 3 of the GNU General Public License.
-
- "Copyright" also means copyright-like laws that apply to other kinds of
-works, such as semiconductor masks.
-
- "The Program" refers to any copyrightable work licensed under this
-License. Each licensee is addressed as "you". "Licensees" and
-"recipients" may be individuals or organizations.
-
- To "modify" a work means to copy from or adapt all or part of the work
-in a fashion requiring copyright permission, other than the making of an
-exact copy. The resulting work is called a "modified version" of the
-earlier work or a work "based on" the earlier work.
-
- A "covered work" means either the unmodified Program or a work based
-on the Program.
-
- To "propagate" a work means to do anything with it that, without
-permission, would make you directly or secondarily liable for
-infringement under applicable copyright law, except executing it on a
-computer or modifying a private copy. Propagation includes copying,
-distribution (with or without modification), making available to the
-public, and in some countries other activities as well.
-
- To "convey" a work means any kind of propagation that enables other
-parties to make or receive copies. Mere interaction with a user through
-a computer network, with no transfer of a copy, is not conveying.
-
- An interactive user interface displays "Appropriate Legal Notices"
-to the extent that it includes a convenient and prominently visible
-feature that (1) displays an appropriate copyright notice, and (2)
-tells the user that there is no warranty for the work (except to the
-extent that warranties are provided), that licensees may convey the
-work under this License, and how to view a copy of this License. If
-the interface presents a list of user commands or options, such as a
-menu, a prominent item in the list meets this criterion.
-
- 1. Source Code.
-
- The "source code" for a work means the preferred form of the work
-for making modifications to it. "Object code" means any non-source
-form of a work.
-
- A "Standard Interface" means an interface that either is an official
-standard defined by a recognized standards body, or, in the case of
-interfaces specified for a particular programming language, one that
-is widely used among developers working in that language.
-
- The "System Libraries" of an executable work include anything, other
-than the work as a whole, that (a) is included in the normal form of
-packaging a Major Component, but which is not part of that Major
-Component, and (b) serves only to enable use of the work with that
-Major Component, or to implement a Standard Interface for which an
-implementation is available to the public in source code form. A
-"Major Component", in this context, means a major essential component
-(kernel, window system, and so on) of the specific operating system
-(if any) on which the executable work runs, or a compiler used to
-produce the work, or an object code interpreter used to run it.
-
- The "Corresponding Source" for a work in object code form means all
-the source code needed to generate, install, and (for an executable
-work) run the object code and to modify the work, including scripts to
-control those activities. However, it does not include the work's
-System Libraries, or general-purpose tools or generally available free
-programs which are used unmodified in performing those activities but
-which are not part of the work. For example, Corresponding Source
-includes interface definition files associated with source files for
-the work, and the source code for shared libraries and dynamically
-linked subprograms that the work is specifically designed to require,
-such as by intimate data communication or control flow between those
-subprograms and other parts of the work.
-
- The Corresponding Source need not include anything that users
-can regenerate automatically from other parts of the Corresponding
-Source.
-
- The Corresponding Source for a work in source code form is that
-same work.
-
- 2. Basic Permissions.
-
- All rights granted under this License are granted for the term of
-copyright on the Program, and are irrevocable provided the stated
-conditions are met. This License explicitly affirms your unlimited
-permission to run the unmodified Program. The output from running a
-covered work is covered by this License only if the output, given its
-content, constitutes a covered work. This License acknowledges your
-rights of fair use or other equivalent, as provided by copyright law.
-
- You may make, run and propagate covered works that you do not
-convey, without conditions so long as your license otherwise remains
-in force. You may convey covered works to others for the sole purpose
-of having them make modifications exclusively for you, or provide you
-with facilities for running those works, provided that you comply with
-the terms of this License in conveying all material for which you do
-not control copyright. Those thus making or running the covered works
-for you must do so exclusively on your behalf, under your direction
-and control, on terms that prohibit them from making any copies of
-your copyrighted material outside their relationship with you.
-
- Conveying under any other circumstances is permitted solely under
-the conditions stated below. Sublicensing is not allowed; section 10
-makes it unnecessary.
-
- 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
-
- No covered work shall be deemed part of an effective technological
-measure under any applicable law fulfilling obligations under article
-11 of the WIPO copyright treaty adopted on 20 December 1996, or
-similar laws prohibiting or restricting circumvention of such
-measures.
-
- When you convey a covered work, you waive any legal power to forbid
-circumvention of technological measures to the extent such circumvention
-is effected by exercising rights under this License with respect to
-the covered work, and you disclaim any intention to limit operation or
-modification of the work as a means of enforcing, against the work's
-users, your or third parties' legal rights to forbid circumvention of
-technological measures.
-
- 4. Conveying Verbatim Copies.
-
- You may convey verbatim copies of the Program's source code as you
-receive it, in any medium, provided that you conspicuously and
-appropriately publish on each copy an appropriate copyright notice;
-keep intact all notices stating that this License and any
-non-permissive terms added in accord with section 7 apply to the code;
-keep intact all notices of the absence of any warranty; and give all
-recipients a copy of this License along with the Program.
-
- You may charge any price or no price for each copy that you convey,
-and you may offer support or warranty protection for a fee.
-
- 5. Conveying Modified Source Versions.
-
- You may convey a work based on the Program, or the modifications to
-produce it from the Program, in the form of source code under the
-terms of section 4, provided that you also meet all of these conditions:
-
- a) The work must carry prominent notices stating that you modified
- it, and giving a relevant date.
-
- b) The work must carry prominent notices stating that it is
- released under this License and any conditions added under section
- 7. This requirement modifies the requirement in section 4 to
- "keep intact all notices".
-
- c) You must license the entire work, as a whole, under this
- License to anyone who comes into possession of a copy. This
- License will therefore apply, along with any applicable section 7
- additional terms, to the whole of the work, and all its parts,
- regardless of how they are packaged. This License gives no
- permission to license the work in any other way, but it does not
- invalidate such permission if you have separately received it.
-
- d) If the work has interactive user interfaces, each must display
- Appropriate Legal Notices; however, if the Program has interactive
- interfaces that do not display Appropriate Legal Notices, your
- work need not make them do so.
-
- A compilation of a covered work with other separate and independent
-works, which are not by their nature extensions of the covered work,
-and which are not combined with it such as to form a larger program,
-in or on a volume of a storage or distribution medium, is called an
-"aggregate" if the compilation and its resulting copyright are not
-used to limit the access or legal rights of the compilation's users
-beyond what the individual works permit. Inclusion of a covered work
-in an aggregate does not cause this License to apply to the other
-parts of the aggregate.
-
- 6. Conveying Non-Source Forms.
-
- You may convey a covered work in object code form under the terms
-of sections 4 and 5, provided that you also convey the
-machine-readable Corresponding Source under the terms of this License,
-in one of these ways:
-
- a) Convey the object code in, or embodied in, a physical product
- (including a physical distribution medium), accompanied by the
- Corresponding Source fixed on a durable physical medium
- customarily used for software interchange.
-
- b) Convey the object code in, or embodied in, a physical product
- (including a physical distribution medium), accompanied by a
- written offer, valid for at least three years and valid for as
- long as you offer spare parts or customer support for that product
- model, to give anyone who possesses the object code either (1) a
- copy of the Corresponding Source for all the software in the
- product that is covered by this License, on a durable physical
- medium customarily used for software interchange, for a price no
- more than your reasonable cost of physically performing this
- conveying of source, or (2) access to copy the
- Corresponding Source from a network server at no charge.
-
- c) Convey individual copies of the object code with a copy of the
- written offer to provide the Corresponding Source. This
- alternative is allowed only occasionally and noncommercially, and
- only if you received the object code with such an offer, in accord
- with subsection 6b.
-
- d) Convey the object code by offering access from a designated
- place (gratis or for a charge), and offer equivalent access to the
- Corresponding Source in the same way through the same place at no
- further charge. You need not require recipients to copy the
- Corresponding Source along with the object code. If the place to
- copy the object code is a network server, the Corresponding Source
- may be on a different server (operated by you or a third party)
- that supports equivalent copying facilities, provided you maintain
- clear directions next to the object code saying where to find the
- Corresponding Source. Regardless of what server hosts the
- Corresponding Source, you remain obligated to ensure that it is
- available for as long as needed to satisfy these requirements.
-
- e) Convey the object code using peer-to-peer transmission, provided
- you inform other peers where the object code and Corresponding
- Source of the work are being offered to the general public at no
- charge under subsection 6d.
-
- A separable portion of the object code, whose source code is excluded
-from the Corresponding Source as a System Library, need not be
-included in conveying the object code work.
-
- A "User Product" is either (1) a "consumer product", which means any
-tangible personal property which is normally used for personal, family,
-or household purposes, or (2) anything designed or sold for incorporation
-into a dwelling. In determining whether a product is a consumer product,
-doubtful cases shall be resolved in favor of coverage. For a particular
-product received by a particular user, "normally used" refers to a
-typical or common use of that class of product, regardless of the status
-of the particular user or of the way in which the particular user
-actually uses, or expects or is expected to use, the product. A product
-is a consumer product regardless of whether the product has substantial
-commercial, industrial or non-consumer uses, unless such uses represent
-the only significant mode of use of the product.
-
- "Installation Information" for a User Product means any methods,
-procedures, authorization keys, or other information required to install
-and execute modified versions of a covered work in that User Product from
-a modified version of its Corresponding Source. The information must
-suffice to ensure that the continued functioning of the modified object
-code is in no case prevented or interfered with solely because
-modification has been made.
-
- If you convey an object code work under this section in, or with, or
-specifically for use in, a User Product, and the conveying occurs as
-part of a transaction in which the right of possession and use of the
-User Product is transferred to the recipient in perpetuity or for a
-fixed term (regardless of how the transaction is characterized), the
-Corresponding Source conveyed under this section must be accompanied
-by the Installation Information. But this requirement does not apply
-if neither you nor any third party retains the ability to install
-modified object code on the User Product (for example, the work has
-been installed in ROM).
-
- The requirement to provide Installation Information does not include a
-requirement to continue to provide support service, warranty, or updates
-for a work that has been modified or installed by the recipient, or for
-the User Product in which it has been modified or installed. Access to a
-network may be denied when the modification itself materially and
-adversely affects the operation of the network or violates the rules and
-protocols for communication across the network.
-
- Corresponding Source conveyed, and Installation Information provided,
-in accord with this section must be in a format that is publicly
-documented (and with an implementation available to the public in
-source code form), and must require no special password or key for
-unpacking, reading or copying.
-
- 7. Additional Terms.
-
- "Additional permissions" are terms that supplement the terms of this
-License by making exceptions from one or more of its conditions.
-Additional permissions that are applicable to the entire Program shall
-be treated as though they were included in this License, to the extent
-that they are valid under applicable law. If additional permissions
-apply only to part of the Program, that part may be used separately
-under those permissions, but the entire Program remains governed by
-this License without regard to the additional permissions.
-
- When you convey a copy of a covered work, you may at your option
-remove any additional permissions from that copy, or from any part of
-it. (Additional permissions may be written to require their own
-removal in certain cases when you modify the work.) You may place
-additional permissions on material, added by you to a covered work,
-for which you have or can give appropriate copyright permission.
-
- Notwithstanding any other provision of this License, for material you
-add to a covered work, you may (if authorized by the copyright holders of
-that material) supplement the terms of this License with terms:
-
- a) Disclaiming warranty or limiting liability differently from the
- terms of sections 15 and 16 of this License; or
-
- b) Requiring preservation of specified reasonable legal notices or
- author attributions in that material or in the Appropriate Legal
- Notices displayed by works containing it; or
-
- c) Prohibiting misrepresentation of the origin of that material, or
- requiring that modified versions of such material be marked in
- reasonable ways as different from the original version; or
-
- d) Limiting the use for publicity purposes of names of licensors or
- authors of the material; or
-
- e) Declining to grant rights under trademark law for use of some
- trade names, trademarks, or service marks; or
-
- f) Requiring indemnification of licensors and authors of that
- material by anyone who conveys the material (or modified versions of
- it) with contractual assumptions of liability to the recipient, for
- any liability that these contractual assumptions directly impose on
- those licensors and authors.
-
- All other non-permissive additional terms are considered "further
-restrictions" within the meaning of section 10. If the Program as you
-received it, or any part of it, contains a notice stating that it is
-governed by this License along with a term that is a further
-restriction, you may remove that term. If a license document contains
-a further restriction but permits relicensing or conveying under this
-License, you may add to a covered work material governed by the terms
-of that license document, provided that the further restriction does
-not survive such relicensing or conveying.
-
- If you add terms to a covered work in accord with this section, you
-must place, in the relevant source files, a statement of the
-additional terms that apply to those files, or a notice indicating
-where to find the applicable terms.
-
- Additional terms, permissive or non-permissive, may be stated in the
-form of a separately written license, or stated as exceptions;
-the above requirements apply either way.
-
- 8. Termination.
-
- You may not propagate or modify a covered work except as expressly
-provided under this License. Any attempt otherwise to propagate or
-modify it is void, and will automatically terminate your rights under
-this License (including any patent licenses granted under the third
-paragraph of section 11).
-
- However, if you cease all violation of this License, then your
-license from a particular copyright holder is reinstated (a)
-provisionally, unless and until the copyright holder explicitly and
-finally terminates your license, and (b) permanently, if the copyright
-holder fails to notify you of the violation by some reasonable means
-prior to 60 days after the cessation.
-
- Moreover, your license from a particular copyright holder is
-reinstated permanently if the copyright holder notifies you of the
-violation by some reasonable means, this is the first time you have
-received notice of violation of this License (for any work) from that
-copyright holder, and you cure the violation prior to 30 days after
-your receipt of the notice.
-
- Termination of your rights under this section does not terminate the
-licenses of parties who have received copies or rights from you under
-this License. If your rights have been terminated and not permanently
-reinstated, you do not qualify to receive new licenses for the same
-material under section 10.
-
- 9. Acceptance Not Required for Having Copies.
-
- You are not required to accept this License in order to receive or
-run a copy of the Program. Ancillary propagation of a covered work
-occurring solely as a consequence of using peer-to-peer transmission
-to receive a copy likewise does not require acceptance. However,
-nothing other than this License grants you permission to propagate or
-modify any covered work. These actions infringe copyright if you do
-not accept this License. Therefore, by modifying or propagating a
-covered work, you indicate your acceptance of this License to do so.
-
- 10. Automatic Licensing of Downstream Recipients.
-
- Each time you convey a covered work, the recipient automatically
-receives a license from the original licensors, to run, modify and
-propagate that work, subject to this License. You are not responsible
-for enforcing compliance by third parties with this License.
-
- An "entity transaction" is a transaction transferring control of an
-organization, or substantially all assets of one, or subdividing an
-organization, or merging organizations. If propagation of a covered
-work results from an entity transaction, each party to that
-transaction who receives a copy of the work also receives whatever
-licenses to the work the party's predecessor in interest had or could
-give under the previous paragraph, plus a right to possession of the
-Corresponding Source of the work from the predecessor in interest, if
-the predecessor has it or can get it with reasonable efforts.
-
- You may not impose any further restrictions on the exercise of the
-rights granted or affirmed under this License. For example, you may
-not impose a license fee, royalty, or other charge for exercise of
-rights granted under this License, and you may not initiate litigation
-(including a cross-claim or counterclaim in a lawsuit) alleging that
-any patent claim is infringed by making, using, selling, offering for
-sale, or importing the Program or any portion of it.
-
- 11. Patents.
-
- A "contributor" is a copyright holder who authorizes use under this
-License of the Program or a work on which the Program is based. The
-work thus licensed is called the contributor's "contributor version".
-
- A contributor's "essential patent claims" are all patent claims
-owned or controlled by the contributor, whether already acquired or
-hereafter acquired, that would be infringed by some manner, permitted
-by this License, of making, using, or selling its contributor version,
-but do not include claims that would be infringed only as a
-consequence of further modification of the contributor version. For
-purposes of this definition, "control" includes the right to grant
-patent sublicenses in a manner consistent with the requirements of
-this License.
-
- Each contributor grants you a non-exclusive, worldwide, royalty-free
-patent license under the contributor's essential patent claims, to
-make, use, sell, offer for sale, import and otherwise run, modify and
-propagate the contents of its contributor version.
-
- In the following three paragraphs, a "patent license" is any express
-agreement or commitment, however denominated, not to enforce a patent
-(such as an express permission to practice a patent or covenant not to
-sue for patent infringement). To "grant" such a patent license to a
-party means to make such an agreement or commitment not to enforce a
-patent against the party.
-
- If you convey a covered work, knowingly relying on a patent license,
-and the Corresponding Source of the work is not available for anyone
-to copy, free of charge and under the terms of this License, through a
-publicly available network server or other readily accessible means,
-then you must either (1) cause the Corresponding Source to be so
-available, or (2) arrange to deprive yourself of the benefit of the
-patent license for this particular work, or (3) arrange, in a manner
-consistent with the requirements of this License, to extend the patent
-license to downstream recipients. "Knowingly relying" means you have
-actual knowledge that, but for the patent license, your conveying the
-covered work in a country, or your recipient's use of the covered work
-in a country, would infringe one or more identifiable patents in that
-country that you have reason to believe are valid.
-
- If, pursuant to or in connection with a single transaction or
-arrangement, you convey, or propagate by procuring conveyance of, a
-covered work, and grant a patent license to some of the parties
-receiving the covered work authorizing them to use, propagate, modify
-or convey a specific copy of the covered work, then the patent license
-you grant is automatically extended to all recipients of the covered
-work and works based on it.
-
- A patent license is "discriminatory" if it does not include within
-the scope of its coverage, prohibits the exercise of, or is
-conditioned on the non-exercise of one or more of the rights that are
-specifically granted under this License. You may not convey a covered
-work if you are a party to an arrangement with a third party that is
-in the business of distributing software, under which you make payment
-to the third party based on the extent of your activity of conveying
-the work, and under which the third party grants, to any of the
-parties who would receive the covered work from you, a discriminatory
-patent license (a) in connection with copies of the covered work
-conveyed by you (or copies made from those copies), or (b) primarily
-for and in connection with specific products or compilations that
-contain the covered work, unless you entered into that arrangement,
-or that patent license was granted, prior to 28 March 2007.
-
- Nothing in this License shall be construed as excluding or limiting
-any implied license or other defenses to infringement that may
-otherwise be available to you under applicable patent law.
-
- 12. No Surrender of Others' Freedom.
-
- If conditions are imposed on you (whether by court order, agreement or
-otherwise) that contradict the conditions of this License, they do not
-excuse you from the conditions of this License. If you cannot convey a
-covered work so as to satisfy simultaneously your obligations under this
-License and any other pertinent obligations, then as a consequence you may
-not convey it at all. For example, if you agree to terms that obligate you
-to collect a royalty for further conveying from those to whom you convey
-the Program, the only way you could satisfy both those terms and this
-License would be to refrain entirely from conveying the Program.
-
- 13. Use with the GNU Affero General Public License.
-
- Notwithstanding any other provision of this License, you have
-permission to link or combine any covered work with a work licensed
-under version 3 of the GNU Affero General Public License into a single
-combined work, and to convey the resulting work. The terms of this
-License will continue to apply to the part which is the covered work,
-but the special requirements of the GNU Affero General Public License,
-section 13, concerning interaction through a network will apply to the
-combination as such.
-
- 14. Revised Versions of this License.
-
- The Free Software Foundation may publish revised and/or new versions of
-the GNU General Public License from time to time. Such new versions will
-be similar in spirit to the present version, but may differ in detail to
-address new problems or concerns.
-
- Each version is given a distinguishing version number. If the
-Program specifies that a certain numbered version of the GNU General
-Public License "or any later version" applies to it, you have the
-option of following the terms and conditions either of that numbered
-version or of any later version published by the Free Software
-Foundation. If the Program does not specify a version number of the
-GNU General Public License, you may choose any version ever published
-by the Free Software Foundation.
-
- If the Program specifies that a proxy can decide which future
-versions of the GNU General Public License can be used, that proxy's
-public statement of acceptance of a version permanently authorizes you
-to choose that version for the Program.
-
- Later license versions may give you additional or different
-permissions. However, no additional obligations are imposed on any
-author or copyright holder as a result of your choosing to follow a
-later version.
-
- 15. Disclaimer of Warranty.
-
- THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
-APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
-HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
-OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
-THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
-PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
-IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
-ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
-
- 16. Limitation of Liability.
-
- IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
-WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
-THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
-GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
-USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
-DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
-PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
-EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
-SUCH DAMAGES.
-
- 17. Interpretation of Sections 15 and 16.
-
- If the disclaimer of warranty and limitation of liability provided
-above cannot be given local legal effect according to their terms,
-reviewing courts shall apply local law that most closely approximates
-an absolute waiver of all civil liability in connection with the
-Program, unless a warranty or assumption of liability accompanies a
-copy of the Program in return for a fee.
-
- END OF TERMS AND CONDITIONS
-
- How to Apply These Terms to Your New Programs
-
- If you develop a new program, and you want it to be of the greatest
-possible use to the public, the best way to achieve this is to make it
-free software which everyone can redistribute and change under these terms.
-
- To do so, attach the following notices to the program. It is safest
-to attach them to the start of each source file to most effectively
-state the exclusion of warranty; and each file should have at least
-the "copyright" line and a pointer to where the full notice is found.
-
-
- Copyright (C)
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with this program. If not, see .
-
-Also add information on how to contact you by electronic and paper mail.
-
- If the program does terminal interaction, make it output a short
-notice like this when it starts in an interactive mode:
-
- Copyright (C)
- This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
- This is free software, and you are welcome to redistribute it
- under certain conditions; type `show c' for details.
-
-The hypothetical commands `show w' and `show c' should show the appropriate
-parts of the General Public License. Of course, your program's commands
-might be different; for a GUI interface, you would use an "about box".
-
- You should also get your employer (if you work as a programmer) or school,
-if any, to sign a "copyright disclaimer" for the program, if necessary.
-For more information on this, and how to apply and follow the GNU GPL, see
-.
-
- The GNU General Public License does not permit incorporating your program
-into proprietary programs. If your program is a subroutine library, you
-may consider it more useful to permit linking proprietary applications with
-the library. If this is what you want to do, use the GNU Lesser General
-Public License instead of this License. But first, please read
-.
diff --git a/pyhctsa/toolboxes/infodynamics-dist/readme.txt b/pyhctsa/toolboxes/infodynamics-dist/readme.txt
deleted file mode 100644
index d93f411..0000000
--- a/pyhctsa/toolboxes/infodynamics-dist/readme.txt
+++ /dev/null
@@ -1,286 +0,0 @@
-Java Information Dynamics Toolkit (JIDT)
-Copyright (C) 2012-2014 Joseph T. Lizier
-Copyright (C) 2014-2016 Joseph T. Lizier and Ipek Özdemir
-Copyright (C) 2016-2019 Joseph T. Lizier, Ipek Özdemir and Pedro Mediano
-Copyright (C) 2019-2022 Joseph T. Lizier, Ipek Özdemir, Pedro Mediano, Emanuele Crosato, Sooraj Sekhar and Oscar Huaigu Xu
-Copyright (C) 2022- Joseph T. Lizier, Ipek Özdemir, Pedro Mediano, Emanuele Crosato, Sooraj Sekhar, Oscar Huaigu Xu and David Shorten
-
-Version 1.6.1 (see release notes below)
-
-JIDT provides a standalone, open source code Java implementation (usable in Matlab, Octave and Python) of information-theoretic measures of distributed computation in complex systems: i.e. information storage, transfer and modification.
-
-This includes implementations for:
-- both discrete and continuous-valued variables, principally for the measures transfer entropy, mutual information and active information storage;
-- using various types of estimators (e.g. Kraskov-Stögbauer-Grassberger estimators, kernel estimation, linear-Gaussian).
-
-=============
- License
-=============
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program. If not, see .
-
-=============
- Website
-=============
-
-Full information on the JIDT (usage, etc) is provided at the project page and wiki on github:
-
-https://github.com/jlizier/jidt/
-https://github.com/jlizier/jidt/wiki
-
-=============
-Installation
-=============
-
-"Full" description of any required installation is at: https://github.com/jlizier/jidt/wiki/Installation
-
-However, if you are reading this file, you've downloaded a distribution and you're halfway there!
-
-There are no dependencies to download; unless:
- a. You don't have java installed - download it from http://www.java.com/
- b. You wish to build the project using the build.xml script - this requires ant: http://ant.apache.org/
- c. You wish to run the JUnit test cases - this requires JUnit: http://www.junit.org/ - for how to run JUnit with our ant script see https://github.com/jlizier/jidt/wiki/JUnitTestCases
-
-Then just put the jar in a relevant location in your file structure.
-
-That's it.
-
-=============
-Documentation
-=============
-
-A research paper describing the toolkit is included in the top level directory -- "InfoDynamicsToolkit.pdf".
-
-A tutorial, providing background to the information-theoretic measures, various estimators, and then to the JIDT toolkit itself is included in the tutorial folder (see "JIDT-TutorialSlides.pdf" for the tutorial slides, and "README-TutorialAndExercise.pdf" for further description of the tutorial exercises).
-
-Javadocs for the toolkit are included in the full distribution at javadocs.
-They can also be generated using "ant javadocs" (useful if you are on a git clone).
-Further, they will are posted on the web via links at https://github.com/jlizier/jidt/wiki/Documentation
-
-The project wiki also contains further information on various aspects; see https://github.com/jlizier/jidt/wiki to start.
-
-Further documentation is provided by the Usage demo examples below.
-
-You can also join our email discussion group jidt-discuss at http://groups.google.com/d/forum/jidt-discuss
-
-=============
- Usage
-=============
-
-Several sets of demonstration code are distributed with the toolkit:
-
- a. demos/AutoAnalyser -- a GUI tool to compute the information-theoretic measures on a chosen data set with the toolkit, and also automatically generate code in Java, Python and Matlab to show how to do this calculation with the toolkit. See description at https://github.com/jlizier/jidt/wiki/AutoAnalyser
-
- b. demos/java -- basic examples on easily using the Java toolkit -- run these from the shell scripts in this directory -- see description at https://github.com/jlizier/jidt/wiki/SimpleJavaExamples
-
- c. Several demo sets mirror the SimpleJavaExamples to demonstrate the use of the toolkit in non-Java environments:
-
- i. demos/octave -- basic examples on easily using the Java toolkit from Octave or Matlab environments -- see description at https://github.com/jlizier/jidt/wiki/OctaveMatlabExamples
-
- ii. demos/python -- basic examples on easily using the Java toolkit from Python -- see description at https://github.com/jlizier/jidt/wiki/PythonExamples
-
- iii. demos/r -- basic examples on easily using the Java toolkit from R -- see description at https://github.com/jlizier/jidt/wiki/R_Examples
-
- iv. demos/julia -- basic examples on easily using the Java toolkit from Julia -- see description at https://github.com/jlizier/jidt/wiki/JuliaExamples
-
- v. demos/clojure -- basic examples on easily using the Java toolkit from Clojure -- see description at https://github.com/jlizier/jidt/wiki/Clojure_Examples
-
- d. demos/octave/CellularAutomata -- using the Java toolkit to plot local information dynamics profiles in cellular automata; the toolkit is run under Octave or Matlab -- see description at https://github.com/jlizier/jidt/wiki/CellularAutomataDemos
-
- e. demos/octave/SchreiberTransferEntropyExamples -- recreates the transfer entropy examples in Schreiber's original paper presenting this measure; shows the correct parameter settings to reproduce these results -- see description at https://github.com/jlizier/jidt/wiki/SchreiberTeDemos
-
- f. demos/octave/DetectingInteractionLags -- demonstration of using the transfer entropy with source-destination lags; the demo is run under Octave or Matlab -- see description at https://github.com/jlizier/jidt/wiki/DetectingInteractionLags
-
- g. demos/java/InterregionalTransfer -- higher level example using collective transfer entropy to infer effective connections between "regions" of data -- see description at https://github.com/jlizier/jidt/wiki/InterregionalTransfer
-
- h. demos/octave/NullDistributions -- investigating the correspondence between analytic and bootstrapped distributions for TE and MI under null hypotheses of no relationship; the demo is run under Octave or Matlab -- see description at https://github.com/jlizier/jidt/wiki/NullDistributions
-
- i. java/unittests -- the JUnit test cases for the Java toolkit are included in the distribution -- these case also be browsed to see simple use cases for the various calculators in the toolkit -- see description at https://github.com/jlizier/jidt/wiki/JUnitTestCases
-
-=============
- Citation
-=============
-
-Please cite your use of this toolkit as:
-
-Joseph T. Lizier, "JIDT: An information-theoretic toolkit for studying the dynamics of complex systems", Frontiers in Robotics and AI 1:11, 2014; doi:10.3389/frobt.2014.00011
-
-A pre-print of this paper is distributed with this toolkit (InfoDynamicsToolkit.pdf) and is available at arXiv:1408.3270 (https://arxiv.org/abs/1408.3270)
-
-=============
- Notices
-=============
-
-This project includes modified files from the Apache Commons Math library -- http://commons.apache.org/proper/commons-math/
-This Apache 2 software is now included as a derivative work in this GPLv3 licensed JIDT project, as per: http://www.apache.org/licenses/GPL-compatibility.html
-Notices and license for this software are found in the notices/commons-math directory.
-
-The project includes adapted code from the JAMA project -- http://math.nist.gov/javanumerics/jama/
-Notices and license for this software are found in the notices/JAMA directory.
-
-The project includes adapted code from the octave-java package of the Octave-Forge project -- http://octave.sourceforge.net/java/
-Notices for this software are found in the notices/JAMA directory.
-
-===============
- Release notes
-===============
-
-v1.6.1 22/8/2023
--------------
-(after 909 commits recorded by github, repository as at https://github.com/jlizier/jidt/tree/90baf68ee7332e15030447b44d262a0fc54773f6 save for this file update)
-Minor updates to supporting use in Python, including virtual environments;
-Minor tweaks to fish schooling examples (mostly comments)
-
-v1.6 5/9/2022
--------------
-(after 889 commits recorded by github, repository as at https://github.com/jlizier/jidt/tree/d750a737bea2a8b1f33b7cd0ad167ec999d907ef save for this file update)
-Adding Flocking/Schooling/Swarming demo;
-Included Pedro's code on IIT and O-/S-Information measures;
-Spiking TE estimator added from David;
-Fixed up AutoAnalyser to work well for Python3 and numpy;
-Links to lecture videos included in the beta wiki for the course;
-Added rudimentary effective network inference (simplified version of the IDTxl full algorithm) in demos/octave/EffectiveNetworkInference;
-
-
-v1.5 26/11/2018
----------------
-(after 753 commits recorded by github, repository as at https://github.com/jlizier/jidt/tree/603445651cc0bf155a42c9ba336141bc7f29bccd save for this file update)
-Added GPU (cuda) capability for KSG Conditional Mutual Information calculator (proper documentation to come), including unit tests and brief wiki page;
-Added auto-embedding for TE/AIS with multivariate KSG, and univariate and multivariate Gaussian estimator (plus unit tests), for Ragwitz criteria and Maximum bias-corrected AIS, and also added Maximum bias corrected AIS and TE to handle source embedding as well;
-Kozachenko entropy estimator adds noise to data by default;
-Added bias-correction property to Gaussian and Kernel estimators for MI and conditional MI, including with surrogates (only option for kernel);
-Enabled use of different bases for different variables in MI discrete estimator;
-All new above features enabled in AutoAnalyser;
-Added drop-down menus for parameters in AutoAnalyser;
-Included long-form lecture slides in course folder;
-
-v1.4 26/11/2017
----------------
-(after 638 commits recorded by github, repository as at https://github.com/jlizier/jidt/tree/589d51674e6a9cfb569432679e515bea17092876 save for this file update)
-Major expansion of functionality for AutoAnalysers: adding Launcher applet and capability to double click jar to launch, added Entropy, CMI, CTE and AIS AutoAnalysers, also added binned estimator type, added all variables/pairs analysis, added statistical significance analysis, and ensured functionality of generated Python code with Python3;
-Added GPU (cuda) capability for KSG Mutual Information calculator (proper documentation and wiki page to come), including unit tests;
-Added fast neighbour search implementations for mixed discrete-continuous KSG MI estimators;
-Expanded Gaussian estimator for multi-information (integration);
-Made all demo/data files readable by Matlab.
-
-
-v1.3.1 21/10/2016
------------------
-(after 385 commits recorded by github, repository as at https://github.com/jlizier/jidt/tree/269e263a84998807c5c02f36397b585a19205938 save for this file update)
-Major update to TransferEntropyCalculatorDiscrete so as to implement arbirtray source and dest embeddings and source-dest delay;
-Conditional TE calculators (continuous) handle empty conditional variables;
-Added auto-embedding method for AIS and TE which maximises bias corrected AIS;
-Added getNumSeparateObservations() method to TE calculators to make reconstructing/separating local values easier after multiple addObservations() calls;
-Fixed kernel estimator classes to return proper densities, not probabilities;
-Bug fix in mixed discrete-continuous MI (Kraskov) implementation;
-Added simple interface for adding joint observations for MultiInfoCalculatorDiscrete
-Including compiled class files for the AutoAnalyser demo in distribution;
-Updated Python demo 1 to show use of numpy arrays with ints;
-Added Python demo 7 and 9 for TE Kraskov with ensemble method and auto-embedding respectively;
-Added Matlab/Octave example 10 for conditional TE via Kraskov (KSG) algorithm;
-Added utilities to prepare for enhancing surrogate calculations with fast nearest neighbour search;
-Minor bug patch to Python readFloatsFile utility;
-
-
-v1.3 10/7/2015 at r691
-----------------------
-Added AutoAnalyser (Code Generator) GUI demo for MI and TE;
-Added auto-embedding capability via Ragwitz criteria for AIS and TE calculators (KSG estimators);
-Added Java demo 9 for showcasing use of Ragwitz auto-embedding;
-Adding small amount of noise to data in all KSG estimators now by default (may be disabled via setProperty());
-Added getProperty() methods for all conditional MI and TE calculators;
-Upgraded Python demos for Python 3 compatibility;
-Fixed bias correction on mixed discrete-continuous KSG calculators;
-Updated the tutorial slides to those in use for ECAL 2015 JIDT tutorial;
-
-v1.2.1 12/2/2015 at r621
-------------------------
-Added tutorial slides, description of exercises and sample exercise solutions;
-Made jar target Java 1.6;
-Added Schreiber TE heart-breath rate with KSG estimator demo code for Python.
-
-v1.2 28/1/2015 at r601
------------------------
-Dynamic correlation exclusion, or Theiler window, added to all Kraskov estimators;
-Added univariate MI calculation to simple demo 6;
-Added Java code for Schreiber TE heart-breath rate with KSG estimator, ready for use as a template in Tutorial;
-Patch for crashes in KSG conditional MI algorithm 2;
-
-v1.1 14/11/2014 at r576
------------------------
-Implemented Fast Nearest Neighbour Search for Kraskov-Stögbauer-Grassberger (KSG) estimators for MI, conditional MI, TE, conditional TE, AIS, Predictive info, and multi-information. This includes a general (multivariate) k-d tree implementation;
-Added multi-threading (using all available processors by default) for the KSG estimators -- code contributed by Ipek Özdemir;
-Added Predictive information / Excess entropy implementations for KSG, kernel and Gaussian estimators;
-Added R, Julia, and Clojure demos;
-Added Windows batch files for the Simple Java Demos;
-Added property for adding a small amount of noise to data in all KSG estimators;
-
-
-v1.0 14/8/2014 at r434
-----------------------
-Added the draft of the paper on the toolkit to the release;
-Javadocs made ready for release;
-Switched source->destination arguments for discrete TE calculators to be with source first in line with continuous calculators;
-Renamed all discrete calculators to have Discrete suffix -- TE and conditional TE calculators also renamed to remove "Apparent" prefix and change "Complete" to "Conditional";
-Kraskov estimators now using 4 nearest neighbours by default;
-Unit test for Gaussian TE against ChaLearn Granger causality measurement;
-Added Schreiber TE demos; Interregional transfer demos; documentation for Interaction lag demos; added examples 7 and 8 to Simple Java demos;
-Added property to add noise to data for Kraskov MI;
-Added derivation of Apache Commons Math code for chi square distribution, and included relevant notices in our release;
-Inserted translation class for arrays between Octave and Java;
-Added analytic statistical significance calculation to Gaussian calculators and discrete TE;
-Corrected Kraskov algorithm 2 for conditional MI to follow equation in Wibral et al. 2014.
-
-
-v0.2.0 20/4/2014 at r284
-------------------------
-Rearchitected (most) Transfer Entropy and Multivariate TE calculators to use an underlying conditional mutual information calculator, and have arbitrary embedding delay, source-dest delay;
-this includes moving Kraskov-Grassberger Transfer Entropy calculator to use a single conditional mutual information estimator instead of two mutual information estimators;
-Rearchitected (most) Active Information Storage calculators to use an underlying mutual information calculator;
-Added Conditional Transfer Entropy calculators using underlying conditional mutual information calculators;
-Moved mixed discrete-continuous calculators to a new "mixed" package;
-bug fixes.
-
-v0.1.4 11/9/2013 at r241
-------------------------
-added scripts to generate CA figures for 2013 book chapters;
-added general Java demo code;
-added Python demo code;
-made Octave/Matlab demos and CA demos properly compatible for Matlab;
-added extra Octave/Matlab general demos;
-added more unit tests for MI and conditional MI calculators, including against results from Wibral's TRENTOOL;
-bug fixes.
-
-v0.1.3 13/1/2013 at r151
-------------------------
-existing Octave/Matlab demo code made compatible with Matlab;
-several bug fixes, including using max norm by default in Kraskov calculator (instead of requiring this to be set explicitly);
-more unit tests (including against results from Kraskov's own MI implementation)
-
-v0.1.2 19/11/2012 at r116
--------------------------
-Includes demo code for two newly submitted papers
-
-v0.1.1 31/10/2012 at r104
-------------------------
-No notes
-
-v0.1 24/10/2012 at r65?
-------------------------
-First distribution
-
-=============
-
-Joseph T. Lizier, 22/08/2023
-
diff --git a/pyhctsa/toolboxes/infodynamics-dist/version-1.6.1.txt b/pyhctsa/toolboxes/infodynamics-dist/version-1.6.1.txt
deleted file mode 100644
index 90c9450..0000000
--- a/pyhctsa/toolboxes/infodynamics-dist/version-1.6.1.txt
+++ /dev/null
@@ -1,11 +0,0 @@
-Java Information Dynamics Toolkit (JIDT)
-Copyright (C) 2012-2014 Joseph T. Lizier
-Copyright (C) 2014-2016 Joseph T. Lizier and Ipek Özdemir
-Copyright (C) 2016-2019 Joseph T. Lizier, Ipek Özdemir and Pedro Mediano
-Copyright (C) 2019-2022 Joseph T. Lizier, Ipek Özdemir, Pedro Mediano, Emanuele Crosato, Sooraj Sekhar and Oscar Huaigu Xu
-Copyright (C) 2022- Joseph T. Lizier, Ipek Özdemir, Pedro Mediano, Emanuele Crosato, Sooraj Sekhar, Oscar Huaigu Xu and David Shorten
-
-Version 1.6.1
-
-22/08/2023
-
diff --git a/pyhctsa/toolboxes/infotheory/mutual_info.py b/pyhctsa/toolboxes/infotheory/mutual_info.py
new file mode 100644
index 0000000..a4739b3
--- /dev/null
+++ b/pyhctsa/toolboxes/infotheory/mutual_info.py
@@ -0,0 +1,381 @@
+"""
+Faithful Python port of the Kraskov-Stoegbauer-Grassberger (KSG) mutual
+information estimators (algorithms 1 and 2) as implemented in the Java
+Information Dynamics Toolkit (JIDT) by J. T. Lizier.
+
+Ported from:
+ infodynamics.measures.continuous.kraskov.MutualInfoCalculatorMultiVariateKraskov
+ infodynamics.measures.continuous.kraskov.MutualInfoCalculatorMultiVariateKraskov1
+ infodynamics.measures.continuous.kraskov.MutualInfoCalculatorMultiVariateKraskov2
+ infodynamics.measures.continuous.MutualInfoMultiVariateCommon
+ infodynamics.measures.continuous.gaussian.MutualInfoCalculatorMultiVariateGaussian
+
+Reference:
+ A. Kraskov, H. Stoegbauer, P. Grassberger, "Estimating mutual information",
+ Phys. Rev. E 69, 066138 (2004). https://doi.org/10.1103/PhysRevE.69.066138
+
+ T. M. Cover & J. A. Thomas, "Elements of Information Theory", Wiley (1991).
+ H. Kantz & T. Schreiber, "Nonlinear Time Series Analysis", CUP (1997).
+ J. T. Lizier, "JIDT: An information-theoretic toolkit ...", Front. Robot. AI (2014).
+"""
+import numpy as np
+from scipy.spatial import cKDTree
+from scipy.stats import chi2
+from scipy.special import digamma
+
+def _as_2d(a) -> np.ndarray:
+ a = np.asarray(a, dtype=float)
+ return a[:, None] if a.ndim == 1 else a
+
+_NORM_TO_P = {
+ "max": np.inf, "maximum": np.inf, "chebyshev": np.inf, "inf": np.inf,
+ "euclidean": 2.0, "euclidean_squared": 2.0, "l2": 2.0,
+}
+
+def _normalise_columns(a: np.ndarray) -> np.ndarray:
+ """Zero-mean, unit-variance per column, using the sample std (ddof=1).
+
+ Divides the sum of squares by (N-1). Note the KSG estimate is actually invariant to the ddof choice: it
+ rescales every variable by the same factor sqrt((N-1)/N), which leaves the
+ (max-norm) neighbour sets unchanged.
+ """
+ mu = a.mean(axis=0)
+ sd = a.std(axis=0, ddof=1)
+ sd = np.where(sd == 0.0, 1.0, sd)
+ return (a - mu) / sd
+
+class KraskovMI:
+ """KSG mutual information estimator.
+
+ Parameters
+ ----------
+ k : int
+ Number of nearest neighbours in the joint space (default 4).
+ algorithm : {1, 2}
+ KSG algorithm 1 or 2 (default 1).
+ norm : {'max', 'euclidean', 'euclidean_squared'}
+ Norm used within each variable / the joint space (default 'max',
+ i.e. the maximum / Chebyshev norm used in the original KSG paper).
+ normalise : bool
+ If True, z-score each variable (per dimension) before estimation.
+ Default is True.
+ add_noise : bool
+ If True, add tiny Gaussian noise to break ties / degenerate distances.
+ Default is True for the Kraskov estimators.
+ noise_level : float
+ Standard deviation of the added noise (default 1e-8).
+ Applied AFTER normalisation.
+ theiler : int
+ Dynamic correlation exclusion (Theiler) window. Neighbours j with
+ ``|j - i| <= theiler`` are excluded for query point i. Default 0
+ (no exclusion). The input is treated as a single contiguous
+ observation set.
+ seed : int or None
+ Seed for the noise RNG for reproducibility.
+ """
+
+ def __init__(self, k: int = 4, algorithm: int = 1, norm: str = "max",
+ normalise: bool = True, add_noise: bool = True,
+ noise_level: float = 1e-8, theiler: int = 0, seed=None):
+ if algorithm not in (1, 2):
+ raise ValueError("algorithm must be 1 or 2")
+ nlow = str(norm).lower()
+ if nlow not in _NORM_TO_P:
+ raise ValueError(f"norm must be one of {sorted(set(_NORM_TO_P))}")
+ if k < 1:
+ raise ValueError("k must be >= 1")
+ if theiler < 0:
+ raise ValueError("theiler must be >= 0")
+
+ self.k = int(k)
+ self.algorithm = int(algorithm)
+ self.norm = nlow
+ self.p = _NORM_TO_P[nlow]
+ self.normalise = bool(normalise)
+ self.add_noise = bool(add_noise)
+ self.noise_level = float(noise_level)
+ self.theiler = int(theiler)
+ self.rng = np.random.default_rng(seed)
+
+ def compute(self, X, Y, local: bool = False):
+ """Compute the KSG MI between X and Y (in nats).
+
+ Returns the average MI (float) unless ``local=True``, in which case the
+ per-sample local MI values (np.ndarray of length N) are returned; their
+ mean equals the average MI.
+ """
+ X = _as_2d(X)
+ Y = _as_2d(Y)
+ if X.shape[0] != Y.shape[0]:
+ raise ValueError("X and Y must have the same number of samples")
+ N = X.shape[0]
+ if N <= self.k + 2 * self.theiler:
+ raise ValueError(
+ f"Too few samples ({N}) for k={self.k} and theiler={self.theiler}")
+
+ if self.normalise:
+ X = _normalise_columns(X)
+ Y = _normalise_columns(Y)
+ if self.add_noise and self.noise_level > 0:
+ X = X + self.rng.normal(0.0, self.noise_level, X.shape)
+ Y = Y + self.rng.normal(0.0, self.noise_level, Y.shape)
+
+ joint = np.hstack([X, Y])
+ joint_tree = cKDTree(joint)
+ x_tree = cKDTree(X)
+ y_tree = cKDTree(Y)
+
+ if self.algorithm == 1:
+ eps = self._joint_radius_alg1(joint_tree, joint, N)
+ n_x = self._count_marginal(x_tree, X, eps, strict=True)
+ n_y = self._count_marginal(y_tree, Y, eps, strict=True)
+ local_mi = (digamma(self.k)
+ - digamma(n_x + 1) - digamma(n_y + 1)
+ + digamma(N))
+ else: # algorithm 2
+ eps_x, eps_y = self._joint_radii_alg2(joint_tree, joint, X, Y, N)
+ n_x = self._count_marginal(x_tree, X, eps_x, strict=False)
+ n_y = self._count_marginal(y_tree, Y, eps_y, strict=False)
+ local_mi = (digamma(self.k) - 1.0 / self.k
+ - digamma(n_x) - digamma(n_y)
+ + digamma(N))
+
+ return local_mi if local else float(np.mean(local_mi))
+
+ def significance(self, X, Y, n_permutations: int = 100):
+ """Permutation significance test (shuffles Y to break X-Y dependence).
+
+ Returns a dict with the measured MI, the surrogate mean/std, a p-value
+ (fraction of surrogates >= measured, Laplace-corrected) and the raw
+ surrogate values.
+ """
+ measured = self.compute(X, Y)
+ Y2 = _as_2d(Y)
+ surrogates = np.empty(n_permutations)
+ for s in range(n_permutations):
+ perm = self.rng.permutation(Y2.shape[0])
+ surrogates[s] = self.compute(X, Y2[perm])
+ p_value = (np.sum(surrogates >= measured) + 1) / (n_permutations + 1)
+ return {
+ "mi": measured,
+ "surrogate_mean": float(surrogates.mean()),
+ "surrogate_std": float(surrogates.std()),
+ "p_value": float(p_value),
+ "surrogates": surrogates,
+ }
+
+ def _joint_radius_alg1(self, joint_tree, joint, N):
+ """Distance to the kth nearest neighbour in the joint space (self excluded)."""
+ if self.theiler == 0:
+ # query k+1 because the nearest returned point is the point itself.
+ d, _ = joint_tree.query(joint, k=self.k + 1, p=self.p)
+ return np.ascontiguousarray(d[:, -1])
+ return self._joint_radius_alg1_theiler(joint_tree, joint, N)
+
+ def _joint_radius_alg1_theiler(self, joint_tree, joint, N):
+ kq = min(N, self.k + 2 * self.theiler + 1) # guarantees >= k survivors
+ d, idxs = joint_tree.query(joint, k=kq, p=self.p)
+ th = self.theiler
+ eps = np.empty(N)
+ for t in range(N):
+ count = 0
+ chosen = None
+ for dist_t, j in zip(d[t], idxs[t]):
+ if abs(int(j) - t) > th:
+ count += 1
+ if count == self.k:
+ chosen = dist_t
+ break
+ if chosen is None: # safety net (shouldn't trigger given kq)
+ chosen = self._kth_joint_distance_fallback(joint_tree, joint, t, N)
+ eps[t] = chosen
+ return eps
+
+ def _joint_radii_alg2(self, joint_tree, joint, X, Y, N):
+ """Per-axis radii eps_x, eps_y = max marginal norm over the k joint NN."""
+ if self.theiler == 0:
+ _, idxs = joint_tree.query(joint, k=self.k + 1, p=self.p)
+ nb = idxs[:, 1:] # k neighbours, excluding the point itself (col 0)
+ eps_x = self._marginal_norm(X[nb] - X[:, None, :]).max(axis=1)
+ eps_y = self._marginal_norm(Y[nb] - Y[:, None, :]).max(axis=1)
+ return eps_x, eps_y
+ return self._joint_radii_alg2_theiler(joint_tree, joint, X, Y, N)
+
+ def _joint_radii_alg2_theiler(self, joint_tree, joint, X, Y, N):
+ kq = min(N, self.k + 2 * self.theiler + 1)
+ _, idxs = joint_tree.query(joint, k=kq, p=self.p)
+ th = self.theiler
+ eps_x = np.empty(N)
+ eps_y = np.empty(N)
+ for t in range(N):
+ survivors = [int(j) for j in idxs[t] if abs(int(j) - t) > th][:self.k]
+ surv = np.asarray(survivors, dtype=int)
+ eps_x[t] = self._marginal_norm(X[surv] - X[t]).max()
+ eps_y[t] = self._marginal_norm(Y[surv] - Y[t]).max()
+ return eps_x, eps_y
+
+ def _count_marginal(self, tree, data, R, strict):
+ """Count points within radius R[i] of point i in `data`.
+
+ strict=True -> distance < R[i] (algorithm 1)
+ strict=False -> distance <= R[i] (algorithm 2)
+ The query point itself and any point inside the Theiler window are
+ excluded (handled by the window-correction loop; the d=0 term removes
+ the point itself).
+ """
+ N = data.shape[0]
+ if strict:
+ # dist <= nextafter(R, -inf) is equivalent to dist < R
+ r_query = np.nextafter(R, -np.inf)
+ else:
+ r_query = R
+ full = np.asarray(
+ tree.query_ball_point(data, r_query, p=self.p, return_length=True),
+ dtype=np.int64,
+ )
+
+ # Subtract contributions from indices within the Theiler window
+ # (range includes 0, which removes the point itself).
+ excluded = np.zeros(N, dtype=np.int64)
+ idx = np.arange(N)
+ for d in range(-self.theiler, self.theiler + 1):
+ j = idx + d
+ valid = (j >= 0) & (j < N)
+ ii = idx[valid]
+ jj = j[valid]
+ dist = self._marginal_norm(data[ii] - data[jj])
+ cond = (dist < R[ii]) if strict else (dist <= R[ii])
+ np.add.at(excluded, ii[cond], 1)
+ return full - excluded
+
+ def _marginal_norm(self, diff):
+ """Norm along the last axis under the configured Minkowski p."""
+ ad = np.abs(diff)
+ if np.isinf(self.p):
+ return ad.max(axis=-1)
+ return (ad ** self.p).sum(axis=-1) ** (1.0 / self.p)
+
+ def _kth_joint_distance_fallback(self, joint_tree, joint, t, N):
+ d, idxs = joint_tree.query(joint[t], k=N, p=self.p)
+ count = 0
+ for dist_t, j in zip(np.atleast_1d(d), np.atleast_1d(idxs)):
+ if abs(int(j) - t) > self.theiler:
+ count += 1
+ if count == self.k:
+ return dist_t
+ raise RuntimeError("Not enough valid neighbours after Theiler exclusion")
+
+
+def ksg_mi(X, Y, k: int = 4, algorithm: int = 1, norm: str = "max",
+ normalise: bool = True, add_noise: bool = True,
+ noise_level: float = 1e-8, theiler: int = 0, local: bool = False,
+ seed=None):
+ """Functional wrapper around :class:`KraskovMI`. Returns MI in nats.
+
+ Example
+ -------
+ >>> ksg_mi(x, y, algorithm=1)
+ >>> ksg_mi(x, y, algorithm=2, normalise=False, add_noise=False)
+ """
+ est = KraskovMI(k=k, algorithm=algorithm, norm=norm, normalise=normalise,
+ add_noise=add_noise, noise_level=noise_level,
+ theiler=theiler, seed=seed)
+ return est.compute(X, Y, local=local)
+
+
+class GaussianMI:
+ r"""Mutual information assuming a (multivariate) Gaussian model.
+
+ Uses the closed form MI = 0.5 * ln( det(C_x) det(C_y) / det(C_xy) ) in nats,
+ where C_x, C_y, C_xy are the sample covariances of X, Y and the stacked
+ [X, Y]. This is the maximum-likelihood plug-in estimate; it is the right tool
+ when the dependence is (close to) linear-Gaussian and is far lower variance
+ than KSG/kernel in that regime, but it only captures *linear* dependence.
+
+ Parameters
+ ----------
+ bias_correction : bool
+ If True, subtract the analytic finite-sample bias (the mean of the
+ chi-square null distribution, d_x * d_y / (2N)) from the estimate,
+ matching JIDT's ``BIAS_CORRECTION`` property. Default False.
+
+ Notes
+ -----
+ The result is exactly invariant to per-variable rescaling (and hence to
+ JIDT's ``NORMALISE`` flag), so no ``normalise`` option is exposed.
+ Full linear-redundancy pruning (JIDT's Cholesky-of-independent-components)
+ is not reproduced; for full-rank inputs the estimate is exact. Add a hair of
+ noise if you have exactly collinear columns.
+ """
+
+ def __init__(self, bias_correction: bool = False):
+ self.bias_correction = bool(bias_correction)
+
+ def _moments(self, X, Y):
+ X = _as_2d(X)
+ Y = _as_2d(Y)
+ if X.shape[0] != Y.shape[0]:
+ raise ValueError("X and Y must have the same number of samples")
+ N, ds, dd = X.shape[0], X.shape[1], Y.shape[1]
+ Z = np.hstack([X, Y])
+ # rowvar=False -> variables in columns; ddof=1 (sample covariance, as JIDT)
+ Cx = np.atleast_2d(np.cov(X, rowvar=False, ddof=1))
+ Cy = np.atleast_2d(np.cov(Y, rowvar=False, ddof=1))
+ Cz = np.atleast_2d(np.cov(Z, rowvar=False, ddof=1))
+ return X, Y, Z, N, ds, dd, Cx, Cy, Cz
+
+ def compute(self, X, Y, local: bool = False):
+ X, Y, Z, N, ds, dd, Cx, Cy, Cz = self._moments(X, Y)
+
+ s_x = np.linalg.slogdet(Cx)
+ s_y = np.linalg.slogdet(Cy)
+ s_z = np.linalg.slogdet(Cz)
+ if s_x.sign <= 0 or s_y.sign <= 0:
+ raise np.linalg.LinAlgError(
+ "Marginal covariance is singular (collinear columns within X or Y)")
+ if s_z.sign <= 0:
+ return np.inf if not local else np.full(N, np.inf) # X,Y jointly collinear
+
+ avg_mi = 0.5 * (s_x.logabsdet + s_y.logabsdet - s_z.logabsdet)
+ bias = (ds * dd) / (2.0 * N) # mean of chi-square null, in nats
+
+ if not local:
+ return float(avg_mi - bias) if self.bias_correction else float(avg_mi)
+
+ # Local values: log[ p_joint / (p_x p_y) ] per sample, Gaussian PDFs
+ mu = Z.mean(axis=0)
+ mu_x, mu_y = mu[:ds], mu[ds:]
+ inv_x = np.linalg.inv(Cx)
+ inv_y = np.linalg.inv(Cy)
+ inv_z = np.linalg.inv(Cz)
+ dev_x = X - mu_x
+ dev_y = Y - mu_y
+ dev_z = Z - mu
+ maha = lambda dev, inv: np.einsum("ij,jk,ik->i", dev, inv, dev)
+ arg_x = maha(dev_x, inv_x)
+ arg_y = maha(dev_y, inv_y)
+ arg_z = maha(dev_z, inv_z)
+ const = 0.5 * (s_x.logabsdet + s_y.logabsdet - s_z.logabsdet)
+ local_mi = 0.5 * (arg_x + arg_y - arg_z) + const # (2*pi)^d factors cancel
+ if self.bias_correction:
+ local_mi = local_mi - bias
+ return local_mi
+
+ def significance(self, X, Y):
+ """Analytic significance test (chi-square null), as in JIDT.
+
+ 2*N*MI is chi-square distributed with d_x*d_y degrees of freedom under the
+ null of independence. Returns the measured MI, the null mean/std and a
+ p-value (P(null >= measured)).
+ """
+ _, _, _, N, ds, dd, *_ = self._moments(X, Y)
+ mi = self.compute(X, Y)
+ dof = ds * dd
+ stat = 2.0 * N * mi
+ return {
+ "mi": mi,
+ "p_value": float(chi2.sf(stat, dof)),
+ "null_mean": dof / (2.0 * N),
+ "null_std": float(np.sqrt(2.0 * dof) / (2.0 * N)),
+ "dof": dof,
+ }
diff --git a/pyhctsa/utils.py b/pyhctsa/utils.py
index 6823880..f94d57f 100644
--- a/pyhctsa/utils.py
+++ b/pyhctsa/utils.py
@@ -9,6 +9,8 @@
from pyhctsa import __version__
from pathlib import Path
import logging
+logger = logging.getLogger('pyhctsa')
+import warnings
import numpy as np
from numpy.typing import ArrayLike
@@ -93,16 +95,6 @@ def _check_optional_deps(dep: str) -> bool:
Returns True if available, else False."""
try:
version(dep)
- # special check for JPype dependency
- if dep == "jpype1":
- try:
- import jpype
- jpype.getDefaultJVMPath()
- except (ImportError, AttributeError):
- return False
- except jpype.JVMNotFoundException:
- logging.warning("JVM not found. Please check your JAVA_HOME or installation.")
- return False
return True
except PackageNotFoundError:
return False
@@ -110,19 +102,19 @@ def _check_optional_deps(dep: str) -> bool:
def _validate_data(ts: np.ndarray) -> bool:
"""validate a time series before computing features"""
if len(ts) < 100:
- logging.warning("Time series is too short!")
+ logger.warning("Time series is too short!")
return False
if np.all(ts == ts[0]):
# constant time series
# maybe do a tolerance instead?
- logging.warning("Time series is constant.")
+ logger.warning("Time series is constant.")
return False
if np.any(np.isnan(ts)):
# data contains nans
- logging.warning("Time series contains NaNs.")
+ logger.warning("Time series contains NaNs.")
return False
if np.any(np.isinf(ts)):
- logging.warning("Time series contains Inf.")
+ logger.warning("Time series contains Inf.")
return False
return True
diff --git a/pyproject.toml b/pyproject.toml
index 1763ddb..454552e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -22,7 +22,6 @@ dependencies = [
"scipy",
"pyyaml",
"statsmodels",
- "jpype1<1.7.0",
"numba",
"scikit-learn",
"antropy",
diff --git a/requirements.txt b/requirements.txt
index 4d02374..f773885 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -2,7 +2,6 @@ scipy
numpy
pyyaml
statsmodels
-jpype1<1.7.0
numba
scikit-learn
antropy
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 0e3c9d3..b1ac4e0 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -79,7 +79,6 @@ class TestOptionalDepChecks:
def test_optional_dep_check_basic(self):
assert _check_optional_deps('numpy') is True
assert _check_optional_deps('test') is False
- assert _check_optional_deps('jpype1') is True
# 4. Validate data checks
class TestValidateData: