Download forcing.py from sparsetrace/dima: direct link, hf CLI and curl.
- Browser
- Download file 16.3 kB
-
https://huggingface.co/sparsetrace/dima/resolve/main/forcing.py
- Command line
-
hf download hf://sparsetrace/dima/forcing.py
-
curl -L -o forcing.py https://huggingface.co/sparsetrace/dima/resolve/main/forcing.py
16.3 kB
| # forcing.py | |
| """ | |
| Forced / nonstationary variants of chaotic attractors. | |
| Standard output is always a single long trajectory: | |
| R_tX : (T-burn_in, D) | |
| For ODEs, forcing MUST be applied inside the integrator (nonautonomous RK4), | |
| not post-hoc. This file provides three forcing mechanisms for Lorenz-63: | |
| 1) Additive forcing to the dynamics (adds kappa*u(t) to dx/dt, dy/dt, dz/dt) | |
| 2) Parameter drift (e.g., rho(t) varies with time / forcing) | |
| 3) Regime switching (piecewise-constant params over time) | |
| All functions accept (T, dt) as primary inputs, with reasonable defaults. | |
| """ | |
| from __future__ import annotations | |
| from dataclasses import dataclass | |
| from typing import Callable, Dict, Optional, Sequence, Tuple, Union | |
| import numpy as np | |
| Array = np.ndarray | |
| # ----------------------------- | |
| # small utils | |
| # ----------------------------- | |
| def _rng_from_seed(seed: Optional[int]) -> np.random.Generator: | |
| return np.random.default_rng(None if seed is None else int(seed)) | |
| def _discard_burn_in(traj: Array, burn_in: int) -> Array: | |
| burn_in = int(burn_in) | |
| if burn_in <= 0: | |
| return traj | |
| if burn_in >= traj.shape[0]: | |
| raise ValueError(f"burn_in={burn_in} must be < T={traj.shape[0]}") | |
| return traj[burn_in:] | |
| def _as_1d(u: Union[None, float, Array]) -> Optional[Array]: | |
| if u is None: | |
| return None | |
| if np.isscalar(u): | |
| return np.asarray([float(u)], dtype=float) | |
| u = np.asarray(u, dtype=float) | |
| if u.ndim != 1: | |
| raise ValueError(f"Expected 1D forcing array, got shape {u.shape}") | |
| return u | |
| def _build_time_grid(T: int, dt: float) -> Array: | |
| return np.arange(int(T), dtype=float) * float(dt) | |
| def _interp_u(u_t: Array, t_grid: Array, tcur: float) -> float: | |
| """ | |
| Forcing evaluated at arbitrary time using linear interpolation. | |
| Uses endpoint values outside the grid. | |
| """ | |
| if u_t.size == 1: | |
| return float(u_t[0]) | |
| return float(np.interp(tcur, t_grid, u_t, left=u_t[0], right=u_t[-1])) | |
| def _kappa_vec(kappa: Union[float, Sequence[float], Array], axis: Optional[int] = None) -> Array: | |
| """ | |
| Convert kappa specification into a length-3 vector for Lorenz63. | |
| - If axis is provided, interpret kappa as scalar magnitude applied to that axis. | |
| - Else if kappa is scalar -> applied to x-axis by default. | |
| - Else if kappa has length 3 -> used directly. | |
| """ | |
| if axis is not None: | |
| kv = np.zeros(3, dtype=float) | |
| kv[int(axis)] = float(kappa) # type: ignore[arg-type] | |
| return kv | |
| if np.isscalar(kappa): | |
| # default: force x-equation | |
| return np.array([float(kappa), 0.0, 0.0], dtype=float) | |
| kv = np.asarray(kappa, dtype=float).reshape(-1) | |
| if kv.size != 3: | |
| raise ValueError("kappa must be scalar, length-3, or scalar+axis") | |
| return kv | |
| # ----------------------------- | |
| # forcing signal helpers (optional) | |
| # ----------------------------- | |
| def forcing_sine(T: int, dt: float, *, amp: float = 1.0, freq_hz: float = 0.1, phase: float = 0.0, bias: float = 0.0) -> Array: | |
| """ | |
| u(t) = bias + amp * sin(2*pi*freq_hz*t + phase) | |
| Returns u_t of length T aligned with sample times n*dt. | |
| """ | |
| t = _build_time_grid(T, dt) | |
| return (bias + amp * np.sin(2.0 * np.pi * float(freq_hz) * t + float(phase))).astype(float) | |
| def forcing_piecewise_constant(T: int, *, values: Sequence[float], lengths: Sequence[int]) -> Array: | |
| """ | |
| Build a piecewise-constant forcing u_t of length T. | |
| Example: values=[0,1,0.5], lengths=[2000,3000,5000] | |
| """ | |
| vals = list(values) | |
| lens = list(map(int, lengths)) | |
| if len(vals) != len(lens): | |
| raise ValueError("values and lengths must have same length") | |
| u = np.concatenate([np.full(L, v, dtype=float) for v, L in zip(vals, lens)], axis=0) | |
| if u.size < T: | |
| u = np.pad(u, (0, T - u.size), mode="edge") | |
| return u[:T] | |
| # ----------------------------- | |
| # core integrator: nonautonomous RK4 for Lorenz63 | |
| # ----------------------------- | |
| def _lorenz63_step_rk4( | |
| x: Array, | |
| t: float, | |
| dt: float, | |
| *, | |
| sigma_fn: Callable[[float], float], | |
| rho_fn: Callable[[float], float], | |
| beta_fn: Callable[[float], float], | |
| force_fn: Callable[[float], Array], # returns (3,) additive term to dx/dt | |
| ) -> Array: | |
| """ | |
| One RK4 step for x' = f(t,x) with Lorenz63 + forcing. | |
| """ | |
| x = np.asarray(x, dtype=float) | |
| def f(tcur: float, s: Array) -> Array: | |
| sigma = float(sigma_fn(tcur)) | |
| rho = float(rho_fn(tcur)) | |
| beta = float(beta_fn(tcur)) | |
| fx = np.empty(3, dtype=float) | |
| fx[0] = sigma * (s[1] - s[0]) | |
| fx[1] = s[0] * (rho - s[2]) - s[1] | |
| fx[2] = s[0] * s[1] - beta * s[2] | |
| fx += force_fn(tcur) | |
| return fx | |
| k1 = f(t, x) | |
| k2 = f(t + 0.5 * dt, x + 0.5 * dt * k1) | |
| k3 = f(t + 0.5 * dt, x + 0.5 * dt * k2) | |
| k4 = f(t + dt, x + dt * k3) | |
| return x + (dt / 6.0) * (k1 + 2 * k2 + 2 * k3 + k4) | |
| def _simulate_lorenz63_nonauto( | |
| T: int, | |
| dt: float, | |
| *, | |
| x0: Array, | |
| sigma_fn: Callable[[float], float], | |
| rho_fn: Callable[[float], float], | |
| beta_fn: Callable[[float], float], | |
| force_fn: Callable[[float], Array], | |
| ) -> Array: | |
| T = int(T) | |
| dt = float(dt) | |
| x = np.asarray(x0, dtype=float).reshape(3).copy() | |
| traj = np.zeros((T, 3), dtype=float) | |
| t = 0.0 | |
| for n in range(T): | |
| traj[n] = x | |
| x = _lorenz63_step_rk4( | |
| x, t, dt, | |
| sigma_fn=sigma_fn, | |
| rho_fn=rho_fn, | |
| beta_fn=beta_fn, | |
| force_fn=force_fn, | |
| ) | |
| t += dt | |
| return traj | |
| # ----------------------------- | |
| # 1) Additive forcing: x' = f(x) + kappa * u(t) | |
| # ----------------------------- | |
| def lorenz63_forced_additive( | |
| T: int, | |
| dt: float = 0.01, | |
| *, | |
| sigma: float = 10.0, | |
| rho: float = 28.0, | |
| beta: float = 8.0 / 3.0, | |
| u_t: Optional[Array] = None, | |
| u_fn: Optional[Callable[[float], float]] = None, | |
| kappa: Union[float, Sequence[float], Array] = 1.0, | |
| axis: Optional[int] = 0, | |
| x0: Optional[Array] = None, | |
| seed: Optional[int] = 0, | |
| burn_in: int = 0, | |
| return_u: bool = False, | |
| ) -> Union[Array, Tuple[Array, Array]]: | |
| """ | |
| Additive forcing applied to dx/dt,dy/dt,dz/dt. | |
| You may provide either: | |
| - u_t: array of length T (sampled at n*dt), or | |
| - u_fn: callable u(t) | |
| kappa: | |
| - scalar + axis (default) => applies to one coordinate derivative | |
| - length-3 vector => applies to all derivatives | |
| - scalar with axis=None => defaults to x-axis | |
| Returns: | |
| traj : (T-burn_in, 3) | |
| and optionally u_used : (T,) if return_u=True | |
| """ | |
| rng = _rng_from_seed(seed) | |
| if x0 is None: | |
| x0 = np.array([1.0, 1.0, 1.0], dtype=float) + 0.01 * rng.standard_normal(3) | |
| else: | |
| x0 = np.asarray(x0, dtype=float).reshape(3) | |
| kv = _kappa_vec(kappa, axis=axis) | |
| # build forcing evaluator | |
| if u_fn is None: | |
| ut = _as_1d(u_t) | |
| if ut is None: | |
| ut = np.zeros(int(T), dtype=float) | |
| # ensure length T | |
| if ut.size != int(T): | |
| if ut.size < int(T): | |
| ut = np.pad(ut, (0, int(T) - ut.size), mode="edge") | |
| ut = ut[: int(T)] | |
| t_grid = _build_time_grid(T, dt) | |
| def u_eval(tcur: float) -> float: | |
| return _interp_u(ut, t_grid, tcur) | |
| u_used = ut | |
| else: | |
| def u_eval(tcur: float) -> float: | |
| return float(u_fn(tcur)) | |
| # produce a sampled u for logging/return | |
| t_grid = _build_time_grid(T, dt) | |
| u_used = np.array([u_eval(ti) for ti in t_grid], dtype=float) | |
| def sigma_fn(_t: float) -> float: | |
| return float(sigma) | |
| def rho_fn(_t: float) -> float: | |
| return float(rho) | |
| def beta_fn(_t: float) -> float: | |
| return float(beta) | |
| def force_fn(tcur: float) -> Array: | |
| return kv * u_eval(tcur) | |
| traj = _simulate_lorenz63_nonauto( | |
| T=T, dt=dt, x0=x0, | |
| sigma_fn=sigma_fn, rho_fn=rho_fn, beta_fn=beta_fn, | |
| force_fn=force_fn | |
| ) | |
| traj = _discard_burn_in(traj, burn_in) | |
| u_used2 = _discard_burn_in(u_used[:, None], burn_in).reshape(-1) # align | |
| return (traj, u_used2) if return_u else traj | |
| # ----------------------------- | |
| # 2) Parameter drift: rho(t) varies (optionally driven by u(t)) | |
| # ----------------------------- | |
| def lorenz63_param_drift( | |
| T: int, | |
| dt: float = 0.01, | |
| *, | |
| sigma: float = 10.0, | |
| rho0: float = 28.0, | |
| beta: float = 8.0 / 3.0, | |
| # specify rho(t) either directly or via forcing | |
| rho_t: Optional[Array] = None, # length T at n*dt | |
| rho_fn: Optional[Callable[[float], float]] = None, | |
| u_t: Optional[Array] = None, # used only if rho_t/rho_fn not provided | |
| u_fn: Optional[Callable[[float], float]] = None, | |
| kappa_rho: float = 1.0, # rho(t) = rho0 + kappa_rho * u(t) | |
| # optional additive forcing simultaneously (set to 0 to disable) | |
| kappa_force: Union[float, Sequence[float], Array] = 0.0, | |
| axis_force: Optional[int] = 0, | |
| x0: Optional[Array] = None, | |
| seed: Optional[int] = 0, | |
| burn_in: int = 0, | |
| return_rho: bool = False, | |
| ) -> Union[Array, Tuple[Array, Array]]: | |
| """ | |
| Nonstationary Lorenz63 where rho(t) drifts. | |
| Priority for defining rho(t): | |
| 1) rho_fn(t) | |
| 2) rho_t sampled at n*dt | |
| 3) rho0 + kappa_rho * u(t) (u from u_fn or u_t; defaults to 0) | |
| You can also add *simultaneous additive forcing* via kappa_force/axis_force. | |
| Returns: | |
| traj : (T-burn_in, 3) | |
| and optionally rho_used : (T-burn_in,) if return_rho=True | |
| """ | |
| rng = _rng_from_seed(seed) | |
| if x0 is None: | |
| x0 = np.array([1.0, 1.0, 1.0], dtype=float) + 0.01 * rng.standard_normal(3) | |
| else: | |
| x0 = np.asarray(x0, dtype=float).reshape(3) | |
| # build u evaluator (only if needed) | |
| ut = _as_1d(u_t) | |
| if ut is None: | |
| ut = np.zeros(int(T), dtype=float) | |
| if ut.size != int(T): | |
| if ut.size < int(T): | |
| ut = np.pad(ut, (0, int(T) - ut.size), mode="edge") | |
| ut = ut[: int(T)] | |
| t_grid = _build_time_grid(T, dt) | |
| if u_fn is None: | |
| def u_eval(tcur: float) -> float: | |
| return _interp_u(ut, t_grid, tcur) | |
| u_used = ut | |
| else: | |
| def u_eval(tcur: float) -> float: | |
| return float(u_fn(tcur)) | |
| u_used = np.array([u_eval(ti) for ti in t_grid], dtype=float) | |
| # build rho evaluator | |
| if rho_fn is not None: | |
| def rho_eval(tcur: float) -> float: | |
| return float(rho_fn(tcur)) | |
| rho_used = np.array([rho_eval(ti) for ti in t_grid], dtype=float) | |
| elif rho_t is not None: | |
| rt = _as_1d(rho_t) | |
| if rt is None: | |
| raise ValueError("rho_t must be a 1D array if provided") | |
| if rt.size != int(T): | |
| if rt.size < int(T): | |
| rt = np.pad(rt, (0, int(T) - rt.size), mode="edge") | |
| rt = rt[: int(T)] | |
| def rho_eval(tcur: float) -> float: | |
| return float(np.interp(tcur, t_grid, rt, left=rt[0], right=rt[-1])) | |
| rho_used = rt | |
| else: | |
| def rho_eval(tcur: float) -> float: | |
| return float(rho0 + kappa_rho * u_eval(tcur)) | |
| rho_used = np.array([rho_eval(ti) for ti in t_grid], dtype=float) | |
| kv_force = _kappa_vec(kappa_force, axis=axis_force) | |
| def sigma_fn(_t: float) -> float: | |
| return float(sigma) | |
| def rho_fn2(tcur: float) -> float: | |
| return float(rho_eval(tcur)) | |
| def beta_fn(_t: float) -> float: | |
| return float(beta) | |
| def force_fn(tcur: float) -> Array: | |
| # optional additive forcing in dynamics | |
| return kv_force * u_eval(tcur) if np.any(kv_force != 0.0) else np.zeros(3, dtype=float) | |
| traj = _simulate_lorenz63_nonauto( | |
| T=T, dt=dt, x0=x0, | |
| sigma_fn=sigma_fn, rho_fn=rho_fn2, beta_fn=beta_fn, | |
| force_fn=force_fn | |
| ) | |
| traj = _discard_burn_in(traj, burn_in) | |
| rho_used2 = _discard_burn_in(rho_used[:, None], burn_in).reshape(-1) | |
| return (traj, rho_used2) if return_rho else traj | |
| # ----------------------------- | |
| # 3) Regime switching: piecewise-constant params (sigma,rho,beta) | |
| # ----------------------------- | |
| class Lorenz63Params: | |
| sigma: float = 10.0 | |
| rho: float = 28.0 | |
| beta: float = 8.0 / 3.0 | |
| def lorenz63_regime_switch( | |
| T: int, | |
| dt: float = 0.01, | |
| *, | |
| # a schedule of regimes: (start_step, params) | |
| schedule: Sequence[Tuple[int, Lorenz63Params]] = ((0, Lorenz63Params()),), | |
| # optional additive forcing simultaneously | |
| u_t: Optional[Array] = None, | |
| u_fn: Optional[Callable[[float], float]] = None, | |
| kappa: Union[float, Sequence[float], Array] = 0.0, | |
| axis: Optional[int] = 0, | |
| x0: Optional[Array] = None, | |
| seed: Optional[int] = 0, | |
| burn_in: int = 0, | |
| return_params: bool = False, | |
| ) -> Union[Array, Tuple[Array, Dict[str, Array]]]: | |
| """ | |
| Piecewise-constant parameter switching. `schedule` is a list of | |
| (start_step, Lorenz63Params). Example: | |
| schedule = [ | |
| (0, Lorenz63Params(rho=28.0)), | |
| (8000, Lorenz63Params(rho=35.0)), | |
| (14000,Lorenz63Params(rho=25.0)), | |
| ] | |
| Regime selection at time t uses idx = floor(t/dt), then picks the last | |
| schedule entry with start_step <= idx. | |
| Returns: | |
| traj : (T-burn_in, 3) | |
| and optionally a dict with sigma_t, rho_t, beta_t arrays (aligned). | |
| """ | |
| rng = _rng_from_seed(seed) | |
| if x0 is None: | |
| x0 = np.array([1.0, 1.0, 1.0], dtype=float) + 0.01 * rng.standard_normal(3) | |
| else: | |
| x0 = np.asarray(x0, dtype=float).reshape(3) | |
| # build regime arrays (per step) | |
| T = int(T) | |
| sigma_t = np.empty(T, dtype=float) | |
| rho_t = np.empty(T, dtype=float) | |
| beta_t = np.empty(T, dtype=float) | |
| sched = sorted([(int(s), p) for s, p in schedule], key=lambda z: z[0]) | |
| if not sched or sched[0][0] != 0: | |
| raise ValueError("schedule must start at step 0") | |
| # fill by segments | |
| for i, (s0, p0) in enumerate(sched): | |
| s1 = sched[i + 1][0] if i + 1 < len(sched) else T | |
| s0 = max(0, min(T, s0)) | |
| s1 = max(s0, min(T, s1)) | |
| sigma_t[s0:s1] = float(p0.sigma) | |
| rho_t[s0:s1] = float(p0.rho) | |
| beta_t[s0:s1] = float(p0.beta) | |
| # forcing evaluator (optional) | |
| kv = _kappa_vec(kappa, axis=axis) | |
| if u_fn is None: | |
| ut = _as_1d(u_t) | |
| if ut is None: | |
| ut = np.zeros(T, dtype=float) | |
| if ut.size != T: | |
| if ut.size < T: | |
| ut = np.pad(ut, (0, T - ut.size), mode="edge") | |
| ut = ut[:T] | |
| t_grid = _build_time_grid(T, dt) | |
| def u_eval(tcur: float) -> float: | |
| return _interp_u(ut, t_grid, tcur) | |
| u_used = ut | |
| else: | |
| t_grid = _build_time_grid(T, dt) | |
| def u_eval(tcur: float) -> float: | |
| return float(u_fn(tcur)) | |
| u_used = np.array([u_eval(ti) for ti in t_grid], dtype=float) | |
| # parameter evaluators: stepwise constant | |
| def _param_at(arr: Array, tcur: float) -> float: | |
| idx = int(tcur / float(dt)) | |
| if idx < 0: | |
| idx = 0 | |
| elif idx >= T: | |
| idx = T - 1 | |
| return float(arr[idx]) | |
| def sigma_fn(tcur: float) -> float: | |
| return _param_at(sigma_t, tcur) | |
| def rho_fn(tcur: float) -> float: | |
| return _param_at(rho_t, tcur) | |
| def beta_fn(tcur: float) -> float: | |
| return _param_at(beta_t, tcur) | |
| def force_fn(tcur: float) -> Array: | |
| if np.all(kv == 0.0): | |
| return np.zeros(3, dtype=float) | |
| return kv * u_eval(tcur) | |
| traj = _simulate_lorenz63_nonauto( | |
| T=T, dt=dt, x0=x0, | |
| sigma_fn=sigma_fn, rho_fn=rho_fn, beta_fn=beta_fn, | |
| force_fn=force_fn | |
| ) | |
| traj = _discard_burn_in(traj, burn_in) | |
| if not return_params: | |
| return traj | |
| # align parameter arrays with burn_in | |
| sigma_used = _discard_burn_in(sigma_t[:, None], burn_in).reshape(-1) | |
| rho_used = _discard_burn_in(rho_t[:, None], burn_in).reshape(-1) | |
| beta_used = _discard_burn_in(beta_t[:, None], burn_in).reshape(-1) | |
| u_used2 = _discard_burn_in(u_used[:, None], burn_in).reshape(-1) | |
| info = { | |
| "sigma_t": sigma_used, | |
| "rho_t": rho_used, | |
| "beta_t": beta_used, | |
| "u_t": u_used2, | |
| } | |
| return traj, info | |