GitHub

Generalized Laplace Analysis

Extract Koopman eigenfunctions directly from trajectory data using weighted time averages.

Overview

Generalized Laplace Analysis (GLA) computes Koopman eigenfunctions directly via weighted time averages, without forming the DMD matrix. Based on the theoretical framework introduced by Mezic (2020), GLA provides a fundamentally different approach to spectral analysis of dynamical systems. Rather than constructing a finite-dimensional approximation of the Koopman operator, GLA iteratively refines eigenvalue estimates and extracts the corresponding eigenfunctions from trajectory data using spectral methods.

This approach is particularly valuable when specific eigenvalues are of interest or when forming the full DMD matrix is computationally expensive. GLA operates directly on observable time series, making it well-suited for streaming data and high-dimensional systems.

Mathematical Formulation

The Koopman operator U acts on observables of a dynamical system. A Koopman eigenfunction phi_lambda associated with eigenvalue lambda satisfies the functional equation:

phi_lambda(T(x)) = lambda * phi_lambda(x)

where T is the dynamical system map. In other words, the eigenfunction transforms multiplicatively under the dynamics. GLA computes this eigenfunction via a weighted time average along trajectories:

phi_lambda(x) = lim_{N -> inf} (1/N) sum_{k=0}^{N-1} lambda^{-k} g(T^k(x))

Here, g is an observable function evaluated along the orbit of x. The weighting by lambda^{-k} isolates the component of the observable that evolves according to the eigenvalue lambda. When the eigenvalue estimate is correct, this sum converges to the projection of g onto the eigenfunction. The iterative refinement procedure adjusts the eigenvalue estimate to maximize convergence.

GlaConfig

The GlaConfig struct controls GLA computation. All parameters have sensible defaults, and the configuration can be customized as needed.

Parameter Type Default Description
eigenvalues Option<Vec<Complex<f64>>> None Optional vector of initial eigenvalue guesses (complex-valued). When set to None, GLA will auto-detect eigenvalues from the data using spectral analysis.
n_eigenvalues usize 4 Number of eigenvalues to find. Used when eigenvalues is None. Ignored if explicit eigenvalue guesses are provided.
tol f64 1e-6 Convergence tolerance for iterative refinement. Smaller values yield more precise eigenvalue estimates at the cost of additional iterations.
max_iter Option<usize> None Maximum number of refinement iterations. When set to None, the iteration count is determined automatically based on the data length and tolerance.

Basic Usage

The following example creates an oscillating two-variable signal, configures GLA to extract two eigenvalues, and inspects the results. For oscillatory data, the eigenvalues should have magnitude near 1.

use koopman_dmd::{GlaConfig, Matrix, gla};
use std::f64::consts::PI;

fn main() {
    // Create an oscillating 2-variable signal
    let n = 500;
    let dt = 0.02;
    let omega = 2.0 * PI * 0.5;
    let mut data = Matrix::zeros(2, n);
    for i in 0..n {
        let t = i as f64 * dt;
        data[(0, i)] = (omega * t).cos();
        data[(1, i)] = (omega * t).sin();
    }

    // Configure GLA with 2 eigenvalues
    let config = GlaConfig {
        eigenvalues: None,
        n_eigenvalues: 2,
        tol: 1e-6,
        max_iter: None,
    };

    // Run GLA
    let result = gla(&data, &config).unwrap();

    // Inspect eigenvalues -- magnitude should be near 1
    for (i, ev) in result.eigenvalues().iter().enumerate() {
        println!("eigenvalue {}: {} (|lambda| = {:.6})",
            i, ev, ev.norm());
    }

    // Check convergence info
    let info = result.convergence_info();
    println!("Converged: {}", info.converged);
    println!("Iterations: {}", info.iterations);
}
import koopman_dmd as kdmd
import numpy as np

# Create an oscillating 2-variable signal
n = 500
dt = 0.02
omega = 2.0 * np.pi * 0.5
t = np.arange(n) * dt
data = np.vstack([np.cos(omega * t), np.sin(omega * t)])

# Configure GLA with 2 eigenvalues
config = kdmd.GlaConfig(
    eigenvalues=None,
    n_eigenvalues=2,
    tol=1e-6,
    max_iter=None,
)

# Run GLA
result = kdmd.gla(data, config)

# Inspect eigenvalues -- magnitude should be near 1
for i, ev in enumerate(result.eigenvalues()):
    print(f"eigenvalue {i}: {ev} (|lambda| = {abs(ev):.6f})")

# Check convergence info
info = result.convergence_info()
print(f"Converged: {info.converged}")
print(f"Iterations: {info.iterations}")
library(koopman.dmd)

