GitHub

Core DMD

Standard Dynamic Mode Decomposition for linear dynamical systems

Mathematical Background

Dynamic Mode Decomposition (DMD) extracts spatiotemporal coherent structures from time-series data by approximating the best-fit linear operator that advances the state forward in time. Given a sequence of snapshots arranged as columns of a data matrix X, the algorithm proceeds as follows.

Step 1 -- Split the data

Partition X into two overlapping matrices: X' containing the first n-1 columns and X'' containing the last n-1 columns. The relationship we seek is:

X'' = A X'

Step 2 -- Compute the SVD

Take the (optionally truncated) singular value decomposition of X':

X' = U Σ V*

where U is the left singular matrix, Σ is the diagonal matrix of singular values, and V* is the conjugate transpose of the right singular matrix.

Step 3 -- Project onto the POD basis

Rather than constructing the full high-dimensional operator A, project it onto the reduced basis spanned by the columns of U:

à = U* X'' V Σ-1

This small r x r matrix captures the essential dynamics, where r is the truncation rank.

Step 4 -- Eigendecomposition

Compute the eigendecomposition of the projected matrix:

à W = W Λ

The DMD eigenvalues λi lie on the diagonal of Λ. The high-dimensional DMD modes are recovered as:

Φ = X'' V Σ-1 W

Step 5 -- Compute amplitudes

Solve for the amplitude vector b that best matches the initial snapshot x0:

Φ b = x0

The amplitudes weight the contribution of each mode to the overall dynamics.

DmdConfig Options

The DmdConfig struct controls how the decomposition is performed. The following fields are available:

Field Type Default Description
rank Option<usize> None Truncation rank for the SVD. None selects rank automatically via a singular-value threshold.
center bool false Subtract the temporal mean from each row before decomposition. The mean is stored and added back during reconstruction.
lifting Option<LiftingConfig> None Optional nonlinear lifting (see Extended DMD).
use koopman_dmd::{DmdConfig};

// Auto-rank, no centering, no lifting
let config = DmdConfig::default();

// Explicit rank with centering
let config = DmdConfig {
    rank: Some(4),
    center: true,
    lifting: None,
};
from koopman_dmd import DmdConfig

# Auto-rank, no centering, no lifting
config = DmdConfig()

# Explicit rank with centering
config = DmdConfig(rank=4, center=True)
library(koopmandmd)

# Auto-rank, no centering, no lifting
config <- dmd_config()

# Explicit rank with centering
config <- dmd_config(rank = 4, center = TRUE)

Basic Usage

The core workflow is: build a configuration, pass your data matrix, and call the fit function. The result contains eigenvalues, modes, amplitudes, and metadata about the decomposition.

use koopman_dmd::{DmdConfig, dmd_fit};
use nalgebra as na;

// Build a 3x100 data matrix (3 state variables, 100 snapshots)
let data: Mat<f64> = generate_signal(3, 100);

let config = DmdConfig::default();
let result = dmd_fit(&data, &config);

// Inspect the result
println!("Rank:        {}", result.rank);
println!("Data dim:    {}", result.data_dim);
println!("Eigenvalues: {:?}", result.eigenvalues);
println!("Modes shape: {:?}", result.modes.shape());
println!("Amplitudes:  {:?}", result.amplitudes);
import numpy as np
from koopman_dmd import DmdConfig, dmd_fit

# Build a 3x100 data matrix (3 state variables, 100 snapshots)
data = generate_signal(3, 100)

config = DmdConfig()
result = dmd_fit(data, config)

# Inspect the result
print("Rank:       ", result.rank)
print("Data dim:   ", result.data_dim)
print("Eigenvalues:", result.eigenvalues)
print("Modes shape:", result.modes.shape)
print("Amplitudes: ", result.amplitudes)
library(koopmandmd)

# Build a 3x100 data matrix (3 state variables, 100 snapshots)
data <- generate_signal(3, 100)

config <- dmd_config()
result <- dmd_fit(data, config)

# Inspect the result
cat("Rank:       ", result$rank, "\n")
cat("Data dim:   ", result$data_dim, "\n")
print(result$eigenvalues)
cat("Modes dim:  ", dim(result$modes), "\n")
print(result$amplitudes)

Mean Centering

Setting center = true subtracts the temporal mean of each row before the decomposition and stores it for later reconstruction. This is useful when your data has a non-zero equilibrium or steady state.

