Python API Reference
Complete reference for the koopman_dmd Python module (PyO3 bindings)
Installation
The Python bindings are built with PyO3 and packaged using maturin. To build and install from source:
# Install the build tool
pip install maturin
# Navigate to the Python binding directory
cd koopman-dmd-py
# Build and install in the current environment
maturin develop --release
After installation, the module is available as koopman_dmd:
import koopman_dmd
DMD
The primary class for Dynamic Mode Decomposition. Supports standard DMD, centering, and Extended DMD via lifting functions.
Constructor
class DMD(rank=None, center=False, lifting=None, lifting_param=None)
| Parameter | Type | Default | Description |
|---|---|---|---|
rank |
int | None |
None |
SVD truncation rank. None selects rank automatically
via 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 |
str | None |
None |
Lifting function type. One of "polynomial",
"trigonometric", or "delay". |
lifting_param |
int | None |
None |
Parameter for the lifting function. Interpreted as polynomial
degree, number of harmonics, or number of delays depending on
the lifting type. |
from koopman_dmd import DMD
# Auto-rank, no centering
dmd = DMD()
# Explicit rank with centering
dmd = DMD(rank=4, center=True)
# Extended DMD with polynomial lifting (degree 3)
dmd = DMD(lifting="polynomial", lifting_param=3)
# Extended DMD with trigonometric lifting (2 harmonics)
dmd = DMD(lifting="trigonometric", lifting_param=2)
# Extended DMD with delay embedding (5 delays)
dmd = DMD(lifting="delay", lifting_param=5)
Methods
fit(data)
Fit the DMD model to a data matrix.
| Parameter | Type | Description |
|---|---|---|
data |
numpy.ndarray |
Data matrix of shape (n_vars, n_time). Each column is a
snapshot; each row is a state variable. |
Returns: self -- the fitted DMD object, enabling method chaining.
import numpy as np
from koopman_dmd import DMD
data = np.random.randn(3, 100) # 3 variables, 100 time steps
dmd = DMD(rank=2).fit(data)
predict(n_ahead, x0=None)
Predict future states using the spectral decomposition. Each time step is computed independently from eigenvalues, modes, and amplitudes (non-recursive). Suitable for long-range forecasting.
| Parameter | Type | Default | Description |
|---|---|---|---|
n_ahead |
int |
Number of future time steps to predict. | |
x0 |
numpy.ndarray | None |
None |
Initial condition vector. If None, uses the first
snapshot from the training data. |
Returns: numpy.ndarray of shape (n_vars, n_ahead).
# Predict 50 steps ahead from the training initial condition
forecast = dmd.predict(50)
# Predict from a custom initial condition
x0 = np.array([1.0, 0.0, -0.5])
forecast = dmd.predict(50, x0=x0)
predict_matrix(n_ahead, x0=None)
Predict future states using recursive matrix multiplication. Each step is computed by applying the reconstructed linear operator to the previous state. Faster for short horizons.
| Parameter | Type | Default | Description |
|---|---|---|---|
n_ahead |
int |
Number of future time steps to predict. | |
x0 |
numpy.ndarray | None |
None |
Initial condition vector. If None, uses the first
snapshot from the training data. |
Returns: numpy.ndarray of shape (n_vars, n_ahead).
forecast_mat = dmd.predict_matrix(50)
reconstruct(n_time, x0=None)
Reconstruct the fitted signal over a given number of time steps. Uses the spectral decomposition to reproduce the training dynamics. If centering was enabled, the stored mean is added back automatically.
| Parameter | Type | Default | Description |
|---|---|---|---|
n_time |
int |
Number of time steps to reconstruct. | |
x0 |
numpy.ndarray | None |
None |
Initial condition. If None, uses the first training snapshot. |
Returns: numpy.ndarray of shape (n_vars, n_time).
fitted = dmd.reconstruct(100)
spectrum(dt)
Compute the continuous-time spectral properties of the decomposition. Converts discrete-time eigenvalues to frequencies, growth rates, and stability indicators.
| Parameter | Type | Description |
|---|---|---|
dt |
float |
Time step between snapshots (seconds). |
Returns: list[dict] -- one dictionary per mode with keys:
| Key | Type | Description |
|---|---|---|
frequency |
float |
Oscillation frequency (Hz). |
magnitude |
float |
Absolute value of the discrete-time eigenvalue. |
growth_rate |
float |
Continuous-time growth rate (positive = growing, negative = decaying). |
damping_ratio |
float |
Damping ratio of the mode. |
stability |
str |
One of "stable", "unstable", or "neutral". |
spec = dmd.spectrum(dt=0.01)
for mode in spec:
print(f"freq={mode['frequency']:.4f} Hz, growth={mode['growth_rate']:.4f}, {mode['stability']}")
stability(tol=1e-6)
Assess the overall stability of the fitted linear system by examining the eigenvalue magnitudes.
| Parameter | Type | Default | Description |
|---|---|---|---|
tol |
float |
1e-6 |
Tolerance for classifying eigenvalues as neutral (on the unit circle). |
Returns: dict with keys:
| Key | Type | Description |
|---|---|---|
spectral_radius |
float |
Maximum eigenvalue magnitude. |
is_stable |
bool |
True if all eigenvalue magnitudes are at most 1 + tol. |
is_unstable |
bool |
True if any eigenvalue magnitude exceeds 1 + tol. |
n_growing |
int |
Number of eigenvalues with magnitude greater than 1 + tol. |
n_decaying |
int |
Number of eigenvalues with magnitude less than 1 - tol. |
n_neutral |
int |
Number of eigenvalues within tol of the unit circle. |
stab = dmd.stability()
print(f"Spectral radius: {stab['spectral_radius']:.6f}")
print(f"Stable: {stab['is_stable']}")
print(f"Growing/Decaying/Neutral: {stab['n_growing']}/{stab['n_decaying']}/{stab['n_neutral']}")
error(data)
Compute reconstruction error metrics by comparing the DMD reconstruction to the original data.
| Parameter | Type | Description |
|---|---|---|
data |
numpy.ndarray |
Original data matrix of shape (n_vars, n_time). |
Returns: dict with keys:
| Key | Type | Description |
|---|---|---|
rmse |
float |
Root mean squared error across all entries. |
relative_error |
float |
Frobenius norm of residual divided by Frobenius norm of data. |
max_error |
float |
Maximum absolute pointwise error. |
err = dmd.error(data)
print(f"RMSE: {err['rmse']:.6f}")
print(f"Relative: {err['relative_error']:.6f}")
print(f"Max: {err['max_error']:.6f}")
dominant_modes(criterion, n)
Extract the most significant modes according to a ranking criterion.
| Parameter | Type | Description |
|---|---|---|
criterion |
str |
Ranking criterion: "amplitude" (by amplitude magnitude)
or "energy" (by mode energy contribution). |
n |
int |
Number of top modes to return. |
Returns: list[dict] -- one dictionary per mode, sorted by
the chosen criterion in descending order.
top = dmd.dominant_modes(criterion="energy", n=3)
for m in top:
print(m)
residual()
Compute per-mode residual norms, measuring how well each individual mode satisfies the DMD eigenvalue equation.
Returns: dict with keys:
| Key | Type | Description |
|---|---|---|
mode_residuals |
list[float] |
Residual norm for each mode. |
max_residual |
float |
Maximum residual across all modes. |
mean_residual |
float |
Mean residual across all modes. |
res = dmd.residual()
print(f"Max residual: {res['max_residual']:.2e}")
print(f"Mean residual: {res['mean_residual']:.2e}")
Properties
| Property | Type | Description |
|---|---|---|
eigenvalues |
list[complex] |
Discrete-time DMD eigenvalues. |
modes |
numpy.ndarray (complex) |
DMD mode matrix of shape (n_vars, rank). Each column is a mode. |
amplitudes |
list[complex] |
Mode amplitudes (weights for the initial condition). |
rank |
int |
Effective rank used in the decomposition. |
print("Eigenvalues:", dmd.eigenvalues)
print("Modes shape:", dmd.modes.shape)
print("Amplitudes: ", dmd.amplitudes)
print("Rank: ", dmd.rank)
HankelDMD
DMD with time-delay embedding via Hankel matrices. Captures higher-order dynamics from scalar or low-dimensional measurements by augmenting the state space with lagged copies of the data.
Constructor
class HankelDMD(delays=None, rank=None, dt=1.0)
| Parameter | Type | Default | Description |
|---|---|---|---|
delays |
int | None |
None |
Number of time-delay embeddings. None selects automatically. |
rank |
int | None |
None |
SVD truncation rank. None selects automatically. |
dt |
float |
1.0 |
Time step between snapshots. |
from koopman_dmd import HankelDMD
hdmd = HankelDMD(delays=10, rank=4, dt=0.01)
Methods
fit(data)
Fit the Hankel-DMD model. Internally constructs the Hankel matrix from the input data, then performs standard DMD on the augmented state.
| Parameter | Type | Description |
|---|---|---|
data |
numpy.ndarray |
Data matrix of shape (n_vars, n_time). |
Returns: self
predict(n_ahead)
Predict future states. Output is projected back to the original observation dimension.
| Parameter | Type | Description |
|---|---|---|
n_ahead |
int |
Number of future time steps to predict. |
Returns: numpy.ndarray of shape (n_vars, n_ahead).
reconstruct()
Reconstruct the training data from the Hankel-DMD model.
Returns: numpy.ndarray of shape (n_vars, n_time).
Properties
| Property | Type | Description |
|---|---|---|
eigenvalues |
list[complex] |
Discrete-time eigenvalues of the Hankel-DMD model. |
rank |
int |
Effective rank used in the decomposition. |
import numpy as np
from koopman_dmd import HankelDMD
# Scalar time series with delay embedding
t = np.linspace(0, 10, 500)
signal = np.sin(2.0 * np.pi * t) + 0.5 * np.sin(6.0 * np.pi * t)
data = signal.reshape(1, -1)
hdmd = HankelDMD(delays=20, rank=4).fit(data)
print("Eigenvalues:", hdmd.eigenvalues)
print("Rank:", hdmd.rank)
recon = hdmd.reconstruct()
forecast = hdmd.predict(100)
GLA
Generalized Laplace Analysis for extracting Koopman eigenvalues and eigenfunctions from trajectory data using spectral methods. GLA solves an optimization problem to find the eigenvalues that best explain the observed dynamics.
Constructor
class GLA(n_eigenvalues=2, tol=1e-6, max_iter=None)
| Parameter | Type | Default | Description |
|---|---|---|---|
n_eigenvalues |
int |
2 |
Number of Koopman eigenvalues to extract. |
tol |
float |
1e-6 |
Convergence tolerance for the iterative solver. |
max_iter |
int | None |
None |
Maximum iterations. None uses an internal default. |
from koopman_dmd import GLA
gla = GLA(n_eigenvalues=4, tol=1e-8)
Methods
fit(data)
Fit the GLA model to trajectory data.
| Parameter | Type | Description |
|---|---|---|
data |
numpy.ndarray |
Data matrix of shape (n_vars, n_time). |
Returns: self
predict(n_ahead)
Predict future states using the extracted Koopman eigenvalues.
| Parameter | Type | Description |
|---|---|---|
n_ahead |
int |
Number of future time steps. |
Returns: numpy.ndarray
reconstruct()
Reconstruct the training data from the GLA model.
Returns: numpy.ndarray
Properties
| Property | Type | Description |
|---|---|---|
eigenvalues |
list[complex] |
Extracted Koopman eigenvalues. |
convergence_rate |
float |
Convergence rate of the iterative solver at termination. |
import numpy as np
from koopman_dmd import GLA
t = np.linspace(0, 20, 1000)
signal = np.cos(1.5 * t) + 0.3 * np.cos(3.7 * t)
data = signal.reshape(1, -1)
gla = GLA(n_eigenvalues=4, tol=1e-8).fit(data)
print("Eigenvalues:", gla.eigenvalues)
print("Convergence rate:", gla.convergence_rate)
recon = gla.reconstruct()
forecast = gla.predict(200)
Functions
generate_trajectory
generate_trajectory(ic, map_name, n_steps, params=None)
Generate a trajectory from one of the built-in dynamical maps.
| Parameter | Type | Default | Description |
|---|---|---|---|
ic |
list |
Initial condition as a list of floats. | |
map_name |
str |
Name of the dynamical map. One of:
"standard",
"froeschle",
"extended_standard",
"henon",
"logistic".
|
|
n_steps |
int |
Number of iteration steps to compute. | |
params |
dict | None |
None |
Map-specific parameters. Keys depend on the map (see below). |
Returns: numpy.ndarray of shape (n_vars, n_steps).
Map parameters:
| Map | Parameters | Default Values |
|---|---|---|
"standard" |
{"epsilon": float} |
epsilon = 0.12 |
"froeschle" |
{"epsilon": float} |
epsilon = 0.12 |
"extended_standard" |
{"epsilon": float} |
epsilon = 0.12 |
"henon" |
{"a": float, "b": float} |
a = 1.4, b = 0.3 |
"logistic" |
{"r": float} |
r = 3.9 |
from koopman_dmd import generate_trajectory
# Standard map trajectory
traj = generate_trajectory(
ic=[0.1, 0.2],
map_name="standard",
n_steps=10000,
params={"epsilon": 0.12}
)
# Henon map with custom parameters
traj = generate_trajectory(
ic=[0.0, 0.0],
map_name="henon",
n_steps=5000,
params={"a": 1.4, "b": 0.3}
)
# Logistic map
traj = generate_trajectory(
ic=[0.5],
map_name="logistic",
n_steps=1000,
params={"r": 3.9}
)
harmonic_time_average
harmonic_time_average(ic, map_name, observable, omega, n_iter, params=None)
Compute the harmonic time average of an observable along a trajectory at a
specified frequency. The harmonic time average isolates the Fourier component
of the observable at frequency omega, providing a finite-time
approximation of the corresponding Koopman eigenfunction.
| Parameter | Type | Default | Description |
|---|---|---|---|
ic |
list |
Initial condition. | |
map_name |
str |
Name of the dynamical map. | |
observable |
str |
Observable function. One of:
"identity",
"sin_pi",
"cos_pi",
"sin_pi_xy",
"quadratic".
|
|
omega |
float |
Frequency at which to compute the harmonic average. | |
n_iter |
int |
Number of iterations for the time average. | |
params |
dict | None |
None |
Map-specific parameters. |
Returns: dict with keys:
| Key | Type | Description |
|---|---|---|
magnitude |
float |
Absolute value of the harmonic time average. |
phase |
float |
Phase angle (radians). |
real |
float |
Real part of the harmonic time average. |
imag |
float |
Imaginary part of the harmonic time average. |
from koopman_dmd import harmonic_time_average
result = harmonic_time_average(
ic=[0.1, 0.2],
map_name="standard",
observable="identity",
omega=0.3,
n_iter=10000,
params={"epsilon": 0.12}
)
print(f"Magnitude: {result['magnitude']:.6f}")
print(f"Phase: {result['phase']:.6f}")
print(f"Real: {result['real']:.6f}")
print(f"Imag: {result['imag']:.6f}")
mesochronic_compute
mesochronic_compute(map_name, x_range, y_range, resolution, observable, omega, n_iter, params=None)
Compute the harmonic time average over a grid of initial conditions, producing matrices suitable for mesochronic analysis and visualization. This function evaluates the HTA at each grid point in the specified rectangular domain.
| Parameter | Type | Default | Description |
|---|---|---|---|
map_name |
str |
Name of the dynamical map. | |
x_range |
tuple |
Range of x-coordinates as (x_min, x_max). |
|
y_range |
tuple |
Range of y-coordinates as (y_min, y_max). |
|
resolution |
int |
Number of grid points along each axis. | |
observable |
str |
Observable function name. | |
omega |
float |
Frequency for the harmonic average. | |
n_iter |
int |
Number of iterations per grid point. | |
params |
dict | None |
None |
Map-specific parameters. |
Returns: dict with keys:
| Key | Type | Description |
|---|---|---|
hta_matrix |
numpy.ndarray |
HTA magnitude at each grid point, shape (resolution, resolution). |
phase_matrix |
numpy.ndarray |
HTA phase at each grid point, shape (resolution, resolution). |
x_coords |
numpy.ndarray |
x-axis coordinate values, shape (resolution,). |
y_coords |
numpy.ndarray |
y-axis coordinate values, shape (resolution,). |
from koopman_dmd import mesochronic_compute
result = mesochronic_compute(
map_name="standard",
x_range=(0.0, 1.0),
y_range=(0.0, 1.0),
resolution=200,
observable="identity",
omega=0.0,
n_iter=5000,
params={"epsilon": 0.12}
)
# Visualize with matplotlib
import matplotlib.pyplot as plt
plt.pcolormesh(
result["x_coords"],
result["y_coords"],
result["hta_matrix"],
cmap="viridis"
)
plt.colorbar(label="HTA magnitude")
plt.xlabel("x")
plt.ylabel("y")
plt.title("Mesochronic plot -- Standard Map")
plt.show()
classify_phase_space
classify_phase_space(ic, map_name, omega, n_iter, params=None)
Classify a single initial condition as belonging to a regular (quasiperiodic) or chaotic region of phase space, based on the convergence behavior of the harmonic time average.
| Parameter | Type | Default | Description |
|---|---|---|---|
ic |
list |
Initial condition. | |
map_name |
str |
Name of the dynamical map. | |
omega |
float |
Frequency for the harmonic average. | |
n_iter |
int |
Number of iterations. | |
params |
dict | None |
None |
Map-specific parameters. |
Returns: dict with keys:
| Key | Type | Description |
|---|---|---|
classification |
str |
"regular" or "chaotic". |
hta_magnitude |
float |
Final HTA magnitude. |
hta_phase |
float |
Final HTA phase (radians). |
from koopman_dmd import classify_phase_space
result = classify_phase_space(
ic=[0.1, 0.2],
map_name="standard",
omega=0.0,
n_iter=50000,
params={"epsilon": 0.12}
)
print(f"Classification: {result['classification']}")
print(f"HTA magnitude: {result['hta_magnitude']:.6f}")
hta_convergence
hta_convergence(ic, map_name, observable, omega, max_iter, step, params=None)
Compute the harmonic time average at increasing iteration counts to study its convergence behavior. Returns the HTA value at evenly spaced checkpoints, which can be used to assess whether the average has stabilized or is still fluctuating (indicating chaotic dynamics).
| Parameter | Type | Default | Description |
|---|---|---|---|
ic |
list |
Initial condition. | |
map_name |
str |
Name of the dynamical map. | |
observable |
str |
Observable function name. | |
omega |
float |
Frequency for the harmonic average. | |
max_iter |
int |
Maximum number of iterations. | |
step |
int |
Step size between checkpoints. | |
params |
dict | None |
None |
Map-specific parameters. |
Returns: dict -- contains convergence data (iteration counts and
corresponding HTA values at each checkpoint).
from koopman_dmd import hta_convergence
conv = hta_convergence(
ic=[0.1, 0.2],
map_name="standard",
observable="identity",
omega=0.3,
max_iter=50000,
step=500,
params={"epsilon": 0.12}
)
Complete Examples
DMD: Fit, Predict, and Analyze
import numpy as np
from koopman_dmd import DMD
# Generate a multi-component signal
n = 200
dt = 0.02
t = np.arange(n) * dt
x1 = np.sin(2.0 * np.pi * 1.5 * t)
x2 = np.cos(2.0 * np.pi * 1.5 * t) * np.exp(-0.1 * t)
x3 = np.sin(2.0 * np.pi * 3.0 * t) * 0.5
data = np.vstack([x1, x2, x3]) # shape (3, 200)
# Fit the DMD model
dmd = DMD(rank=4, center=True).fit(data)
# Inspect eigenvalues and modes
print("Rank: ", dmd.rank)
print("Eigenvalues:", dmd.eigenvalues)
print("Modes shape:", dmd.modes.shape)
print("Amplitudes: ", dmd.amplitudes)
# Spectral analysis
spec = dmd.spectrum(dt=dt)
for i, mode in enumerate(spec):
print(f"Mode {i}: freq={mode['frequency']:.2f} Hz, "
f"growth={mode['growth_rate']:.4f}, {mode['stability']}")
# Stability check
stab = dmd.stability()
print(f"System stable: {stab['is_stable']}")
# Reconstruction error
err = dmd.error(data)
print(f"RMSE: {err['rmse']:.6f}")
print(f"Relative error: {err['relative_error']:.6f}")
# Dominant modes
top = dmd.dominant_modes(criterion="energy", n=2)
print("Top 2 modes by energy:", top)
# Mode residuals
res = dmd.residual()
print(f"Max residual: {res['max_residual']:.2e}")
# Predict 100 steps ahead
forecast = dmd.predict(100)
print(f"Forecast shape: {forecast.shape}")
# Reconstruct the training window
fitted = dmd.reconstruct(n)
print(f"Reconstruction shape: {fitted.shape}")
Extended DMD with Polynomial Lifting
import numpy as np
from koopman_dmd import DMD
# Nonlinear signal: sin^2(t)
n = 300
dt = 0.05
t = np.arange(n) * dt
signal = np.sin(t) ** 2
data = signal.reshape(1, -1)
# Standard DMD (will have high error on nonlinear signal)
dmd_std = DMD().fit(data)
err_std = dmd_std.error(data)
print(f"Standard DMD RMSE: {err_std['rmse']:.6f}")
# Extended DMD with polynomial lifting (degree 2)
dmd_ext = DMD(lifting="polynomial", lifting_param=2).fit(data)
err_ext = dmd_ext.error(data)
print(f"Extended DMD RMSE: {err_ext['rmse']:.6f}")
# Predictions are automatically back-projected to original space
forecast = dmd_ext.predict(100)
print(f"Forecast shape: {forecast.shape}") # (1, 100)
Hankel-DMD for Scalar Time Series
import numpy as np
from koopman_dmd import HankelDMD
# Two-frequency signal from scalar measurement
t = np.linspace(0, 10, 500)
signal = np.sin(2.0 * np.pi * t) + 0.5 * np.sin(6.0 * np.pi * t)
data = signal.reshape(1, -1)
# Hankel-DMD with time-delay embedding
hdmd = HankelDMD(delays=20, rank=4, dt=t[1] - t[0]).fit(data)
print("Eigenvalues:", hdmd.eigenvalues)
print("Rank:", hdmd.rank)
# Reconstruct and predict
recon = hdmd.reconstruct()
forecast = hdmd.predict(100)
print(f"Reconstruction shape: {recon.shape}")
print(f"Forecast shape: {forecast.shape}")
GLA for Spectral Extraction
import numpy as np
from koopman_dmd import GLA
# Signal with two distinct frequencies
t = np.linspace(0, 20, 1000)
signal = np.cos(1.5 * t) + 0.3 * np.cos(3.7 * t)
data = signal.reshape(1, -1)
# Extract 4 Koopman eigenvalues via GLA
gla = GLA(n_eigenvalues=4, tol=1e-8).fit(data)
print("Koopman eigenvalues:")
for ev in gla.eigenvalues:
print(f" {ev}")
print(f"Convergence rate: {gla.convergence_rate:.2e}")
# Reconstruct and forecast
recon = gla.reconstruct()
forecast = gla.predict(200)
print(f"Forecast shape: {forecast.shape}")
Trajectory Generation and HTA Analysis
import numpy as np
from koopman_dmd import (
generate_trajectory,
harmonic_time_average,
classify_phase_space,
hta_convergence,
mesochronic_compute,
)
# Generate a trajectory on the standard map
traj = generate_trajectory(
ic=[0.1, 0.2],
map_name="standard",
n_steps=10000,
params={"epsilon": 0.12}
)
print(f"Trajectory shape: {traj.shape}") # (2, 10000)
# Compute harmonic time average
hta = harmonic_time_average(
ic=[0.1, 0.2],
map_name="standard",
observable="identity",
omega=0.3,
n_iter=10000,
params={"epsilon": 0.12}
)
print(f"HTA magnitude: {hta['magnitude']:.6f}")
print(f"HTA phase: {hta['phase']:.6f}")
# Classify the initial condition
cls = classify_phase_space(
ic=[0.1, 0.2],
map_name="standard",
omega=0.0,
n_iter=50000,
params={"epsilon": 0.12}
)
print(f"Region: {cls['classification']}")
# Study convergence of the HTA
conv = hta_convergence(
ic=[0.1, 0.2],
map_name="standard",
observable="identity",
omega=0.3,
max_iter=50000,
step=500,
params={"epsilon": 0.12}
)
# Compute mesochronic plot data
meso = mesochronic_compute(
map_name="standard",
x_range=(0.0, 1.0),
y_range=(0.0, 1.0),
resolution=100,
observable="identity",
omega=0.0,
n_iter=5000,
params={"epsilon": 0.12}
)
print(f"HTA matrix shape: {meso['hta_matrix'].shape}")
print(f"Phase matrix shape: {meso['phase_matrix'].shape}")