from __future__ import annotations from dataclasses import dataclass import numpy as np @dataclass class RecurrentFeatures: input_weight: np.ndarray recurrent_weight: np.ndarray bias: np.ndarray readout: np.ndarray | None = None @property def hidden_size(self) -> int: return len(self.bias) def states(self, series: np.ndarray) -> np.ndarray: hidden = np.zeros(self.hidden_size, dtype=np.float64) outputs = np.empty((len(series), self.hidden_size), dtype=np.float64) for index, value in enumerate(series): hidden = np.tanh( self.input_weight * value + self.recurrent_weight @ hidden + self.bias ) outputs[index] = hidden return outputs def design(self, series: np.ndarray) -> np.ndarray: states = self.states(series) return np.column_stack([states, series, np.ones(len(series))]) def predict(self, series: np.ndarray) -> np.ndarray: if self.readout is None: raise RuntimeError("Fit or load a readout before prediction.") return self.design(series) @ self.readout def unpack_genome(genome: np.ndarray, hidden_size: int) -> RecurrentFeatures: cursor = 0 input_weight = genome[cursor : cursor + hidden_size] cursor += hidden_size raw_recurrent = genome[cursor : cursor + hidden_size**2].reshape( hidden_size, hidden_size, ) cursor += hidden_size**2 bias = genome[cursor : cursor + hidden_size] radius = max(abs(np.linalg.eigvals(raw_recurrent)).max(), 1e-8) recurrent = raw_recurrent * (0.95 / max(float(radius), 0.95)) return RecurrentFeatures(input_weight, recurrent, bias) def fit_readout( model: RecurrentFeatures, series: np.ndarray, targets: np.ndarray, indices: np.ndarray, ridge: float = 1e-4, ) -> np.ndarray: design = model.design(series)[indices] gram = design.T @ design + ridge * np.eye(design.shape[1]) model.readout = np.linalg.solve(gram, design.T @ targets[indices]) return model.readout def parameter_count(hidden_size: int, horizons: int) -> int: recurrent = hidden_size + hidden_size**2 + hidden_size readout = (hidden_size + 2) * horizons return recurrent + readout