Source code for qilisdk.ml.datasets.narma
# Copyright 2026 Qilimanjaro Quantum Tech
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
import numpy as np
from qilisdk.ml.datasets.dataset import Dataset, DatasetSample
# Above this order the plain recurrence can diverge, so the update is squashed
# with a ``tanh`` to keep the series bounded (standard practice in the literature).
_SQUASH_ORDER_THRESHOLD = 10
[docs]
class NARMA(Dataset):
r"""
Nonlinear Auto-Regressive Moving Average (NARMA) benchmark.
NARMA is a classic system-identification task. A
random input stream :math:`u(t) \sim \mathcal{U}(0, 0.5)` drives an
order-:math:`n` nonlinear recurrence whose output :math:`y(t)` must be
predicted from :math:`u`:
.. math::
y(t+1) = \alpha\, y(t)
+ \beta\, y(t) \sum_{i=0}^{n-1} y(t-i)
+ \gamma\, u(t-n+1)\, u(t)
+ \delta.
The default coefficients :math:`(\alpha, \beta, \gamma, \delta) =
(0.3, 0.05, 1.5, 0.1)` correspond to the ubiquitous ``NARMA10`` task
(``order=10``).
"""
_DEFAULT_DRAW_STYLE = "1d"
_DRAW_COMPONENT_LABELS = ("u",)
def __init__(
self,
order: int = 10,
*,
alpha: float = 0.3,
beta: float = 0.05,
gamma: float = 1.5,
delta: float = 0.1,
input_range: tuple[float, float] = (0.0, 0.5),
seed: int | None = None,
) -> None:
"""Configure a NARMA generator.
Args:
order (int): Memory order :math:`n` of the recurrence. Defaults to ``10``.
alpha (float): Linear self-feedback coefficient. Defaults to ``0.3``.
beta (float): Nonlinear memory coefficient. Defaults to ``0.05``.
gamma (float): Input-coupling coefficient. Defaults to ``1.5``.
delta (float): Constant bias term. Defaults to ``0.1``.
input_range (tuple[float, float]): Bounds of the driving signal. Defaults to ``(0.0, 0.5)``.
seed (int | None): Seed for the driving signal. Defaults to ``None``.
Raises:
ValueError: If ``order`` is less than 1.
"""
super().__init__(seed=seed)
if order < 1:
raise ValueError(f"order must be a positive integer, got {order}.")
self._order = order
self._alpha = alpha
self._beta = beta
self._gamma = gamma
self._delta = delta
self._input_range = input_range
@property
[docs]
def order(self) -> int:
"""Return the memory order of the recurrence.
Returns:
int: The configured order :math:`n`.
"""
return self._order
[docs]
def generate(self, npoints: int) -> DatasetSample:
"""Generate a NARMA input/output stream.
Args:
npoints (int): Number of time steps to produce.
Returns:
DatasetSample: The DataSetSample containing the inputs and targets.
The output is a ``horizon``-step-ahead prediction task, so both ``inputs`` and
``targets`` are shaped ``(npoints, 1)``.
Raises:
ValueError: If ``npoints`` is not positive.
"""
if npoints < 1:
raise ValueError(f"npoints must be a positive integer, got {npoints}.")
rng = self._rng()
low, high = self._input_range
u = rng.uniform(low, high, size=npoints)
y = np.zeros(npoints, dtype=np.float64)
n = self._order
squash = n > _SQUASH_ORDER_THRESHOLD
for t in range(n - 1, npoints - 1):
memory = float(np.sum(y[t - n + 1 : t + 1]))
update = self._alpha * y[t] + self._beta * y[t] * memory + self._gamma * u[t - n + 1] * u[t] + self._delta
y[t + 1] = np.tanh(update) if squash else update
return DatasetSample(inputs=u.reshape(-1, 1), targets=y.reshape(-1, 1))