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.
- random_processes-0.1.0/LICENSE +21 -0
- random_processes-0.1.0/PKG-INFO +117 -0
- random_processes-0.1.0/README.md +90 -0
- random_processes-0.1.0/pyproject.toml +41 -0
- random_processes-0.1.0/random_processes/__init__.py +13 -0
- random_processes-0.1.0/random_processes/generator.py +86 -0
- random_processes-0.1.0/random_processes/kernel.py +68 -0
- random_processes-0.1.0/random_processes/multi.py +98 -0
- random_processes-0.1.0/random_processes/testing.py +328 -0
- random_processes-0.1.0/random_processes/visualization.py +80 -0
- random_processes-0.1.0/random_processes.egg-info/PKG-INFO +117 -0
- random_processes-0.1.0/random_processes.egg-info/SOURCES.txt +14 -0
- random_processes-0.1.0/random_processes.egg-info/dependency_links.txt +1 -0
- random_processes-0.1.0/random_processes.egg-info/requires.txt +3 -0
- random_processes-0.1.0/random_processes.egg-info/top_level.txt +1 -0
- random_processes-0.1.0/setup.cfg +4 -0
|
@@ -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
|
+

|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+

|
|
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
|
+

|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+

|
|
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
|
+

|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+

|
|
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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
random_processes
|