| """Numerical integrators for multiscale polynomial memory systems.""" |
| from __future__ import annotations |
|
|
| from dataclasses import dataclass |
| from typing import Iterable, Optional, Sequence, Tuple, Union |
|
|
| import numpy as np |
| from scipy.integrate import solve_ivp |
|
|
| from .utils import vec, unvec |
|
|
| State = np.ndarray |
| Params = Union[Tuple[np.ndarray, np.ndarray], Tuple[np.ndarray, np.ndarray, np.ndarray]] |
|
|
|
|
| @dataclass |
| class IntegrationResult: |
| """Container holding the final state and, optionally, the trajectory.""" |
|
|
| state: State |
| trajectory: Optional[np.ndarray] = None |
|
|
|
|
| def _validate_inputs(xs: np.ndarray, dt: float) -> None: |
| if xs.ndim != 1: |
| raise ValueError("`xs` must be a one-dimensional array") |
| if not np.isscalar(dt) or dt <= 0: |
| raise ValueError("`dt` must be a positive scalar") |
|
|
|
|
| def integrate_system( |
| xs: np.ndarray, |
| dt: float, |
| params: Params, |
| *, |
| state: Optional[np.ndarray] = None, |
| return_trajectory: bool = False, |
| ) -> IntegrationResult: |
| """Integrate a discrete control signal with either single or multiscale parameters.""" |
|
|
| _validate_inputs(xs, dt) |
| multiscale = len(params) == 3 |
|
|
| if multiscale: |
| A, B, M = params |
| n, m = B.shape |
| if state is None: |
| state = np.zeros(n * m, dtype=float) |
| elif state.shape != (n * m,): |
| raise ValueError(f"Expected state of shape {(n * m,)}, got {state.shape}") |
|
|
| def get_fun(x_val: float): |
| return lambda _, y: vec(A @ unvec(y, n, m) @ M + x_val * B) |
|
|
| else: |
| A, b = params |
| d = b.shape[0] |
| if state is None: |
| state = np.zeros(d, dtype=float) |
| elif state.shape != (d,): |
| raise ValueError(f"Expected state of shape {(d,)}, got {state.shape}") |
|
|
| def get_fun(x_val: float): |
| return lambda _, y: A @ y + b * x_val |
|
|
| trajectory: Optional[list[np.ndarray]] = [] if return_trajectory else None |
|
|
| for x_val in xs: |
| fun = get_fun(float(x_val)) |
| result = solve_ivp(fun, (0.0, float(dt)), state, method="RK45") |
| state = result.y[:, -1] |
| if return_trajectory: |
| trajectory.append(state.copy()) |
|
|
| if return_trajectory and trajectory is not None: |
| history = np.stack(trajectory, axis=0) |
| else: |
| history = None |
|
|
| return IntegrationResult(state=state, trajectory=history) |
|
|
|
|
| __all__ = ["IntegrationResult", "integrate_system"] |
|
|