Skip to content

Commit

Permalink
Merge branch 'master' of https://github.com/klapo/PyDMD
Browse files Browse the repository at this point in the history
  • Loading branch information
klapo committed Aug 3, 2023
2 parents 9120093 + c9bbdc5 commit 29177b3
Show file tree
Hide file tree
Showing 9 changed files with 10,258 additions and 27 deletions.
8,104 changes: 8,104 additions & 0 deletions docs/source/_tutorials/tutorial16rdmd.html

Large diffs are not rendered by default.

38 changes: 23 additions & 15 deletions pydmd/bopdmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,10 @@ def compute_residual(alpha):
B, residual, error = compute_residual(alpha)
U, S, Vh = self._compute_irank_svd(Phi(alpha, t), tolrank)

# Initialize termination flags.
converged = False
stalled = False

# Initialize storage.
all_error = np.zeros(maxiter)
djac_matrix = np.zeros((M * IS, IA), dtype="complex")
Expand Down Expand Up @@ -548,7 +552,7 @@ def step(_lambda, rhs, scales_pvt, ij_pvt):
0.5
* np.linalg.multi_dot(
[delta_0.conj().T, djac_matrix.conj().T, rhs_temp]
).real
)[0].real
)
improvement_ratio = actual_improvement / pred_improvement

Expand All @@ -564,20 +568,21 @@ def step(_lambda, rhs, scales_pvt, ij_pvt):
B_0, residual_0, error_0 = compute_residual(alpha_0)

if error_0 < error:
alpha, B = alpha_0, B_0
residual, error = residual_0, error_0
break

# Terminate if no appropriate step length was found.
# Terminate if no appropriate step length was found...
if error_0 >= error:
if verbose:
msg = (
"Failed to find appropriate step length at "
"iteration {}. Current error {}."
)
warnings.warn(msg.format(itr, error))
print(msg.format(itr, error))
return B, alpha

# ...otherwise, update and proceed.
alpha, B, residual, error = alpha_0, B_0, residual_0, error_0

# Record the current error.
all_error[itr] = error

Expand All @@ -586,23 +591,26 @@ def step(_lambda, rhs, scales_pvt, ij_pvt):
update_msg = "Step {} Error {} Lambda {}"
print(update_msg.format(itr, error, _lambda))

# Terminate if the tolerance is met.
if error < tol:
# Update termination status and terminate if converged or stalled.
converged = error < tol
error_reduction = all_error[itr - 1] - all_error[itr]
stalled = (itr > 0) and (
error_reduction < eps_stall * all_error[itr - 1]
)

if converged:
if verbose:
print("Convergence reached!")
return B, alpha

# Terminate if a stall is detected.
if (
itr > 0
and all_error[itr - 1] - all_error[itr]
< eps_stall * all_error[itr - 1]
):
if stalled:
if verbose:
msg = (
"Stall detected: error reduced by less than {} "
"times the error at the previous step. "
"Iteration {}. Current error {}."
)
warnings.warn(msg.format(eps_stall, itr, error))
print(msg.format(eps_stall, itr, error))
return B, alpha

U, S, Vh = self._compute_irank_svd(Phi(alpha, t), tolrank)
Expand All @@ -613,7 +621,7 @@ def step(_lambda, rhs, scales_pvt, ij_pvt):
"Failed to reach tolerance after maxiter = {} iterations. "
"Current error {}."
)
warnings.warn(msg.format(maxiter, error))
print(msg.format(maxiter, error))

return B, alpha

Expand Down
9 changes: 6 additions & 3 deletions pydmd/costsdmd.py → pydmd/costs.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import xarray as xr


