What is a Linear Dynamical System?
A Linear Dynamical System (LDS) is a mathematical model used to describe how a system evolves over time. These systems are a subset of state-space models, where the hidden state dynamics are continuous. What makes these models linear is that the latent dynamics evolve according to a linear function of the previous state. The observations, however, can be related to the hidden state through a nonlinear link function.
At its core, an LDS defines:
- A state transition function: how the internal state evolves from one time step to the next.
- An observation function: how the internal state generates the observed data.
StateSpaceDynamics.LinearDynamicalSystem — Type
LinearDynamicalSystem{T<:Real, S<:AbstractStateModel{T}, O<:AbstractObservationModel{T}}Represents a unified Linear Dynamical System with customizable state and observation models.
Fields
state_model::S: The state model (e.g., GaussianStateModel)obs_model::O: The observation model (e.g., GaussianObservationModel or PoissonObservationModel)latent_dim::Int: Dimension of the latent stateobs_dim::Int: Dimension of the observationsfit_bool::Vector{Bool}: Vector indicating which parameters to fit during optimization. Length 6 for the Gaussian path ([x0, P0, A&b&B, Q, C&d&D, R]); the M-step regression fits each row jointly. Length 5 for the Poisson path ([x0, P0, A&b, Q, C&d]).
The Gaussian Linear Dynamical System
The Gaussian Linear Dynamical System — typically just referred to as an LDS — is a specific type of linear dynamical system where both the state transition and observation functions are linear, and all noise is Gaussian.
The generative model is given by:
\[\begin{aligned} x_t &\sim \mathcal{N}(A x_{t-1} + B u_t + b, Q) \\ y_t &\sim \mathcal{N}(C x_t + D v_t + d, R) \end{aligned}\]
Where:
- $x_t$ is the hidden state at time $t$
- $y_t$ is the observed data at time $t$
- $u_t$ is the dynamics input at time $t$
- $v_t$ is the observation input at time $t$
- $A$ is the state transition matrix
- $C$ is the observation matrix
- $Q$ is the process noise covariance
- $R$ is the observation noise covariance
- $B$ and $D$ are input matrices
- $b$ and $d$ are bias terms
This can equivalently be written in equation form:
\[\begin{aligned} x_t &= A x_{t-1} + b + \epsilon_t \\ y_t &= C x_t + d + \eta_t \end{aligned}\]
Where:
- $\epsilon_t \sim \mathcal{N}(0, Q)$ is the process noise
- $\eta_t \sim \mathcal{N}(0, R)$ is the observation noise
StateSpaceDynamics.GaussianStateModel — Type
GaussianStateModel{T<:Real, M<:AbstractMatrix{T}, V<:AbstractVector{T}}Represents the state model of a Linear Dynamical System with Gaussian noise.
State evolution:
\[x_1 ~ N(x_0, P_0) x_{t+1} | x_t ~ N(A x_t + b + B ux_t, Q)\]
where B·ux_t is present only when B is supplied (i.e., has nonzero columns).
Fields
A::M: Transition matrix (sizelatent_dim × latent_dim).Q::M: Process noise covariance matrix.b::V: Bias vector (lengthlatent_dim).x0::V: Initial state mean (lengthlatent_dim).P0::M: Initial state covariance (sizelatent_dim × latent_dim).B::M: Optional dynamics input matrix (latent_dim × ux_dim). When supplied, inputsuxmust be passed tofit!/smooth!via a keyword argument.Q_prior::Union{Nothing,IWPrior{T}} = nothing: Optional Inverse-Wishart prior onQ. If set, MAP updates use its mode.P0_prior::Union{Nothing,IWPrior{T}} = nothing: Optional Inverse-Wishart prior onP0. If set, MAP updates use its mode.AB_prior::Union{Nothing,MNPrior{T,Matrix{T}}} = nothing: Optional matrix-normal prior on the stacked dynamics matrix[A B]. Pair withQ_priorfor a full MNIW prior on(AB, Q). Prior matrices are stored as plainMatrix{T}(decoupled fromA's storage typeM) so they match the internalKalmanWorkspaceregardless of howAis stored.
StateSpaceDynamics.GaussianObservationModel — Type
GaussianObservationModel{T<:Real, M<:AbstractMatrix{T}, V<:AbstractVector{T}}Represents the observation model of a Linear Dynamical System with Gaussian noise.
Fields
C::M: Observation matrix of size(obs_dim × latent_dim). Maps latent states into observation space.R::M: Observation noise covariance of size(obs_dim × obs_dim).d::V: Bias vector of length(obs_dim).R_prior::Union{Nothing, IWPrior{T}} = nothing: Optional Inverse-Wishart prior forR.CD_prior::Union{Nothing,MNPrior{T,Matrix{T}}} = nothing: Optional matrix-normal prior on the stacked emission matrix[C D]. Pair withR_priorfor a full MNIW prior on(CD, R). Prior matrices are stored as plainMatrix{T}(decoupled fromC's storage typeM) so they match the internalKalmanWorkspaceregardless of howCis stored.
The Poisson Linear Dynamical System
The Poisson Linear Dynamical System is a variant of the LDS where the observations are modeled as counts. This is useful in fields like neuroscience where we are often interested in modeling spike count data. To relate the spiking data to the Gaussian latent variable, we use a nonlinear link function, specifically the exponential function.
The generative model is given by:
\[\begin{aligned} x_t &\sim \mathcal{N}(A x_{t-1} + b, Q) \\ y_t &\sim \text{Poisson}(\exp(C x_t + d)) \end{aligned}\]
Where $d$ is a bias term.
StateSpaceDynamics.PoissonObservationModel — Type
PoissonObservationModel{
T<:Real,
M<:AbstractMatrix{T},
V<:AbstractVector{T}
} <: AbstractObservationModel{T}Represents the observation model of a Linear Dynamical System with Poisson observations, with canonical log-link:
\[λ_t = exp(C x_t + d)\]
d is the standard Poisson-GLM intercept — the per-channel baseline log-rate, unconstrained in ℝ; positivity of the rate λ is provided by the exp.
Fields
C::AbstractMatrix{T}: Observation matrix of size(obs_dim × latent_dim). Maps latent states into observation space.d::AbstractVector{T}: Per-neuron baseline log-rate (lengthobs_dim). Free in ℝ.CD_prior::Union{Nothing,MNPrior{T,Matrix{T}}} = nothing: Optional matrix-normal prior on the stacked emission matrix[C d](treated as a single regression oflog λon[x; 1]). Prior matrices are stored as plainMatrix{T}, decoupled fromC's storage typeM.M₀andΛhave shapes(obs_dim, latent_dim+1)and(latent_dim+1, latent_dim+1)respectively. Unlike the Gaussian path there is no IW counterpart since Poisson has no observation-noise covariance — this is an MN-only prior contributing½ tr(([C d] - M₀) Λ ([C d] - M₀)')to the LBFGS objective.
Sampling from Linear Dynamical Systems
You can generate synthetic data from fitted LDS models. Pass a scalar tsteps::Integer for a single trial, or a vector of per-trial lengths to sample a multi-trial dataset (trial lengths may differ):
Base.rand — Method
Random.rand([rng,] lds, tsteps::Integer; latent_inputs=nothing, obs_inputs=nothing)
Random.rand([rng,] lds, tsteps_per_trial::AbstractVector{<:Integer};
latent_inputs=nothing, obs_inputs=nothing)Sample from a Linear Dynamical System.
- With a scalar
tsteps, returns one trial as(x::Matrix, y::Matrix)of sizes(latent_dim, tsteps)and(obs_dim, tsteps)respectively. - With a vector of per-trial lengths, returns
(x::Vector{Matrix}, y::Vector{Matrix}). Lengths may differ across trials.
Optional input sequences:
latent_inputs: dynamics-input sequence consumed byB. Single-trial form is an(ux_dim, tsteps)matrix; multi-trial is aVector{<:AbstractMatrix}of per-trial matrices. Required whensize(state_model.B, 2) > 0.obs_inputs: same shape for the observation inputD. Required whensize(obs_model.D, 2) > 0. Gaussian observation model only.
Inference in Linear Dynamical Systems
In StateSpaceDynamics.jl, we directly maximize the complete-data log-likelihood function with respect to the latent states given the data and the parameters of the model. In other words, the maximum a posteriori (MAP) estimate of the latent state path is:
\[\underset{x}{\text{argmax}} \left[ \log p(x_1) + \sum_{t=2}^T \log p(x_t \mid x_{t-1}) + \sum_{t=1}^T \log p(y_t \mid x_t) \right]\]
This MAP estimation approach has the same computational complexity as traditional Kalman filtering and smoothing — $\mathcal{O}(T)$ — but is significantly more flexible. Notably, it can handle nonlinear observations and non-Gaussian noise while still yielding exact MAP estimates, unlike approximate techniques such as the Extended Kalman Filter (EKF) or Unscented Kalman Filter (UKF).
Newton's Method for Latent State Optimization
To find the MAP trajectory, we iteratively optimize the latent states using Newton's method. The update equation at each iteration is:
\[x^{(i+1)} = x^{(i)} - \left[ \nabla^2 \mathcal{L}(x^{(i)}) \right]^{-1} \nabla \mathcal{L}(x^{(i)})\]
Where:
- $\mathcal{L}(x)$ is the complete-data log-likelihood:
\[\mathcal{L}(x) = \log p(x_1) + \sum_{t=2}^T \log p(x_t \mid x_{t-1}) + \sum_{t=1}^T \log p(y_t \mid x_t)\]
- $\nabla \mathcal{L}(x)$ is the gradient of the full log-likelihood with respect to all latent states
- $\nabla^2 \mathcal{L}(x)$ is the Hessian of the full log-likelihood
This update is performed over the entire latent state sequence $x_{1:T}$, and repeated until convergence.
For Gaussian models, $\mathcal{L}(x)$ is quadratic and Newton's method converges in a single step — recovering the exact Kalman smoother solution. For non-Gaussian models, the Hessian is not constant and the optimization is more complex. However, the MAP estimate can still be computed efficiently using the same approach as the optimization problem is still convex.
StateSpaceDynamics.smooth — Function
smooth(lds, y::AbstractMatrix)Direct smoothing for a single trial.
Arguments
lds::LinearDynamicalSystem: The model.y::AbstractMatrix: Observations (obs_dim × tsteps).
Returns
x_smooth::AbstractMatrix: Smoothed latent means (latent_dim × tsteps).p_smooth::Array{T,3}: Smoothed latent covariances (latentdim × latentdim × tsteps).
Laplace Approximation of Posterior for Non-Conjugate Observation Models
In the case of non-Gaussian observations, we can use a Laplace approximation to compute the posterior distribution of the latent states. For Gaussian observations (which are conjugate with the Gaussian state model), the posterior is also Gaussian and is the exact posterior. However, for non-Gaussian observations, we can approximate the posterior using a Gaussian distribution centered at the MAP estimate of the latent states. This approximation is given by:
\[p(x \mid y) \approx \mathcal{N}(x^{*}, -\left[ \nabla^2 \mathcal{L}(x^{*}) \right]^{-1})\]
Where:
- $x^{*}$ is the MAP estimate of the latent states
- $\nabla^2 \mathcal{L}(x^{*})$ is the Hessian of the log-likelihood at the MAP estimate
Despite the requirement of inverting a Hessian of dimension $(d \times T) \times (d \times T)$, this is still computationally efficient, as the Markov structure of the model renders the Hessian block-tridiagonal, and thus the inversion is tractable.
Learning in Linear Dynamical Systems
Given the latent structure of state-space models, we must rely on either the Expectation-Maximization (EM) or Variational Inference (VI) approaches to learn the parameters of the model. StateSpaceDynamics.jl supports both EM and VI. For LDS models, we can use Laplace EM, where we approximate the posterior of the latent state path using the Laplace approximation as outlined above. Using these approximate posteriors (or exact ones in the Gaussian case), we can apply closed-form updates for the model parameters.
LDS parameters are not uniquely identifiable. For any invertible matrix $S$, the reparameterization
\[\begin{aligned} x'_t &= S x_t,\\ A' &= S A S^{-1},\\ C' &= C S^{-1},\\ Q' &= S Q S^\top,\\ R' &= R \end{aligned}\]
yields the same likelihood. Practical consequences:
- Scale/rotation ambiguity: the latent space can be arbitrarily scaled/rotated.
- Sign & permutation flips: columns of $C$ (and corresponding rows/cols of $A$) can swap or flip signs with no change in fit.
Common remedies
- Fix a convention for the latent scale, e.g. set $Q = I$ or constrain $\mathrm{diag}(Q)=1$.
- Encourage a canonical orientation, e.g. enforce orthonormal columns in $C$ (up to sign) after each M-step. (Not yet implemented)
- When comparing fits across runs, align parameters via a Procrustes or Hungarian matching step.
These issues affect parameter interpretability but not predictive performance; be cautious when interpreting individual entries of $A$, $C$, or $Q$.
StatsAPI.fit! — Method
fit!(lds, y; max_iter=100, tol=1e-6, progress=true,
latent_inputs=nothing, obs_inputs=nothing)Fit a Gaussian Linear Dynamical System via Expectation-Maximization.
Arguments
lds::LinearDynamicalSystem{T,S,O}: model to fit in placey: observations. Two shapes accepted:AbstractMatrix{T}of size(obs_dim, T)— single trialAbstractVector{<:AbstractMatrix{T}}— multi-trial, each(obs_dim, T_i), trial lengths may differ
Keywords
max_iter::Int=100: maximum EM iterationstol::Float64=1e-6: convergence tolerance on ELBO changeprogress::Bool=true: show progress barlatent_inputs: optional dynamics-input sequence.Vector{<:AbstractMatrix}for multi-trial (each(ux_dim, T_i)); required whensize(state_model.B, 2) > 0.obs_inputs: optional observation-input sequence (same shape) for the obs-side input matrixD. Required whensize(obs_model.D, 2) > 0.
Returns a Vector{T} of ELBO values, one per iteration.
Inverse-Wishart Priors on Covariances (MAP)
To encourage well-conditioned covariance estimates, StateSpaceDynamics.jl supports Inverse-Wishart (IW) priors on the covariance matrices of a Linear Dynamical System. These can be used to impose shrinkage toward a target scale, yielding maximum a posteriori (MAP) estimates instead of pure MLEs.
You can attach an IW prior to:
- The process noise covariance
Q_prior - The initial state covariance
P0_prior - The observation noise covariance
R_prior(Gaussian models only)
StateSpaceDynamics.IWPrior — Type
IWPrior{T<:Real, M<:AbstractMatrix}Inverse-Wishart prior for a covariance matrix Σ ~ IW(Ψ, ν), with density p(Σ) ∝ |Σ|^{-(ν + d + 1)/2} exp(-½ tr(Ψ Σ^{-1})) for d = size(Σ,1).
Fields
Ψ::M: Scale matrix (d×d, SPD).ν::T: Degrees of freedom (must satisfyν > d + 1for a proper mode).
Notes
- The MAP update for a posterior IW(Ψ + S, ν + n) is
(Ψ + S) / (ν + n + d + 1).
Definition
For a covariance matrix $\Sigma \in \mathbb{R}^{d\times d}$,
\[\Sigma \sim \text{IW}(\Psi, \nu), \qquad p(\Sigma)\propto |\Sigma|^{-(\nu+d+1)/2}\exp\!\Big[-\tfrac12\operatorname{tr}(\Psi\,\Sigma^{-1})\Big].\]
- $\Psi$ is the scale matrix (positive-definite).
- $\nu$ is the degrees of freedom. A proper mode exists when $\nu > d+1$.
Given $n$ effective samples and sample statistic $S$, the posterior is
\[\Sigma\mid\text{data}\sim\text{IW}(\Psi+S,\;\nu+n)\]
and the MAP (mode) update used internally is
\[\widehat{\Sigma}_{\text{MAP}} =\frac{\Psi+S}{\nu+n+d+1}.\]
Example: Adding IW Priors to an LDS
using LinearAlgebra, Random, StateSpaceDynamics
rng = MersenneTwister(42)
D, P = 3, 4
# Proper SPD scale matrices (avoid UniformScaling)
ΨQ = diagm(0 => fill(0.01, D))
ΨP0 = diagm(0 => fill(0.01, D))
ΨR = diagm(0 => fill(0.01, P))
Qprior = IWPrior(Ψ = ΨQ, ν = D + 3.0)
P0prior = IWPrior(Ψ = ΨP0, ν = D + 3.0)
Rprior = IWPrior(Ψ = ΨR, ν = P + 3.0)
# Gaussian LDS
A = 0.9I + 0.05randn(rng, D, D)
Q = Matrix(I, D, D) .* 0.3
b = zeros(D); x0 = zeros(D); P0 = Matrix(I, D, D) .* 0.8
C = randn(rng, P, D)
R = Matrix(I, P, P) .* 0.25
d = 0.1 .* randn(rng, P)
gsm = GaussianStateModel(A=A, Q=Q, b=b, x0=x0, P0=P0,
Q_prior=Qprior, P0_prior=P0prior)
gom = GaussianObservationModel(C=C, R=R, d=d, R_prior=Rprior)
lds = LinearDynamicalSystem(gsm, gom)
X, Y = rand(rng, lds, fill(100, 5)) # 5 trials of 100 timesteps each
fit!(lds, Y; max_iter=20, progress=false)Effect on Learning
- When a prior is present, the M-step uses the MAP mode formula above.
- The reported ELBO includes IW log-prior terms (up to constants), so convergence still tracks the true MAP objective.
- With small datasets or high-dimensional states, the priors keep $Q$, $P_0$, and $R$ positive-definite and well-conditioned.
Choosing $\Psi$ and $\nu$
| Goal | Typical choice | Notes |
|---|---|---|
| Mild shrinkage | $\Psi = 0.01I$, $\nu = d+3$ | Gentle pull toward small covariances |
| Strong regularization | Larger $\nu$ | Acts like adding pseudo-samples |
| Data-scaled prior | $\Psi$ ≈ empirical covariance | Centers shrinkage around realistic scale |
Tip: In Julia,
Iis aUniformScaling(not a matrix). UseMatrix(I, d, d)ordiagm(0 => fill(..., d))to build $Ψ$.
Matrix-Normal Priors on Regression Coefficients (MAP)
The M-step updates for the dynamics and emission parameters are (multivariate) linear regressions: the dynamics update fits the stacked coefficient matrix $[A\; b]$ and the emission update fits $[C\; d]$. StateSpaceDynamics.jl supports a matrix-normal (MN) prior on these coefficient matrices, turning each regression into a MAP (ridge-style) update:
\[W_{\text{MAP}} = (XY^\top + M_0 \Lambda)(XX + \Lambda)^{-1}\]
where $M_0$ is the prior mean and $\Lambda$ the column precision. Setting $M_0 = 0$ recovers ordinary ridge regression; $\Lambda = 0$ recovers the unpenalized least-squares update.
You can attach an MN prior to:
- The stacked dynamics matrix via
AB_prioron aGaussianStateModel - The stacked emission matrix via
CD_prioron aGaussianObservationModelorPoissonObservationModel
The MN prior is kept separate from the covariance priors on purpose: pairing an AB_prior/CD_prior with the matching Q_prior/R_prior (IWPrior) recovers a full matrix-normal-inverse-Wishart (MNIW) prior, but each half can also be used on its own — e.g. the Poisson observation model has no noise covariance, so only the MN half applies there.
StateSpaceDynamics.MNPrior — Type
MNPrior{T<:Real, M<:AbstractMatrix}Matrix-normal prior on a regression coefficient matrix W (size k × p) appearing in a linear regression Y = W X + ε with row-noise covariance Σ:
\[W | Σ ~ MN(M₀, Σ ⊗ Λ⁻¹)\]
equivalently vec(W) ~ N(vec(M₀), Λ⁻¹ ⊗ Σ). Σ is the regression's row covariance (paired with IWPrior when both halves of an MNIW prior are desired); Λ is the column precision and is the only piece that enters the MAP update for W.
Fields
M₀::M: prior mean (k × p, same shape as W). Use a zero matrix for plain ridge; an identity-like matrix for shrinkage toward a random walk onA.Λ::M: column precision (p × p, SPD).
Notes
- The MAP update for W given the regression sufficient statistics
XX = X XᵀandXY = X Yᵀismath W = (XYᵀ + M₀ Λ) (XX + Λ)⁻¹Reduces to OLS whenΛ = 0, and to ordinary ridge regression whenM₀ = 0. - Mathematically half of an MNIW prior; combine with an
IWPrioron the same regression to recover the full conjugate prior on(W, Σ).