When to center. If your system oscillates around a non-zero baseline (e.g., temperature anomalies relative to a seasonal mean), centering removes the constant offset and lets DMD focus on the dynamic components. Without centering, the largest DMD mode may simply represent the mean, consuming one degree of freedom in your rank budget.

Effects on eigenvalues:

Caution. Do not center data from systems that genuinely have a growing or shrinking mean trajectory. Centering assumes the mean is static; applying it to non-stationary means can distort the reconstructed dynamics.

Rank Selection

Automatic rank

When rank is None, the library selects rank automatically by examining the singular values of X'. Singular values below a threshold (typically relative to the largest singular value) are discarded. This is a good default for exploratory analysis.

Manual rank

Setting an explicit rank lets you control the trade-off between reconstruction accuracy and model complexity. A lower rank produces a smoother, more interpretable model but may miss fine-grained dynamics. A higher rank captures more detail but can overfit to noise.

Guidelines for choosing rank:

Prediction

The library provides two prediction strategies. Both extrapolate the fitted model forward (or backward) in time, but they differ in how they compute subsequent states.

predict_modes

Reconstruct each time step directly from the DMD eigenvalues, modes, and amplitudes:

x(k) = ∑i φi bi λik

This method is non-recursive: every time step is computed independently from the spectral decomposition. It does not accumulate numerical error over long horizons, making it well-suited for long-range forecasts.

predict_matrix

Advance the state one step at a time by multiplying by the reconstructed operator:

x(k+1) = A x(k)

This is a recursive approach: each step depends on the previous one. It can be faster for short horizons and naturally respects the linear map structure, but numerical errors may accumulate over many steps.

When to use each

use koopman_dmd::{dmd_fit, predict_modes, predict_matrix, DmdConfig};

let config = DmdConfig::default();
let result = dmd_fit(&data, &config);

// Predict 50 future time steps via spectral formula
let forecast_modes = predict_modes(&result, 50);

// Predict 50 future time steps via recursive matrix multiply
let forecast_matrix = predict_matrix(&result, 50);
from koopman_dmd import dmd_fit, predict_modes, predict_matrix, DmdConfig

config = DmdConfig()
result = dmd_fit(data, config)

# Predict 50 future time steps via spectral formula
forecast_modes = predict_modes(result, 50)

# Predict 50 future time steps via recursive matrix multiply
forecast_matrix = predict_matrix(result, 50)
library(koopmandmd)

config <- dmd_config()
result <- dmd_fit(data, config)

# Predict 50 future time steps via spectral formula
forecast_modes <- predict_modes(result, 50)

# Predict 50 future time steps via recursive matrix multiply
forecast_matrix <- predict_matrix(result, 50)

Reconstruction and Error

After fitting, you typically want to assess how well the DMD model reproduces the original data. The library provides two utilities for this purpose.

dmd_reconstruct

Produces the fitted values by reconstructing each snapshot from the DMD modes, eigenvalues, and amplitudes. If centering was used, the stored mean is added back automatically. The result has the same dimensions as the original data matrix.

dmd_error

Computes error metrics comparing the reconstruction to the original data. Returns:

use koopman_dmd::{dmd_fit, dmd_reconstruct, dmd_error, DmdConfig};

let config = DmdConfig { rank: Some(4), ..DmdConfig::default() };
let result = dmd_fit(&data, &config);

// Reconstruct the fitted values
let fitted = dmd_reconstruct(&result);

// Compute error metrics
let err = dmd_error(&data, &result);
println!("RMSE:           {:.6}", err.rmse);
println!("Relative error: {:.6}", err.relative);
from koopman_dmd import dmd_fit, dmd_reconstruct, dmd_error, DmdConfig

config = DmdConfig(rank=4)
result = dmd_fit(data, config)

# Reconstruct the fitted values
fitted = dmd_reconstruct(result)

# Compute error metrics
err = dmd_error(data, result)
print(f"RMSE:           {err.rmse:.6f}")
print(f"Relative error: {err.relative:.6f}")
library(koopmandmd)

config <- dmd_config(rank = 4)
result <- dmd_fit(data, config)

# Reconstruct the fitted values
fitted <- dmd_reconstruct(result)

# Compute error metrics
err <- dmd_error(data, result)
cat("RMSE:          ", err$rmse, "\n")
cat("Relative error:", err$relative, "\n")