# Create an oscillating 2-variable signal
n <- 500
dt <- 0.02
omega <- 2 * pi * 0.5
t <- (0:(n - 1)) * dt
data <- rbind(cos(omega * t), sin(omega * t))

# Configure GLA with 2 eigenvalues
config <- gla_config(
  eigenvalues = NULL,
  n_eigenvalues = 2,
  tol = 1e-6,
  max_iter = NULL
)

# Run GLA
result <- gla(data, config)

# Inspect eigenvalues -- magnitude should be near 1
evs <- result$eigenvalues()
for (i in seq_along(evs)) {
  cat(sprintf("eigenvalue %d: %s (|lambda| = %.6f)\n",
      i, evs[i], Mod(evs[i])))
}

# Check convergence info
info <- result$convergence_info()
cat("Converged:", info$converged, "\n")
cat("Iterations:", info$iterations, "\n")

Prediction and Reconstruction

Once GLA has been fitted, you can use the extracted eigenfunctions and eigenvalues for forecasting with gla_predict and for computing fitted values with gla_reconstruct. These results can be compared against standard DMD to evaluate the quality of the GLA decomposition.

// Forecast 50 steps ahead using GLA
let forecast = gla_predict(&result, 50).unwrap();
println!("Forecast shape: {} x {}",
    forecast.nrows(), forecast.ncols());

// Reconstruct fitted values over the training window
let reconstruction = gla_reconstruct(&result).unwrap();

// Compare with standard DMD
let dmd_result = DMD::new(2).fit(&x, &y).unwrap();
let dmd_recon = dmd_result.reconstruct();

// Compute reconstruction error for both methods
let gla_error = (&reconstruction - &data).norm() / data.norm();
let dmd_error = (&dmd_recon - &data).norm() / data.norm();
println!("GLA reconstruction error: {:.6e}", gla_error);
println!("DMD reconstruction error: {:.6e}", dmd_error);
# Forecast 50 steps ahead using GLA
forecast = kdmd.gla_predict(result, n_steps=50)
print(f"Forecast shape: {forecast.shape}")

# Reconstruct fitted values over the training window
reconstruction = kdmd.gla_reconstruct(result)

# Compare with standard DMD
dmd = kdmd.DMD(rank=2)
dmd_result = dmd.fit(data[:, :-1], data[:, 1:])
dmd_recon = dmd_result.reconstruct()

# Compute reconstruction error for both methods
gla_error = np.linalg.norm(reconstruction - data) / np.linalg.norm(data)
dmd_error = np.linalg.norm(dmd_recon - data) / np.linalg.norm(data)
print(f"GLA reconstruction error: {gla_error:.6e}")
print(f"DMD reconstruction error: {dmd_error:.6e}")
# Forecast 50 steps ahead using GLA
forecast <- gla_predict(result, n_steps = 50)
cat("Forecast dimensions:", dim(forecast), "\n")

# Reconstruct fitted values over the training window
reconstruction <- gla_reconstruct(result)

# Compare with standard DMD
x <- data[, 1:(n - 1)]
y <- data[, 2:n]
dmd_result <- dmd_fit(x, y, rank = 2)
dmd_recon <- dmd_result$reconstruct()

# Compute reconstruction error for both methods
gla_error <- norm(reconstruction - data, "F") / norm(data, "F")
dmd_error <- norm(dmd_recon - data, "F") / norm(data, "F")
cat(sprintf("GLA reconstruction error: %.6e\n", gla_error))
cat(sprintf("DMD reconstruction error: %.6e\n", dmd_error))

When to Use GLA vs DMD

GLA and standard DMD approach the same underlying problem from different angles. The right choice depends on your specific use case and computational constraints.

GLA is better for

DMD is better for

Convergence

GLA uses an iterative refinement procedure to improve eigenvalue estimates. At each iteration, the weighted time average is computed with the current eigenvalue guess, and the eigenvalue is updated based on the resulting eigenfunction estimate. The process continues until the change between successive eigenvalue estimates falls below the specified tolerance tol, or until the maximum number of iterations max_iter is reached.

The tol parameter controls the precision of the final eigenvalue estimates. A tolerance of 1e-6 (the default) is suitable for most applications. For high-precision work, values of 1e-10 or smaller can be used, though this will increase computation time. The convergence rate depends on the spectral gap between eigenvalues: well-separated eigenvalues converge quickly, while closely spaced eigenvalues may require more iterations.

Note on chaotic systems: Convergence may not be achieved for chaotic dynamical systems. In such cases, the continuous spectrum of the Koopman operator means that point eigenvalues may not exist, and the iterative procedure may fail to converge regardless of the number of iterations. Monitor the convergence_info output and consider increasing max_iter or adjusting tol if convergence is not reached. For chaotic systems, standard DMD or Hankel-DMD may be more appropriate.