Source code for neuralfieldmanifold.simulation.systems

"""Classical dynamical systems with NumPy integration and random streams."""

import numpy as np

from .._validation import positive_integer


[docs] def lorenz( n_samples=12000, *, dt=0.005, sigma=10.0, rho=28.0, beta=8 / 3, initial_state=(1.0, 1.0, 1.0), ): """Integrate the Lorenz system with fourth-order Runge-Kutta steps. Parameters ---------- n_samples : int Number of integration steps. dt : float Time step. sigma, rho, beta : float Lorenz system parameters. initial_state : tuple of float Initial x, y, and z coordinates. Returns ------- ndarray, shape (n_samples, 3) Trajectory beginning after the first integration step. The initial transient is included. """ n_samples = positive_integer(n_samples, "n_samples") if not np.isfinite(dt) or dt <= 0: raise ValueError("dt must be positive") state = np.asarray(initial_state, dtype=float) if state.shape != (3,) or not np.isfinite(state).all(): raise ValueError("initial_state must have three finite coordinates") x, y, z = state out = np.empty((n_samples, 3)) def derivative(x, y, z): return sigma * (y - x), x * (rho - z) - y, x * y - beta * z for i in range(n_samples): k1 = derivative(x, y, z) k2 = derivative( x + 0.5 * dt * k1[0], y + 0.5 * dt * k1[1], z + 0.5 * dt * k1[2] ) k3 = derivative( x + 0.5 * dt * k2[0], y + 0.5 * dt * k2[1], z + 0.5 * dt * k2[2] ) k4 = derivative(x + dt * k3[0], y + dt * k3[1], z + dt * k3[2]) x += (dt / 6.0) * (k1[0] + 2 * k2[0] + 2 * k3[0] + k4[0]) y += (dt / 6.0) * (k1[1] + 2 * k2[1] + 2 * k3[1] + k4[1]) z += (dt / 6.0) * (k1[2] + 2 * k2[2] + 2 * k3[2] + k4[2]) out[i] = x, y, z return out
[docs] def ornstein_uhlenbeck( n_samples, *, dt=0.005, theta=2.0, mean=0.0, sigma=1.0, initial=0.0, seed=0, method="exact", innovations=None, ): """OU process using exact or Euler transitions; first state follows one step. Supplied ``innovations`` are standard-normal draws, not scaled noise. """ n_samples = positive_integer(n_samples, "n_samples") if dt <= 0 or theta <= 0 or sigma < 0 or method not in {"exact", "euler"}: raise ValueError("Invalid OU parameters") draws = ( np.random.default_rng(seed).normal(size=n_samples) if innovations is None else np.asarray(innovations) ) if draws.shape != (n_samples,) or not np.isfinite(draws).all(): raise ValueError("Invalid innovations") rate = np.exp(-theta * dt) sd = sigma * np.sqrt((1 - np.exp(-2 * theta * dt)) / (2 * theta)) state = initial out = np.empty(n_samples) for i, draw in enumerate(draws): state = ( state * rate + mean * (1 - rate) + sd * draw if method == "exact" else state - theta * dt * (state - mean) + sigma * np.sqrt(dt) * draw ) out[i] = state return out
[docs] def rotating_var( n_samples, *, burn_in_samples=300, alpha=(0.45, 0.35, 0.30), beta=(-0.10, -0.08, -0.06), periods_samples=(500.0, 900.0), noise_scale=1.0, seed=0, innovations=None, ): """Three-dimensional rotating VAR(2), returned as a NumPy array. Innovations have shape ``(n_samples + burn_in_samples, 3)`` and are supplied already scaled when provided. """ n_samples = positive_integer(n_samples, "n_samples") if burn_in_samples < 0 or noise_scale < 0 or min(periods_samples) <= 0: raise ValueError("Invalid burn-in, noise scale, or rotation periods") alpha, beta = np.asarray(alpha), np.asarray(beta) if alpha.shape != (3,) or beta.shape != (3,): raise ValueError("alpha and beta must have three entries") total = n_samples + burn_in_samples noise = ( np.random.default_rng(seed).normal(size=(total, 3)) * noise_scale if innovations is None else np.asarray(innovations) ) if noise.shape != (total, 3) or not np.isfinite(noise).all(): raise ValueError("Invalid innovations") def rz(t): c, s = np.cos(t), np.sin(t) return np.array([[c, -s, 0], [s, c, 0], [0, 0, 1]]) def ry(t): c, s = np.cos(t), np.sin(t) return np.array([[c, 0, s], [0, 1, 0], [-s, 0, c]]) previous, older = np.zeros(3), np.zeros(3) out = np.empty((total, 3)) for k in range(total): t1 = 2 * np.pi * k / periods_samples[0] t2 = 2 * np.pi * k / periods_samples[1] + 0.7 r1, r2 = rz(t1) @ ry(t1 * 0.5), ry(t2) @ rz(t2 * 0.5) current = ( r1 @ np.diag(alpha) @ r1.T @ previous + r2 @ np.diag(beta) @ r2.T @ older + noise[k] ) out[k] = current previous, older = current, previous return out[burn_in_samples:]