DynamicalModes¶
- class kooplearn.DynamicalModes(values: ndarray[complexfloating], right_eigenfunctions: ndarray[complexfloating], left_projections: ndarray[complexfloating])[source]¶
Bases:
objectContainer 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_modesmethods 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 modedt (
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 modedt (
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.inffor 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 eigenvalueeigenvalue_imag: Imaginary part of eigenvalueeigenvalue_magnitude: Magnitude of eigenvalueis_stable: Boolean, True if |λ| < 1is_conjugate_pair: Boolean, True if mode comes from conjugate pair
- Return type:
pandas.DataFrame
Notes
Requires pandas to be installed.