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.
Install, fit a first decomposition, read its summary and its paths.
The model in its equations, the decomposition, forecasting, the spectral state, and what to be careful about.
A synthetic chirp with a known answer, then the 10-year Treasury yield from FRED.
The specification, the result, the spectral state and the schedule records, with their sources.
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,
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.
SpectralState: the decomposition read as a time-local measure
on the frequency axis, with its centroid, dispersion, effective
count, Wasserstein velocity and disentanglement score.
SummaryTable and InformationCriteria, the records every
result renders itself through, as text, HTML or a dataframe.
The categorical option aliases: Alignment, Transition,
Objective, Scaling.
DimensionError, SpecificationError and NumericalError,
one per kind of fix, under a common NSSDNError.
The private _core and _internals layers: validators,
containers, the torch module, the objective, the trainer and the
predictor.
How the library is designed#
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.
Malformed shapes raise DimensionError,
inconsistent specifications
SpecificationError, and non-finite data
or a diverged fit NumericalError, each
with the numbers that triggered it.
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 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}
}