class CostsDMD:
class COSTS:
"""Coherent Spatio-Temporal Scale Separation with DMD.
:param window_length: Length of the analysis window in number of time steps.
Expand Down Expand Up @@ -80,8 +80,6 @@ def __init__(
n_components=None,
):
self._n_components = n_components
self._step_size = step_size
self._window_length = window_length
self._svd_rank = svd_rank
self._global_svd = global_svd
self._initialize_artificially = initialize_artificially
Expand Down Expand Up @@ -197,6 +195,11 @@ def omega_classes(self):
raise ValueError("You need to call `cluster_omega()` first.")
return self._omega_classes

@staticmethod
def relative_error(x_est, x_true):
"""Helper function for calculating the relative error."""
return np.linalg.norm(x_est - x_true) / np.linalg.norm(x_true)

@staticmethod
def build_windows(data, window_length, step_size, integer_windows=False):
"""Calculate how many times to slide the window across the data."""
Expand Down
260 changes: 260 additions & 0 deletions pydmd/mrcosts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
import numpy as np
from pydmd.bopdmd import BOPDMD
from .utils import compute_rank, compute_svd
import copy
from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score
import matplotlib.pyplot as plt
import xarray as xr
from pydmd.costs import COSTS


class mrCOSTS:
"""Multi-resolution Coherent Spatio-Temporal Scale Separation (mrCOSTS) with DMD.
:param window_length_array: Length of the analysis window in number of time steps.
:type window_length_array: int
:param step_size_array: Number of time steps to slide each CSM-DMD window.
:type step_size_array: int
:param n_components: Number of independent frequency bands for this window length.
:type n_components: int
:param svd_rank_array: The rank of the BOPDMD fit.
:type svd_rank_array: int
:param global_svd: Flag indicating whether to find the proj_basis and initial
values using the entire dataset instead of individually for each window.
Generally using the global_svd speeds up the fitting process by not finding a
new initial value for each window. Default is True.
:type global_svd: bool
:param initialize_artificially: Flag indicating whether to initialize the DMD using
imaginary eigenvalues (i.e., the imaginary component of the cluster results from a
previous iteration) through the `cluster_centroids` keyword. Default is False.
:type initialize_artificially: bool
:param pydmd_kwargs: Keyword arguments to pass onto the BOPDMD object.
:type pydmd_kwargs: dict
:param cluster_centroids: Cluster centroids from a previous fitting iteration to
use for the initial guess of the eigenvalues. Should only be the imaginary
component.
:type cluster_centroids: numpy array
:param reset_alpha_init: Flag indicating whether the initial guess for the BOPDMD
eigenvalues should be reset for each window. Resetting the initial value increases
the computation time due to finding a new initial guess. Default is False.
:type reset_alpha_init: bool
:param force_even_eigs: Flag indicating whether an even svd_rank should be forced
when not specifying the svd_rank directly (i.e., svd_rank=0). Default is True.
:type global_svd: bool
:param max_rank: Maximum svd_rank allowed when the svd_rank is found through rank
truncation (i.e., svd_rank=0).
:type max_rank: int
:param use_kmean_freqs: Flag specifying if the BOPDMD fit should use initial values
taken from cluster centroids, e.g., from a previoius iteration.
:type use_kmean_freqs: bool
:param init_alpha: Initial guess for the eigenvalues provided to BOPDMD. Must be equal
to the `svd_rank`.
:type init_alpha: numpy array
:param max_rank: Maximum allowed `svd_rank`. Overrides the optimal rank truncation if
`svd_rank=0`.
:type max_rank: int
:param n_components: Number of frequency bands to use for clustering.
:type n_components: int
:param force_even_eigs: Flag specifying if the `svd_rank` should be forced to be even.
:type force_even_eigs: bool
:param reset_alpha_init: Flag specifying if the initial eigenvalue guess should be reset
between windows.
:type reset_alpha_init: bool
"""

def __init__(
self,
window_length_array=None,
step_size_array=None,
svd_rank_array=None,
global_svd=True,
initialize_artificially=False,
use_last_freq=False,
use_kmean_freqs=False,
init_alpha=None,
pydmd_kwargs=None,
cluster_centroids=None,
reset_alpha_init=False,
force_even_eigs=True,
max_rank=None,
n_components_array=None,
):
self._n_components_array = n_components_array
self._step_size_array = step_size_array
self._window_length_array = window_length_array
self._svd_rank_array = svd_rank_array
self._global_svd = global_svd
self._initialize_artificially = initialize_artificially
self._use_last_freq = use_last_freq
self._use_kmean_freqs = use_kmean_freqs
self._init_alpha = init_alpha
self._cluster_centroids = cluster_centroids
self._reset_alpha_init = reset_alpha_init
self._force_even_eigs = force_even_eigs
self._max_rank = max_rank

# Initialize variables that are defined in fitting.
self._n_data_vars = None
self._n_time_steps = None
self._window_length = None
self._n_slides = None
self._time_array = None
self._modes_array = None
self._omega_array = None
self._amplitudes_array = None
self._cluster_centroids = None
self._omega_classes = None
self._transform_method = None
self._window_means_array = None
self._non_integer_n_slide = None

# Specify default keywords to hand to BOPDMD.
if pydmd_kwargs is None:
self._pydmd_kwargs = {
"eig_sort": "imag",
"proj_basis": None,
"use_proj": False,
}
else:
self._pydmd_kwargs = pydmd_kwargs
self._pydmd_kwargs["eig_sort"] = pydmd_kwargs.get(
"eig_sort", "imag"
)
self._pydmd_kwargs["proj_basis"] = pydmd_kwargs.get(
"proj_basis", None
)
self._pydmd_kwargs["use_proj"] = pydmd_kwargs.get("use_proj", False)

@property
def svd_rank(self):
"""
:return: the rank used for the svd truncation.
:rtype: int or float
"""
return self._svd_rank_array

@property
def window_length(self):
"""
:return: the length of the windows used for this decomposition level.
:rtype: int or float
"""
return self._window_length

@property
def n_slides(self):
"""
:return: number of window slides for this decomposition level.
:rtype: int
"""
return self._n_slides

@property
def modes_array(self):
if not hasattr(self, "_modes_array"):
raise ValueError("You need to call fit before")
return self._modes_array

@property
def amplitudes_array(self):
if not hasattr(self, "_amplitudes_array"):
raise ValueError("You need to call fit first.")
return self._amplitudes_array

@property
def omega_array(self):
if not hasattr(self, "_omega_array"):
raise ValueError("You need to call fit first.")
return self._omega_array

@property
def time_array(self):
if not hasattr(self, "_time_array"):
raise ValueError("You need to call fit first.")
return self._time_array

@property
def window_means_array(self):
if not hasattr(self, "_window_means_array"):
raise ValueError("You need to call fit first.")
return self._window_means_array

@property
def n_components(self):
if not hasattr(self, "_n_components"):
raise ValueError("You need to call `cluster_omega()` first.")
return self._n_components

@property
def cluster_centroids(self):
if not hasattr(self, "_cluster_centroids"):
raise ValueError("You need to call `cluster_omega()` first.")
return self._cluster_centroids

@property
def omega_classes(self):
if not hasattr(self, "_omega_classes"):
raise ValueError("You need to call `cluster_omega()` first.")
return self._omega_classes

def fit(self, data):
window_lengths = self._window_length_array
step_sizes = self.step_size_array
svd_ranks = self.svd_rank_array

mrd_list = []
suppress_growth = True
transform_method = "squared"

data_iter = np.zeros((num_decompositions, data.shape[0], data.shape[1]))
data_iter[0, :, :] = data

for n_decomp, (window, step, rank) in enumerate(
zip(window_lengths, step_sizes, svd_ranks)
):
print("Working on window length={}".format(window))

x_iter = data_iter[n_decomp, :, :].squeeze()
mrd = COSTS(
svd_rank=rank,
global_svd=True,
pydmd_kwargs={"eig_constraints": {"conjugate_pairs", "stable"}},
)

print("Fitting")
print("_________________________________________________")
mrd.fit(x_iter, np.atleast_2d(time), window, step, verbose=True)
print("_________________________________________________")

# Cluster the frequency bands
if self._cluster_sweep:
n_components = mrd.cluster_hyperparameter_sweep(
transform_method=transform_method
)
else:
n_components = self._n_components_array[n_decomp]

_ = mrd.cluster_omega(
n_components=n_components, transform_method=transform_method
)

# Global reconstruction
global_reconstruction = mrd.global_reconstruction(
{"suppress_growth": suppress_growth}
)
re = mrd.relative_error(global_reconstruction.real, x_iter)
if verbose:
print("Error in Global Reconstruction = {:.2}".format(re))

# Scale separation
xr_low_frequency, xr_high_frequency = mrd.scale_separation(
scale_reconstruction_kwargs={"suppress_growth": suppress_growth}
)

# Pass the low frequency component to the next level of decomposition.
if n_decomp < num_decompositions - 1:
data_iter[n_decomp + 1, :, :] = xr_low_frequency

# Save the object for later use.
mrd_list.append(copy.copy(mrd))
Loading

0 comments on commit 29177b3

Please sign in to comment.