DynamicalModes

class kooplearn.DynamicalModes(values: ndarray[complexfloating], right_eigenfunctions: ndarray[complexfloating], left_projections: ndarray[complexfloating])[source]

Bases: object

Container for dynamical modes from eigenvalue decomposition.

This class stores and manages the modal decomposition of a dynamical system, including eigenvalues, eigenfunctions, and their projections. It automatically handles complex conjugate pairs, sorts modes by stability, and provides convenient access to mode shapes, frequencies, and decay rates.

Warning

The class should be not initialized directly, and will be the return type of .dynamical_modes methods of Kooplearn estimators.

Parameters:
  • values (np.ndarray, shape (rank,)) – 1D array of eigenvalues (complex or real)

  • right_eigenfunctions (np.ndarray, shape (n_points, rank)) – 2D array of right eigenfunctions. Each column is an eigenfunction in the spatial domain.

  • left_projections (np.ndarray, shape (rank, n_features)) – 2D array of left projection vectors. Each row is a projection vector in the feature space.

Variables:

n_modes (int) – Number of modes after filtering complex conjugate pairs

Notes

Complex conjugate pairs are automatically detected and only one from each pair is stored. When reconstructing modes from complex conjugate pairs, the real part is doubled to account for the missing conjugate:

\[\text{mode} = 2 \cdot \text{Re}(\phi_r(x) \langle \phi_l, f \rangle)\]

where \(\phi_r\) is the right eigenfunction and \(\langle \phi_l, f \rangle\) is the left projection on the mode’s observable.

Tip

Modes are sorted by stability: stable modes (\(|\lambda| < 1\)) are ordered by decreasing half-life, followed by unstable modes.

Examples

>>> import numpy as np
>>> from kooplearn.datasets import make_duffing
>>> from kooplearn.kernel import KernelRidge
>>>
>>> # Sample data from the Duffing oscillator
>>> data = make_duffing(X0 = np.array([0, 0]), n_steps=1000)
>>> data = data.to_numpy()
>>>
>>> # Fit the model
>>> model = KernelRidge(n_components=4, kernel='rbf', alpha=1e-6, random_state=42)
>>> model = model.fit(data)
>>>
>>> # Initialize the container
>>> modes = model.dynamical_modes(data)
>>>
>>> # Access individual mode
>>> mode_0 = modes[0]  # Returns (1001, 2) real array
>>> print(f"Mode shape: {mode_0.shape}")
Mode shape: (1001, 2)
>>>
>>> # Iterate over all modes
>>> for idx, mode in enumerate(modes):
...     print(f"Mode {idx}: shape={mode.shape}, frequency={modes.frequency(idx):.3f}")
Mode 0: shape=(1001, 2), frequency=0.000
Mode 1: shape=(1001, 2), frequency=0.003
Mode 2: shape=(1001, 2), frequency=0.000
>>>
>>> # Get summary statistics
>>> summary_df = modes.summary(dt=0.1)
>>> # Filter and analyze stable modes
>>> stable_modes = summary_df[summary_df['is_stable']]
>>> print(f"Number of stable modes: {len(stable_modes)}")
Number of stable modes: 3
>>>
>>> slowest_decay = stable_modes.loc[stable_modes['lifetime'].idxmax()]
>>> print(f"Slowest decay: lifetime={slowest_decay['lifetime']:.1f}s")
Slowest decay: lifetime=69258.2s

Methods

frequency(key: int, dt: float = 1.0) float[source]

Get the oscillation frequency of a mode in physical time units.

Parameters:
  • key (int) – Index of the mode

  • dt (float, optional) – Time step size, by default 1.0. Used to convert from per-timestep to per-unit-time frequencies: \(f_{\text{physical}} = f_{\text{discrete}} / \Delta t\)

Returns:

Frequency in cycles per unit time

Return type:

float

Raises:
  • TypeError – If key is not an integer

  • IndexError – If key is out of range

Notes

The returned frequency is in cycles per unit time (Hz if time is in seconds). For angular frequency (rad/time), multiply by \(2\pi\).

\[\omega = 2\pi f\]
get_eigenvalue(key: int) complex[source]

Get the eigenvalue for a specific mode.

Parameters:

key (int) – Index of the mode

Returns:

The eigenvalue with positive imaginary part for conjugate pairs

Return type:

complex

Raises:
  • TypeError – If key is not an integer

  • IndexError – If key is out of range

get_right_eigenfunction(key: int) ndarray[complexfloating][source]

Get the right eigenfunction associated to a specific mode.

Parameters:

key (int) – Index of the mode

Returns:

complex – The right eigenfunction at index key

Return type:

np.ndarray, shape (n_points,)

Raises:
  • TypeError – If key is not an integer

  • IndexError – If key is out of range

lifetime(key: int, dt: float = 1.0) float[source]

Get the decay time constant (e-folding time) of a mode.

Parameters:
  • key (int) – Index of the mode

  • dt (float, optional) – Time step size, by default 1.0. Used to convert from timesteps to physical time units: \(\tau_{\text{physical}} = \tau_{\text{discrete}} \times \Delta t\)

Returns:

Time constant in physical time units. Returns np.inf for unstable modes.

Return type:

float

Raises:
  • TypeError – If key is not an integer

  • IndexError – If key is out of range

Notes

This returns the e-folding time (time for amplitude to decay by factor e ≈ 2.718). For the actual half-life (time to decay by half), multiply by ln(2) ≈ 0.693:

\[t_{1/2} = \tau \cdot \ln(2)\]
property n_modes: int

Number of modes in the container.

Returns:

Total number of modes after filtering complex conjugate pairs

Return type:

int

summary(dt: float = 1.0)[source]

Generate a summary DataFrame of all mode properties.

Parameters:

dt (float, optional) – Time step size, by default 1.0, for converting to physical units

Returns:

DataFrame with the following columns:

  • frequency : Oscillation frequency (cycles per unit time)

  • lifetime : Decay time constant (time units)

  • eigenvalue_real : Real part of eigenvalue

  • eigenvalue_imag : Imaginary part of eigenvalue

  • eigenvalue_magnitude : Magnitude of eigenvalue

  • is_stable : Boolean, True if |λ| < 1

  • is_conjugate_pair : Boolean, True if mode comes from conjugate pair

Return type:

pandas.DataFrame

Notes

Requires pandas to be installed.