ns-sdn#

A neural state-space model that decomposes a non-stationary series into a trend and a few sinusoids whose amplitude, frequency and participation move with a latent state. Fit it to one series, read the decomposition back as arrays, forecast from it, and summarize the whole spectral picture in a handful of coordinates. One model, one fit(), one frozen result, in the same shape as cultivars.

Getting started

Install, fit a first decomposition, read its summary and its paths.

Getting started
User guide

The model in its equations, the decomposition, forecasting, the spectral state, and what to be careful about.

User guide
Examples

A synthetic chirp with a known answer, then the 10-year Treasury yield from FRED.

Examples
API reference

The specification, the result, the spectral state and the schedule records, with their sources.

API reference

Release status#

ns-sdn 1.0.0 is a stable release

API: frozen for 1.x numerics: match the published run

The model-fit-result pattern, the names NSSDN, NSSDNResult, SpectralState, Stage and Penalties, their argument names, result fields and summary wording are the 1.x contract under semantic versioning. A change that alters the output of the default configuration on the published data is a major version; every change to a public surface is listed in the release notes. The default configuration reproduces the IEEE-ORSS DGS10 run to the parameter count. Pin the exact version in anything that must reproduce:

pip install "ns-sdn==1.0.0"

Install#

Python 3.12 or newer. The core depends on numpy and torch; pandas is optional and switches on to_pandas() on every result, and fedfred is optional and fetches the FRED series the examples use.

pip install ns-sdn
pip install "ns-sdn[pandas,data]"     # dataframes and FRED access
uv add ns-sdn
uv add "ns-sdn[pandas,data]"
git clone https://github.com/nikhilxsunder/ns-sdn.git
cd ns-sdn
uv sync --group dev

On Linux, PyPI’s torch wheel pulls the CUDA stack; the installation page shows the CPU-only install, which is all the model needs.

In sixty seconds#

A series with two oscillations and a drift, a three-component model with a short schedule, and the three things every result gives you: the decomposition, a forecast, and a summary that says what it assumes. The example is synthetic so it runs without a network connection; the examples do the same on FRED data.

import numpy as np
from ns_sdn.spectral_decomposition import NSSDN, Stage

t = np.arange(300.0)
y = 2.0 + 0.003 * t + np.sin(0.12 * t) + 0.4 * np.sin(0.55 * t)
y += 0.05 * np.random.default_rng(0).standard_normal(300)

res = NSSDN(y, k=3, state_dim=16, schedule=(Stage(1e-2, 80),), seed=0).fit()

print(res.trend.shape, res.components.shape, res.frequency.shape)   # the decomposition
print(np.allclose(res.trend + res.components.sum(axis=1), res.emission, atol=1e-5))
print(res.forecast(5).shape)                                        # a forecast
print(res.summary().title, res.nobs, res.n_params)                  # a summary
(300,) (300, 3) (300, 3)
True
(5,)
NSSDN(k=3) Results 299 1155

print(res) renders the summary table: one row per component with its mean amplitude, frequency, period, gate, mixing weight and share of power, the fit statistics above it, and under it the notes that qualify the numbers. A result is a frozen dataclass, so everything in the table is also an attribute, and to_pandas() returns every path as a column when pandas is installed.

The model#

At each step the network reads a latent state and emits a trend plus \(K\) sinusoids,

\[\hat y_t = B_t + \sum_{k=1}^{K} w_k\, g_{t,k}\, A_{t,k}\, \sin\!\bigl(\theta_{t,k} + \phi_{t,k}\bigr), \qquad \theta_{t,k} = \theta_{t-1,k} + \omega_{t,k}\,\Delta t, \qquad h_t = F_\psi(h_{t-1}, y_t),\]

where \(B_t\) is the trend, \(A_{t,k}\) the amplitude, \(\omega_{t,k}\) the instantaneous frequency, \(\phi_{t,k}\) the phase offset and \(g_{t,k}\) the gate of component \(k\), each a linear head on the state \(h_t\) of a GRU, and \(w\) a learned mixing vector. The phase integrates the frequency, so each component is a chirp whose frequency the state can move. Everything to the right of the first equals sign is returned to you as an array; the user guide derives the model and the decomposition page says how to read each path.

What is inside#

Five public modules. The top-level ns_sdn namespace holds modules only; every public name is imported from the module that defines it, and the API reference is organised the same way.

NSSDN, the specification; NSSDNResult, the fitted record with every path of the decomposition, one-step and recursive forecasts; Stage and Penalties, the training schedule.

ns_sdn.spectral_decomposition

SpectralState: the decomposition read as a time-local measure on the frequency axis, with its centroid, dispersion, effective count, Wasserstein velocity and disentanglement score.

ns_sdn.spectral_state

SummaryTable and InformationCriteria, the records every result renders itself through, as text, HTML or a dataframe.

ns_sdn.summary

The categorical option aliases: Alignment, Transition, Objective, Scaling.

ns_sdn.typing

DimensionError, SpecificationError and NumericalError, one per kind of fix, under a common NSSDNError.

ns_sdn.exceptions
Engine

The private _core and _internals layers: validators, containers, the torch module, the objective, the trainer and the predictor.

Engine reference

How the library is designed#

One pattern

A model is a validated specification; fit() returns a frozen result. Results carry integer positions and leave your index alone; inputs are anything numpy.asarray() accepts. The pattern is cultivars’s, so a workflow written against one library reads the other.

Refusals, not silent failure

Malformed shapes raise DimensionError, inconsistent specifications SpecificationError, and non-finite data or a diverged fit NumericalError, each with the numbers that triggered it.

Summaries that argue

Every summary says which alignment it was fitted under and what that makes of its one-step numbers, that its likelihood is concentrated at a penalized fit, and that components without an ordering constraint are identified only up to permutation.

The decomposition is the product

The result carries every path the paper’s figures are drawn from, on the original scale by default and in training units on request, and a SpectralState that reduces them to coordinates a regime-change claim can be tested against.

Citing#

If ns-sdn contributes to published work, cite the IEEE-ORSS paper the package implements [Sunder and Leon, 2026], the FLAIRS paper that introduced the architecture [Sunder, 2026], and the software; the citing page has the entries.

@software{sunder_nssdn,
  author    = {Sunder, Nikhil},
  title     = {ns-sdn: the Non-Stationary Spectral Decomposition Network},
  year      = {2026},
  publisher = {Zenodo},
  doi       = {10.5281/zenodo.23368625},
  url       = {nikhilxsunder/ns-sdn},
  license   = {MIT}
}