random-processes 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alexandr
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,117 @@
1
+ Metadata-Version: 2.1
2
+ Name: random-processes
3
+ Version: 0.1.0
4
+ Summary: Python library for generating random processes with specified autocorrelation properties. Supports custom kernels and multivariate correlated noise.
5
+ Author-email: Alexander Abramov <extremal.ru@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/avabr/random-processes
8
+ Project-URL: Repository, https://github.com/avabr/random-processes
9
+ Project-URL: Issues, https://github.com/avabr/random-processes/issues
10
+ Keywords: random-process,colored-noise,autocorrelation,stochastic,signal-processing,spectral-method
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
20
+ Classifier: Topic :: Scientific/Engineering :: Physics
21
+ Requires-Python: >=3.10
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: numpy
25
+ Requires-Dist: scipy
26
+ Requires-Dist: matplotlib
27
+
28
+ # random-processes
29
+
30
+ Generate random processes with specified autocorrelation properties.
31
+
32
+ ## Quick start
33
+
34
+ **Exponential:** `R(tau) = D * exp(-lambda * |tau|)`
35
+
36
+ ```python
37
+ from random_processes import generate, exponential_kernel
38
+
39
+ k = exponential_kernel(D=1.0, lam=2.0)
40
+ t, x = generate(k, duration=100.0, dt=0.01, seed=42)
41
+ ```
42
+
43
+ **Oscillating:** `R(tau) = D * exp(-lambda * |tau|) * cos(w0 * tau)`
44
+
45
+ ```python
46
+ from random_processes import generate, oscillating_kernel
47
+
48
+ k = oscillating_kernel(D=2.0, lam=0.5, w0=10.0)
49
+ t, x = generate(k, duration=200.0, dt=0.005, seed=42)
50
+ ```
51
+
52
+ **Custom kernel:**
53
+
54
+ ```python
55
+ import numpy as np
56
+ from random_processes import generate, AutocorrKernel
57
+
58
+ k = AutocorrKernel(func=lambda tau: np.exp(-tau**2), name="gaussian")
59
+ t, x = generate(k, duration=50.0, dt=0.01, seed=7)
60
+ ```
61
+
62
+
63
+
64
+ ![Exponential kernel](figures/exponential.png)
65
+
66
+
67
+
68
+ ![Oscillating kernel](figures/oscillating.png)
69
+
70
+ ## Multivariate correlated noise
71
+
72
+ ```python
73
+ import numpy as np
74
+ from random_processes import generate, exponential_kernel, oscillating_kernel
75
+
76
+ kernels = [exponential_kernel(D=1.0, lam=2.0), oscillating_kernel(D=0.5, lam=1.0, w0=3.0)]
77
+ corr = np.array([[1.0, 0.7],
78
+ [0.7, 1.0]])
79
+ t, X = generate(kernels, corr=corr, duration=100.0, dt=0.01, seed=42)
80
+ # X.shape == (2, 10000)
81
+ ```
82
+
83
+ ## Visualization
84
+
85
+ ```python
86
+ from random_processes.visualization import plot_realization, plot_correlation
87
+ import matplotlib.pyplot as plt
88
+
89
+ fig, axes = plt.subplots(2, 1, figsize=(10, 6))
90
+ plot_realization(t, x, ax=axes[0])
91
+ plot_correlation(t, x, k, ax=axes[1])
92
+ plt.tight_layout()
93
+ plt.show()
94
+ ```
95
+
96
+ ## Testing
97
+
98
+ Confidence bands use Fisher z-transform with effective sample size to account for correlation in the data. This is an approximation (exact intervals require Bartlett's formula), but sufficient for validation purposes.
99
+
100
+ Scalar (with plots / without):
101
+ ```bash
102
+ python -c "from random_processes.testing import run_test_suite; run_test_suite()"
103
+ python -c "from random_processes.testing import run_test_suite; run_test_suite(show_plots=False)"
104
+ ```
105
+
106
+ Multivariate (with plots / without):
107
+ ```bash
108
+ python -c "from random_processes.testing import run_multi_test_suite; run_multi_test_suite()"
109
+ python -c "from random_processes.testing import run_multi_test_suite; run_multi_test_suite(show_plots=False)"
110
+ ```
111
+
112
+ ## Requirements
113
+
114
+ - Python >= 3.10
115
+ - numpy
116
+ - scipy
117
+ - matplotlib (for visualization)
@@ -0,0 +1,90 @@
1
+ # random-processes
2
+
3
+ Generate random processes with specified autocorrelation properties.
4
+
5
+ ## Quick start
6
+
7
+ **Exponential:** `R(tau) = D * exp(-lambda * |tau|)`
8
+
9
+ ```python
10
+ from random_processes import generate, exponential_kernel
11
+
12
+ k = exponential_kernel(D=1.0, lam=2.0)
13
+ t, x = generate(k, duration=100.0, dt=0.01, seed=42)
14
+ ```
15
+
16
+ **Oscillating:** `R(tau) = D * exp(-lambda * |tau|) * cos(w0 * tau)`
17
+
18
+ ```python
19
+ from random_processes import generate, oscillating_kernel
20
+
21
+ k = oscillating_kernel(D=2.0, lam=0.5, w0=10.0)
22
+ t, x = generate(k, duration=200.0, dt=0.005, seed=42)
23
+ ```
24
+
25
+ **Custom kernel:**
26
+
27
+ ```python
28
+ import numpy as np
29
+ from random_processes import generate, AutocorrKernel
30
+
31
+ k = AutocorrKernel(func=lambda tau: np.exp(-tau**2), name="gaussian")
32
+ t, x = generate(k, duration=50.0, dt=0.01, seed=7)
33
+ ```
34
+
35
+
36
+
37
+ ![Exponential kernel](figures/exponential.png)
38
+
39
+
40
+
41
+ ![Oscillating kernel](figures/oscillating.png)
42
+
43
+ ## Multivariate correlated noise
44
+
45
+ ```python
46
+ import numpy as np
47
+ from random_processes import generate, exponential_kernel, oscillating_kernel
48
+
49
+ kernels = [exponential_kernel(D=1.0, lam=2.0), oscillating_kernel(D=0.5, lam=1.0, w0=3.0)]
50
+ corr = np.array([[1.0, 0.7],
51
+ [0.7, 1.0]])
52
+ t, X = generate(kernels, corr=corr, duration=100.0, dt=0.01, seed=42)
53
+ # X.shape == (2, 10000)
54
+ ```
55
+
56
+ ## Visualization
57
+
58
+ ```python
59
+ from random_processes.visualization import plot_realization, plot_correlation
60
+ import matplotlib.pyplot as plt
61
+
62
+ fig, axes = plt.subplots(2, 1, figsize=(10, 6))
63
+ plot_realization(t, x, ax=axes[0])
64
+ plot_correlation(t, x, k, ax=axes[1])
65
+ plt.tight_layout()
66
+ plt.show()
67
+ ```
68
+
69
+ ## Testing
70
+
71
+ Confidence bands use Fisher z-transform with effective sample size to account for correlation in the data. This is an approximation (exact intervals require Bartlett's formula), but sufficient for validation purposes.
72
+
73
+ Scalar (with plots / without):
74
+ ```bash
75
+ python -c "from random_processes.testing import run_test_suite; run_test_suite()"
76
+ python -c "from random_processes.testing import run_test_suite; run_test_suite(show_plots=False)"
77
+ ```
78
+
79
+ Multivariate (with plots / without):
80
+ ```bash
81
+ python -c "from random_processes.testing import run_multi_test_suite; run_multi_test_suite()"
82
+ python -c "from random_processes.testing import run_multi_test_suite; run_multi_test_suite(show_plots=False)"
83
+ ```
84
+
85
+ ## Requirements
86
+
87
+ - Python >= 3.10
88
+ - numpy
89
+ - scipy
90
+ - matplotlib (for visualization)
@@ -0,0 +1,41 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "random-processes"
7
+ version = "0.1.0"
8
+ description = "Python library for generating random processes with specified autocorrelation properties. Supports custom kernels and multivariate correlated noise."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = {text = "MIT"}
12
+ authors = [
13
+ {name = "Alexander Abramov", email = "extremal.ru@gmail.com"},
14
+ ]
15
+ keywords = ["random-process", "colored-noise", "autocorrelation", "stochastic", "signal-processing", "spectral-method"]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Intended Audience :: Developers",
19
+ "Intended Audience :: Science/Research",
20
+ "License :: OSI Approved :: MIT License",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3.10",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Topic :: Scientific/Engineering :: Mathematics",
26
+ "Topic :: Scientific/Engineering :: Physics",
27
+ ]
28
+ dependencies = [
29
+ "numpy",
30
+ "scipy",
31
+ "matplotlib",
32
+ ]
33
+
34
+ [project.urls]
35
+ Homepage = "https://github.com/avabr/random-processes"
36
+ Repository = "https://github.com/avabr/random-processes"
37
+ Issues = "https://github.com/avabr/random-processes/issues"
38
+
39
+ [tool.setuptools.packages.find]
40
+ where = ["."]
41
+ include = ["random_processes*"]
@@ -0,0 +1,13 @@
1
+ """random_processes - Generate random processes with specified autocorrelation properties."""
2
+
3
+ from .kernel import AutocorrKernel, exponential_kernel, oscillating_kernel
4
+ from .generator import generate
5
+ from .multi import generate_multi
6
+
7
+ __all__ = [
8
+ "AutocorrKernel",
9
+ "exponential_kernel",
10
+ "oscillating_kernel",
11
+ "generate",
12
+ "generate_multi",
13
+ ]
@@ -0,0 +1,86 @@
1
+ """Spectral (FFT-based) generation of colored noise with a given autocorrelation kernel."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import warnings
6
+
7
+ import numpy as np
8
+
9
+ from .kernel import AutocorrKernel
10
+
11
+
12
+ def generate(
13
+ kernel: AutocorrKernel | list[AutocorrKernel],
14
+ duration: float,
15
+ dt: float,
16
+ seed: int | None = None,
17
+ corr: np.ndarray | None = None,
18
+ ) -> tuple[np.ndarray, np.ndarray]:
19
+ """Generate a realization of a stationary random process with the given autocorrelation.
20
+
21
+ Uses the spectral method: R(tau) -> S(f) via FFT -> filter white noise in frequency domain.
22
+
23
+ If a list of kernels is passed, generates multivariate correlated noise
24
+ (requires ``corr`` — an N×N correlation matrix).
25
+
26
+ Parameters
27
+ ----------
28
+ kernel : AutocorrKernel or list of AutocorrKernel
29
+ Autocorrelation kernel(s). A list activates multivariate mode.
30
+ duration : float
31
+ Total time of the realization.
32
+ dt : float
33
+ Time step between samples.
34
+ seed : int, optional
35
+ Random seed for reproducibility.
36
+ corr : ndarray, optional
37
+ Correlation matrix (N×N). Required when kernel is a list.
38
+
39
+ Returns
40
+ -------
41
+ t : ndarray of shape (n,)
42
+ Time array.
43
+ x : ndarray of shape (n,) or (N, n)
44
+ Process realization(s).
45
+ """
46
+ if isinstance(kernel, list):
47
+ from .multi import generate_multi
48
+ if corr is None:
49
+ raise ValueError("corr is required when kernel is a list of kernels.")
50
+ return generate_multi(kernel, corr, duration, dt, seed)
51
+
52
+ rng = np.random.default_rng(seed)
53
+
54
+ n = int(duration / dt)
55
+ # Use power-of-2 length for efficient FFT, with padding to avoid circular correlation artifacts
56
+ n_fft = 2 * n
57
+ n_fft = 1 << (n_fft - 1).bit_length() # next power of 2
58
+
59
+ # Build symmetric autocorrelation on the FFT grid
60
+ tau = np.arange(n_fft) * dt
61
+ tau[n_fft // 2 + 1:] = tau[n_fft // 2 + 1:] - n_fft * dt # make symmetric: [..., -2dt, -dt]
62
+ R = kernel(tau)
63
+
64
+ # Power spectral density = FFT of autocorrelation (Wiener-Khinchin)
65
+ S = np.fft.rfft(R).real
66
+ neg_fraction = np.sum(S < 0) / len(S)
67
+ if neg_fraction > 0.01:
68
+ warnings.warn(
69
+ f"Kernel may not be positive definite: {neg_fraction:.1%} of spectral "
70
+ f"density values are negative (min={S.min():.4g}). "
71
+ f"The output process will have distorted autocorrelation.",
72
+ stacklevel=2,
73
+ )
74
+ S = np.maximum(S, 0.0)
75
+
76
+ # Generate white noise in frequency domain and shape it
77
+ white = rng.standard_normal(n_fft)
78
+ X_freq = np.fft.rfft(white)
79
+ X_freq *= np.sqrt(S)
80
+
81
+ # Back to time domain
82
+ x_full = np.fft.irfft(X_freq, n=n_fft)
83
+ x = x_full[:n]
84
+
85
+ t = np.arange(n) * dt
86
+ return t, x
@@ -0,0 +1,68 @@
1
+ """Autocorrelation kernels for colored noise generation."""
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Callable
5
+
6
+ import numpy as np
7
+
8
+
9
+ @dataclass
10
+ class AutocorrKernel:
11
+ """Autocorrelation kernel R(tau) defining the statistical properties of a random process.
12
+
13
+ Parameters
14
+ ----------
15
+ func : callable
16
+ Autocorrelation function R(tau). Must satisfy R(0) > 0 and |R(tau)| <= R(0).
17
+ name : str
18
+ Human-readable name for display purposes.
19
+ params : dict
20
+ Kernel parameters (for introspection and display).
21
+ """
22
+
23
+ func: Callable[[np.ndarray], np.ndarray]
24
+ name: str = ""
25
+ params: dict = field(default_factory=dict)
26
+
27
+ def __call__(self, tau: np.ndarray) -> np.ndarray:
28
+ return self.func(tau)
29
+
30
+ @property
31
+ def variance(self) -> float:
32
+ return float(self.func(np.array([0.0]))[0])
33
+
34
+
35
+ def exponential_kernel(D: float, lam: float) -> AutocorrKernel:
36
+ """R(tau) = D * exp(-lambda * |tau|)
37
+
38
+ Parameters
39
+ ----------
40
+ D : float
41
+ Variance (dispersion) of the process.
42
+ lam : float
43
+ Decay rate (lambda). Larger values = faster decorrelation.
44
+ """
45
+ return AutocorrKernel(
46
+ func=lambda tau, D=D, lam=lam: D * np.exp(-lam * np.abs(tau)),
47
+ name="exponential",
48
+ params={"D": D, "lambda": lam},
49
+ )
50
+
51
+
52
+ def oscillating_kernel(D: float, lam: float, w0: float) -> AutocorrKernel:
53
+ """R(tau) = D * exp(-lambda * |tau|) * cos(w0 * tau)
54
+
55
+ Parameters
56
+ ----------
57
+ D : float
58
+ Variance (dispersion) of the process.
59
+ lam : float
60
+ Decay rate (lambda).
61
+ w0 : float
62
+ Oscillation frequency (rad/s).
63
+ """
64
+ return AutocorrKernel(
65
+ func=lambda tau, D=D, lam=lam, w0=w0: D * np.exp(-lam * np.abs(tau)) * np.cos(w0 * tau),
66
+ name="oscillating",
67
+ params={"D": D, "lambda": lam, "w0": w0},
68
+ )
@@ -0,0 +1,98 @@
1
+ """Multivariate correlated noise generation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import warnings
6
+
7
+ import numpy as np
8
+
9
+ from .kernel import AutocorrKernel
10
+
11
+
12
+ def generate_multi(
13
+ kernels: list[AutocorrKernel],
14
+ corr: np.ndarray,
15
+ duration: float,
16
+ dt: float,
17
+ seed: int | None = None,
18
+ ) -> tuple[np.ndarray, np.ndarray]:
19
+ """Generate N correlated random processes with given marginal autocorrelations.
20
+
21
+ Cross-spectral density is built as S_ij(f) = corr_ij * sqrt(S_i(f) * S_j(f)).
22
+
23
+ Parameters
24
+ ----------
25
+ kernels : list of AutocorrKernel
26
+ One kernel per channel (marginal autocorrelation).
27
+ corr : ndarray of shape (N, N)
28
+ Correlation matrix between channels. Must be positive definite.
29
+ duration : float
30
+ Total time of the realization.
31
+ dt : float
32
+ Time step between samples.
33
+ seed : int, optional
34
+ Random seed for reproducibility.
35
+
36
+ Returns
37
+ -------
38
+ t : ndarray of shape (n,)
39
+ Time array.
40
+ X : ndarray of shape (N, n)
41
+ Process realizations, one row per channel.
42
+ """
43
+ rng = np.random.default_rng(seed)
44
+ corr = np.asarray(corr, dtype=float)
45
+ n_ch = len(kernels)
46
+
47
+ # Validate correlation matrix
48
+ try:
49
+ np.linalg.cholesky(corr)
50
+ except np.linalg.LinAlgError:
51
+ raise ValueError("Correlation matrix is not positive definite.")
52
+
53
+ n = int(duration / dt)
54
+ n_fft = 2 * n
55
+ n_fft = 1 << (n_fft - 1).bit_length()
56
+ n_freq = n_fft // 2 + 1
57
+
58
+ # Build symmetric tau grid
59
+ tau = np.arange(n_fft) * dt
60
+ tau[n_fft // 2 + 1:] = tau[n_fft // 2 + 1:] - n_fft * dt
61
+
62
+ # Compute marginal PSDs
63
+ S = np.empty((n_ch, n_freq))
64
+ for i, kernel in enumerate(kernels):
65
+ R = kernel(tau)
66
+ S_i = np.fft.rfft(R).real
67
+ neg_fraction = np.sum(S_i < 0) / len(S_i)
68
+ if neg_fraction > 0.01:
69
+ warnings.warn(
70
+ f"Kernel '{kernel.name}' (channel {i}) may not be positive definite: "
71
+ f"{neg_fraction:.1%} of spectral density values are negative.",
72
+ stacklevel=2,
73
+ )
74
+ S[i] = np.maximum(S_i, 0.0)
75
+
76
+ # Build cross-spectral matrix and Cholesky at each frequency
77
+ # S_cross[i,j,f] = corr[i,j] * sqrt(S[i,f] * S[j,f])
78
+ sqrt_S = np.sqrt(S) # (n_ch, n_freq)
79
+ S_cross = corr[:, :, None] * (sqrt_S[:, None, :] * sqrt_S[None, :, :]) # (n_ch, n_ch, n_freq)
80
+
81
+ # Cholesky decomposition at each frequency: transpose to (n_freq, n_ch, n_ch)
82
+ S_cross = S_cross.transpose(2, 0, 1) # (n_freq, n_ch, n_ch)
83
+ # Add small diagonal for numerical stability
84
+ S_cross += np.eye(n_ch)[None, :, :] * 1e-30
85
+ L = np.linalg.cholesky(S_cross) # (n_freq, n_ch, n_ch)
86
+
87
+ # Generate N independent white noises in frequency domain
88
+ white = rng.standard_normal((n_ch, n_fft))
89
+ W = np.array([np.fft.rfft(white[i]) for i in range(n_ch)]) # (n_ch, n_freq)
90
+
91
+ # Mix channels: X_freq[i,f] = sum_j L[f,i,j] * W[j,f]
92
+ X_freq = np.einsum("fij,jf->if", L, W)
93
+
94
+ # Back to time domain
95
+ X = np.array([np.fft.irfft(X_freq[i], n=n_fft)[:n] for i in range(n_ch)])
96
+
97
+ t = np.arange(n) * dt
98
+ return t, X
@@ -0,0 +1,328 @@
1
+ """Statistical testing of generated processes against their theoretical kernels."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import numpy as np
6
+ from scipy import stats
7
+
8
+ from .kernel import AutocorrKernel, exponential_kernel, oscillating_kernel
9
+ from .generator import generate
10
+ from .visualization import plot_realization, plot_correlation
11
+
12
+
13
+ def _effective_sample_size(n: int, dt: float, kernel: AutocorrKernel) -> float:
14
+ """Estimate effective sample size for correlated data.
15
+
16
+ n_eff = n * dt / (2 * correlation_time), where correlation_time = integral_0^inf |R(tau)/R(0)| dtau,
17
+ estimated numerically.
18
+ """
19
+ R0 = kernel.variance
20
+ tau_max = 1000 * dt
21
+ tau_grid = np.linspace(0, tau_max, 10000)
22
+ r_norm = np.abs(kernel(tau_grid)) / R0
23
+ corr_time = np.trapz(r_norm, tau_grid)
24
+ n_eff = max(n * dt / (2 * corr_time), 10.0)
25
+ return n_eff
26
+
27
+
28
+ def correlation_test(
29
+ t: np.ndarray,
30
+ x: np.ndarray,
31
+ kernel: AutocorrKernel,
32
+ confidence: float = 0.95,
33
+ max_lag_fraction: float = 0.1,
34
+ ) -> dict:
35
+ """Test whether empirical autocorrelation fits theoretical within confidence band.
36
+
37
+ Uses Fisher z-transform with effective sample size to account for correlation.
38
+
39
+ Returns
40
+ -------
41
+ dict with keys:
42
+ passed : bool
43
+ hit_rate : float — fraction of lags within the confidence interval
44
+ variance_ok : bool — whether empirical variance is within confidence interval
45
+ empirical_variance : float
46
+ theoretical_variance : float
47
+ n_eff : float — effective sample size used
48
+ """
49
+ n = len(x)
50
+ dt = t[1] - t[0]
51
+ max_lag = int(n * max_lag_fraction)
52
+
53
+ x_centered = x - x.mean()
54
+ var_emp = np.mean(x_centered**2)
55
+ var_theory = kernel.variance
56
+
57
+ lags = np.arange(1, max_lag)
58
+ r_emp = np.array([np.mean(x_centered[:n - k] * x_centered[k:]) / var_emp for k in lags])
59
+
60
+ tau = lags * dt
61
+ r_theory = kernel(tau) / var_theory
62
+
63
+ # Fisher z-transform test with effective sample size
64
+ n_eff = _effective_sample_size(n, dt, kernel)
65
+ z_alpha = stats.norm.ppf((1 + confidence) / 2)
66
+ z_theory = np.arctanh(np.clip(r_theory, -0.999, 0.999))
67
+ se = 1.0 / np.sqrt(max(n_eff - 3, 4))
68
+ r_upper = np.tanh(z_theory + z_alpha * se)
69
+ r_lower = np.tanh(z_theory - z_alpha * se)
70
+
71
+ hits = np.sum((r_emp >= r_lower) & (r_emp <= r_upper))
72
+ hit_rate = hits / len(lags)
73
+
74
+ # Variance confidence interval (chi-squared with effective df)
75
+ df_eff = max(n_eff - 1, 2)
76
+ chi2_lo = stats.chi2.ppf((1 - confidence) / 2, df_eff)
77
+ chi2_hi = stats.chi2.ppf((1 + confidence) / 2, df_eff)
78
+ var_lo = df_eff * var_emp / chi2_hi
79
+ var_hi = df_eff * var_emp / chi2_lo
80
+ variance_ok = var_lo <= var_theory <= var_hi
81
+
82
+ return {
83
+ "passed": hit_rate >= confidence and variance_ok,
84
+ "hit_rate": float(hit_rate),
85
+ "variance_ok": variance_ok,
86
+ "empirical_variance": float(var_emp),
87
+ "theoretical_variance": float(var_theory),
88
+ "n_eff": float(n_eff),
89
+ }
90
+
91
+
92
+ def run_test_suite(show_plots: bool = True):
93
+ """Run tests for predefined kernels and optionally visualize results."""
94
+ import matplotlib.pyplot as plt
95
+
96
+ test_cases = [
97
+ # Exponential: varying decay and variance
98
+ {
99
+ "kernel": exponential_kernel(D=1.0, lam=2.0),
100
+ "duration": 100.0,
101
+ "dt": 0.01,
102
+ "seed": 42,
103
+ },
104
+ {
105
+ "kernel": exponential_kernel(D=0.5, lam=0.5),
106
+ "duration": 200.0,
107
+ "dt": 0.02,
108
+ "seed": 123,
109
+ },
110
+ {
111
+ "kernel": exponential_kernel(D=3.0, lam=10.0),
112
+ "duration": 50.0,
113
+ "dt": 0.005,
114
+ "seed": 11,
115
+ },
116
+ {
117
+ "kernel": exponential_kernel(D=0.1, lam=0.1),
118
+ "duration": 500.0,
119
+ "dt": 0.05,
120
+ "seed": 55,
121
+ },
122
+ # Oscillating: varying frequency and decay
123
+ {
124
+ "kernel": oscillating_kernel(D=1.0, lam=1.0, w0=5.0),
125
+ "duration": 100.0,
126
+ "dt": 0.01,
127
+ "seed": 77,
128
+ },
129
+ {
130
+ "kernel": oscillating_kernel(D=2.0, lam=0.5, w0=10.0),
131
+ "duration": 200.0,
132
+ "dt": 0.005,
133
+ "seed": 99,
134
+ },
135
+ {
136
+ "kernel": oscillating_kernel(D=1.0, lam=3.0, w0=20.0),
137
+ "duration": 50.0,
138
+ "dt": 0.002,
139
+ "seed": 33,
140
+ },
141
+ {
142
+ "kernel": oscillating_kernel(D=0.5, lam=0.2, w0=2.0),
143
+ "duration": 300.0,
144
+ "dt": 0.02,
145
+ "seed": 7,
146
+ },
147
+ ]
148
+
149
+ for i, case in enumerate(test_cases):
150
+ kernel = case["kernel"]
151
+ t, x = generate(kernel, case["duration"], case["dt"], seed=case["seed"])
152
+ result = correlation_test(t, x, kernel)
153
+
154
+ status = "PASSED" if result["passed"] else "FAILED"
155
+ print(
156
+ f"[{status}] {kernel.name} {kernel.params} | "
157
+ f"hit_rate={result['hit_rate']:.2%} "
158
+ f"var={result['empirical_variance']:.4f} (theory={result['theoretical_variance']:.4f}) "
159
+ f"var_ok={result['variance_ok']} n_eff={result['n_eff']:.0f}"
160
+ )
161
+
162
+ if show_plots:
163
+ fig, axes = plt.subplots(2, 1, figsize=(10, 6))
164
+ plot_realization(t, x, title=f"{kernel.name} {kernel.params}", ax=axes[0])
165
+ plot_correlation(t, x, kernel, ax=axes[1])
166
+ fig.tight_layout()
167
+
168
+ if show_plots:
169
+ plt.show()
170
+
171
+
172
+ def cross_correlation_test(
173
+ t: np.ndarray,
174
+ X: np.ndarray,
175
+ kernels: list[AutocorrKernel],
176
+ corr: np.ndarray,
177
+ confidence: float = 0.95,
178
+ ) -> dict:
179
+ """Test whether empirical cross-correlations match the specified correlation matrix.
180
+
181
+ Uses Fisher z-transform with effective sample size.
182
+
183
+ Returns
184
+ -------
185
+ dict with keys:
186
+ passed : bool
187
+ results : list of dicts, one per channel pair (i, j) where i < j
188
+ Each contains: i, j, rho_theory, rho_empirical, ci_lower, ci_upper, ok
189
+ n_eff : float
190
+ """
191
+ n_ch, n = X.shape
192
+ dt = t[1] - t[0]
193
+
194
+ # Compute marginal PSDs for theoretical cross-correlation calculation
195
+ n_fft = 2 * n
196
+ n_fft = 1 << (n_fft - 1).bit_length()
197
+ tau = np.arange(n_fft) * dt
198
+ tau[n_fft // 2 + 1:] = tau[n_fft // 2 + 1:] - n_fft * dt
199
+ S_all = []
200
+ for k in kernels:
201
+ S_k = np.maximum(np.fft.rfft(k(tau)).real, 0.0)
202
+ S_all.append(S_k)
203
+
204
+ # Effective sample size: use the slowest-decorrelating kernel
205
+ n_effs = [_effective_sample_size(n, dt, k) for k in kernels]
206
+ n_eff = min(n_effs)
207
+
208
+ z_alpha = stats.norm.ppf((1 + confidence) / 2)
209
+ se = 1.0 / np.sqrt(max(n_eff - 3, 4))
210
+
211
+ pair_results = []
212
+ for i in range(n_ch):
213
+ for j in range(i + 1, n_ch):
214
+ # True instantaneous correlation accounts for spectral shape mismatch:
215
+ # rho_actual = corr_ij * sum(sqrt(S_i * S_j)) / sqrt(sum(S_i) * sum(S_j))
216
+ cross_sum = np.sum(np.sqrt(S_all[i] * S_all[j]))
217
+ auto_prod = np.sqrt(np.sum(S_all[i]) * np.sum(S_all[j]))
218
+ rho_theory = float(corr[i, j] * cross_sum / auto_prod)
219
+ rho_emp = float(np.corrcoef(X[i], X[j])[0, 1])
220
+
221
+ z_theory = np.arctanh(np.clip(rho_theory, -0.999, 0.999))
222
+ ci_lower = float(np.tanh(z_theory - z_alpha * se))
223
+ ci_upper = float(np.tanh(z_theory + z_alpha * se))
224
+ ok = ci_lower <= rho_emp <= ci_upper
225
+
226
+ pair_results.append({
227
+ "i": i, "j": j,
228
+ "rho_theory": rho_theory,
229
+ "rho_empirical": rho_emp,
230
+ "ci_lower": ci_lower,
231
+ "ci_upper": ci_upper,
232
+ "ok": ok,
233
+ })
234
+
235
+ all_ok = all(r["ok"] for r in pair_results)
236
+
237
+ # Also test each channel's autocorrelation
238
+ autocorr_ok = True
239
+ for i in range(n_ch):
240
+ res = correlation_test(t, X[i], kernels[i], confidence=confidence)
241
+ if not res["passed"]:
242
+ autocorr_ok = False
243
+
244
+ return {
245
+ "passed": all_ok and autocorr_ok,
246
+ "cross_correlation_ok": all_ok,
247
+ "autocorrelation_ok": autocorr_ok,
248
+ "pairs": pair_results,
249
+ "n_eff": float(n_eff),
250
+ }
251
+
252
+
253
+ def run_multi_test_suite(show_plots: bool = True):
254
+ """Run tests for multivariate correlated noise generation."""
255
+ import matplotlib.pyplot as plt
256
+
257
+ multi_cases = [
258
+ {
259
+ "kernels": [exponential_kernel(D=1.0, lam=2.0), exponential_kernel(D=1.0, lam=2.0)],
260
+ "corr": np.array([[1.0, 0.8], [0.8, 1.0]]),
261
+ "duration": 200.0,
262
+ "dt": 0.01,
263
+ "seed": 42,
264
+ },
265
+ {
266
+ "kernels": [exponential_kernel(D=1.0, lam=2.0), oscillating_kernel(D=0.5, lam=1.0, w0=5.0)],
267
+ "corr": np.array([[1.0, 0.5], [0.5, 1.0]]),
268
+ "duration": 200.0,
269
+ "dt": 0.01,
270
+ "seed": 77,
271
+ },
272
+ {
273
+ "kernels": [exponential_kernel(D=1.0, lam=1.0), exponential_kernel(D=2.0, lam=0.5)],
274
+ "corr": np.array([[1.0, -0.6], [-0.6, 1.0]]),
275
+ "duration": 300.0,
276
+ "dt": 0.01,
277
+ "seed": 11,
278
+ },
279
+ {
280
+ "kernels": [
281
+ exponential_kernel(D=1.0, lam=2.0),
282
+ exponential_kernel(D=0.5, lam=1.0),
283
+ oscillating_kernel(D=1.0, lam=1.0, w0=3.0),
284
+ ],
285
+ "corr": np.array([
286
+ [1.0, 0.7, 0.3],
287
+ [0.7, 1.0, 0.5],
288
+ [0.3, 0.5, 1.0],
289
+ ]),
290
+ "duration": 500.0,
291
+ "dt": 0.01,
292
+ "seed": 99,
293
+ },
294
+ ]
295
+
296
+ for case in multi_cases:
297
+ kernels = case["kernels"]
298
+ corr_matrix = case["corr"]
299
+ n_ch = len(kernels)
300
+
301
+ t, X = generate(kernels, corr=corr_matrix, duration=case["duration"], dt=case["dt"], seed=case["seed"])
302
+ result = cross_correlation_test(t, X, kernels, corr_matrix)
303
+
304
+ status = "PASSED" if result["passed"] else "FAILED"
305
+ names = "+".join(k.name for k in kernels)
306
+ print(f"[{status}] {n_ch}ch {names} | autocorr={result['autocorrelation_ok']} n_eff={result['n_eff']:.0f}")
307
+ for p in result["pairs"]:
308
+ ok_str = "ok" if p["ok"] else "MISS"
309
+ print(
310
+ f" ch{p['i']}-ch{p['j']}: "
311
+ f"rho={p['rho_empirical']:+.4f} (theory={p['rho_theory']:+.2f}) "
312
+ f"CI=[{p['ci_lower']:+.4f}, {p['ci_upper']:+.4f}] [{ok_str}]"
313
+ )
314
+
315
+ if show_plots:
316
+ fig, axes = plt.subplots(n_ch, 1, figsize=(10, 3 * n_ch))
317
+ if n_ch == 1:
318
+ axes = [axes]
319
+ for i in range(n_ch):
320
+ plot_realization(t, X[i], title=f"ch{i}: {kernels[i].name} {kernels[i].params}", ax=axes[i])
321
+ fig.tight_layout()
322
+
323
+ if show_plots:
324
+ plt.show()
325
+
326
+
327
+ if __name__ == "__main__":
328
+ run_test_suite()
@@ -0,0 +1,80 @@
1
+ """Visualization utilities for colored noise analysis."""
2
+
3
+ import numpy as np
4
+ import matplotlib.pyplot as plt
5
+
6
+ from .kernel import AutocorrKernel
7
+
8
+
9
+ def plot_realization(t: np.ndarray, x: np.ndarray, title: str = "", ax=None):
10
+ """Plot a single realization of the random process."""
11
+ if ax is None:
12
+ _, ax = plt.subplots(figsize=(10, 3))
13
+ ax.plot(t, x, linewidth=0.5)
14
+ ax.set_xlabel("t")
15
+ ax.set_ylabel("x(t)")
16
+ ax.set_title(title or "Realization")
17
+ ax.grid(True, alpha=0.3)
18
+ return ax
19
+
20
+
21
+ def plot_correlation(
22
+ t: np.ndarray,
23
+ x: np.ndarray,
24
+ kernel: AutocorrKernel,
25
+ max_lag_fraction: float = 0.1,
26
+ confidence: float = 0.95,
27
+ ax=None,
28
+ ):
29
+ """Plot empirical vs theoretical normalized autocorrelation with confidence band.
30
+
31
+ Parameters
32
+ ----------
33
+ t, x : ndarray
34
+ Time and process arrays.
35
+ kernel : AutocorrKernel
36
+ Theoretical autocorrelation kernel.
37
+ max_lag_fraction : float
38
+ Fraction of total samples to use as max lag.
39
+ confidence : float
40
+ Confidence level for Fisher z-transform interval.
41
+ """
42
+ from scipy import stats
43
+ from .testing import _effective_sample_size
44
+
45
+ if ax is None:
46
+ _, ax = plt.subplots(figsize=(10, 4))
47
+
48
+ n = len(x)
49
+ max_lag = int(n * max_lag_fraction)
50
+ dt = t[1] - t[0]
51
+
52
+ # Empirical normalized autocorrelation
53
+ x_centered = x - x.mean()
54
+ var = np.mean(x_centered**2)
55
+ lags = np.arange(max_lag)
56
+ r_emp = np.array([np.mean(x_centered[:n - k] * x_centered[k:]) / var for k in lags])
57
+
58
+ tau = lags * dt
59
+ r_theory = kernel(tau) / kernel(np.array([0.0]))[0]
60
+
61
+ # Fisher z-transform confidence interval with effective sample size
62
+ n_eff = _effective_sample_size(n, dt, kernel)
63
+ z_alpha = stats.norm.ppf((1 + confidence) / 2)
64
+ z_theory = np.arctanh(np.clip(r_theory[1:], -0.999, 0.999))
65
+ se = 1.0 / np.sqrt(max(n_eff - 3, 4))
66
+ r_upper = np.tanh(z_theory + z_alpha * se)
67
+ r_lower = np.tanh(z_theory - z_alpha * se)
68
+
69
+ ax.plot(tau, r_emp, label="empirical", linewidth=1)
70
+ ax.plot(tau, r_theory, "--", label="theoretical", linewidth=1)
71
+ ax.fill_between(
72
+ tau[1:], r_lower, r_upper, alpha=0.2, color="orange",
73
+ label=f"{confidence:.0%} confidence",
74
+ )
75
+ ax.set_xlabel("tau")
76
+ ax.set_ylabel("R(tau) / R(0)")
77
+ ax.set_title(f"Autocorrelation — {kernel.name}")
78
+ ax.legend()
79
+ ax.grid(True, alpha=0.3)
80
+ return ax
@@ -0,0 +1,117 @@
1
+ Metadata-Version: 2.1
2
+ Name: random-processes
3
+ Version: 0.1.0
4
+ Summary: Python library for generating random processes with specified autocorrelation properties. Supports custom kernels and multivariate correlated noise.
5
+ Author-email: Alexander Abramov <extremal.ru@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/avabr/random-processes
8
+ Project-URL: Repository, https://github.com/avabr/random-processes
9
+ Project-URL: Issues, https://github.com/avabr/random-processes/issues
10
+ Keywords: random-process,colored-noise,autocorrelation,stochastic,signal-processing,spectral-method
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
20
+ Classifier: Topic :: Scientific/Engineering :: Physics
21
+ Requires-Python: >=3.10
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: numpy
25
+ Requires-Dist: scipy
26
+ Requires-Dist: matplotlib
27
+
28
+ # random-processes
29
+
30
+ Generate random processes with specified autocorrelation properties.
31
+
32
+ ## Quick start
33
+
34
+ **Exponential:** `R(tau) = D * exp(-lambda * |tau|)`
35
+
36
+ ```python
37
+ from random_processes import generate, exponential_kernel
38
+
39
+ k = exponential_kernel(D=1.0, lam=2.0)
40
+ t, x = generate(k, duration=100.0, dt=0.01, seed=42)
41
+ ```
42
+
43
+ **Oscillating:** `R(tau) = D * exp(-lambda * |tau|) * cos(w0 * tau)`
44
+
45
+ ```python
46
+ from random_processes import generate, oscillating_kernel
47
+
48
+ k = oscillating_kernel(D=2.0, lam=0.5, w0=10.0)
49
+ t, x = generate(k, duration=200.0, dt=0.005, seed=42)
50
+ ```
51
+
52
+ **Custom kernel:**
53
+
54
+ ```python
55
+ import numpy as np
56
+ from random_processes import generate, AutocorrKernel
57
+
58
+ k = AutocorrKernel(func=lambda tau: np.exp(-tau**2), name="gaussian")
59
+ t, x = generate(k, duration=50.0, dt=0.01, seed=7)
60
+ ```
61
+
62
+
63
+
64
+ ![Exponential kernel](figures/exponential.png)
65
+
66
+
67
+
68
+ ![Oscillating kernel](figures/oscillating.png)
69
+
70
+ ## Multivariate correlated noise
71
+
72
+ ```python
73
+ import numpy as np
74
+ from random_processes import generate, exponential_kernel, oscillating_kernel
75
+
76
+ kernels = [exponential_kernel(D=1.0, lam=2.0), oscillating_kernel(D=0.5, lam=1.0, w0=3.0)]
77
+ corr = np.array([[1.0, 0.7],
78
+ [0.7, 1.0]])
79
+ t, X = generate(kernels, corr=corr, duration=100.0, dt=0.01, seed=42)
80
+ # X.shape == (2, 10000)
81
+ ```
82
+
83
+ ## Visualization
84
+
85
+ ```python
86
+ from random_processes.visualization import plot_realization, plot_correlation
87
+ import matplotlib.pyplot as plt
88
+
89
+ fig, axes = plt.subplots(2, 1, figsize=(10, 6))
90
+ plot_realization(t, x, ax=axes[0])
91
+ plot_correlation(t, x, k, ax=axes[1])
92
+ plt.tight_layout()
93
+ plt.show()
94
+ ```
95
+
96
+ ## Testing
97
+
98
+ Confidence bands use Fisher z-transform with effective sample size to account for correlation in the data. This is an approximation (exact intervals require Bartlett's formula), but sufficient for validation purposes.
99
+
100
+ Scalar (with plots / without):
101
+ ```bash
102
+ python -c "from random_processes.testing import run_test_suite; run_test_suite()"
103
+ python -c "from random_processes.testing import run_test_suite; run_test_suite(show_plots=False)"
104
+ ```
105
+
106
+ Multivariate (with plots / without):
107
+ ```bash
108
+ python -c "from random_processes.testing import run_multi_test_suite; run_multi_test_suite()"
109
+ python -c "from random_processes.testing import run_multi_test_suite; run_multi_test_suite(show_plots=False)"
110
+ ```
111
+
112
+ ## Requirements
113
+
114
+ - Python >= 3.10
115
+ - numpy
116
+ - scipy
117
+ - matplotlib (for visualization)
@@ -0,0 +1,14 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ random_processes/__init__.py
5
+ random_processes/generator.py
6
+ random_processes/kernel.py
7
+ random_processes/multi.py
8
+ random_processes/testing.py
9
+ random_processes/visualization.py
10
+ random_processes.egg-info/PKG-INFO
11
+ random_processes.egg-info/SOURCES.txt
12
+ random_processes.egg-info/dependency_links.txt
13
+ random_processes.egg-info/requires.txt
14
+ random_processes.egg-info/top_level.txt
@@ -0,0 +1,3 @@
1
+ numpy
2
+ scipy
3
+ matplotlib
@@ -0,0 +1 @@
1
+ random_processes
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+