Skip to content

Commit 201f910

Browse files
authored
Merge pull request PyDMD#568 from ClimeTrend/fit-econ
Economy fit method for the BOPDMD class
2 parents 1f6db1f + 21959de commit 201f910

File tree

5 files changed

+216
-9
lines changed

5 files changed

+216
-9
lines changed

pydmd/bopdmd.py

Lines changed: 117 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1351,17 +1351,31 @@ def _check_eig_constraints(self, eig_constraints):
13511351
msg = "Invalid eigenvalue constraint combination provided."
13521352
raise ValueError(msg)
13531353

1354-
def _initialize_alpha(self):
1354+
def _initialize_alpha(self, s=None, V=None):
13551355
"""
13561356
Uses projected trapezoidal rule to approximate the eigenvalues of A in
13571357
z' = Az.
13581358
The computed eigenvalues will serve as our initial guess for alpha.
1359-
1359+
If the singular values and right singular vectors are provided, they
1360+
will be used to compute the projection. Otherwise, the snapshot data
1361+
will be projected onto the projection basis.
1362+
1363+
:param s: the singular values of the snapshot matrix. If provided, they
1364+
will be used along with V to compute the projection.
1365+
:type s: numpy.ndarray
1366+
:param V: the right singular vectors of the snapshot matrix. If
1367+
provided, they will be used along with s to compute the projection.
1368+
:type V: numpy.ndarray
13601369
:return: Approximated eigenvalues of the matrix A.
13611370
:rtype: numpy.ndarray
13621371
"""
1363-
# Project the snapshot data onto the projection basis.
1364-
ux = self._proj_basis.conj().T.dot(self.snapshots)
1372+
if s is not None and V is not None:
1373+
# If the singular values and right singular vectors are provided,
1374+
# use them to compute the projection.
1375+
ux = np.diag(s).dot(V)
1376+
else:
1377+
# Project the snapshot data onto the projection basis.
1378+
ux = self._proj_basis.conj().T.dot(self.snapshots)
13651379
ux1 = ux[:, :-1]
13661380
ux2 = ux[:, 1:]
13671381

@@ -1461,6 +1475,105 @@ def fit(self, X, t):
14611475

14621476
return self
14631477

1478+
def fit_econ(self, s, V, t):
1479+
"""
1480+
Compute the Optimized Dynamic Mode Decomposition using the
1481+
SVD results of the snapshot matrix.
1482+
Use this method if you have pre-computed the SVD and the
1483+
original snapshot matrix is not avaialble or too large to
1484+
store in memory.
1485+
The left singular vectors (U) must be specified as the projection
1486+
basis when initializing the BOPDMD object.
1487+
1488+
:param s: the singular values.
1489+
:type s: array_like
1490+
:param V: matrix of right singular vectors, where each row is a
1491+
different singular vector.
1492+
:type V: numpy.ndarray
1493+
:param t: the input time vector.
1494+
:type t: numpy.ndarray
1495+
"""
1496+
self._reset()
1497+
self._time = np.asarray(t).squeeze()
1498+
s = np.asarray(s).squeeze()
1499+
1500+
if self._proj_basis is None or not self._use_proj:
1501+
msg = """
1502+
proj_basis must be provided when using fit_econ,
1503+
and use_proj must be set to True.
1504+
"""
1505+
raise ValueError(msg)
1506+
if (
1507+
not isinstance(self._proj_basis, np.ndarray)
1508+
or self._proj_basis.ndim != 2
1509+
or self._proj_basis.shape[1] != self._svd_rank
1510+
):
1511+
msg = (
1512+
"proj_basis must be a 2D np.ndarray with "
1513+
f"{self._svd_rank} columns."
1514+
)
1515+
raise ValueError(msg)
1516+
1517+
if self._time.ndim > 1:
1518+
msg = "Input time vector t must be one-dimensional."
1519+
raise ValueError(msg)
1520+
1521+
if s.ndim != 1 or len(s) != self._svd_rank:
1522+
msg = f"s must be one-dimensional and of length {self._svd_rank}."
1523+
raise ValueError(msg)
1524+
1525+
if (
1526+
not isinstance(V, np.ndarray)
1527+
or V.ndim != 2
1528+
or V.shape[0] != self._svd_rank
1529+
or V.shape[1] != len(self._time)
1530+
):
1531+
msg = (
1532+
"V must be a 2D numpy.ndarray with shape "
1533+
f"({self._svd_rank}, {len(self._time)})."
1534+
)
1535+
raise ValueError(msg)
1536+
1537+
# Set/check the initial guess for the continuous-time DMD eigenvalues.
1538+
if self._init_alpha is None:
1539+
self._init_alpha = self._initialize_alpha(s=s, V=V)
1540+
elif (
1541+
not isinstance(self._init_alpha, np.ndarray)
1542+
or self._init_alpha.ndim > 1
1543+
or len(self._init_alpha) != self._svd_rank
1544+
):
1545+
msg = "init_alpha must be a 1D np.ndarray with {} entries."
1546+
raise ValueError(msg.format(self._svd_rank))
1547+
1548+
# Build the BOP-DMD operator now that the initial alpha and
1549+
# the projection basis have been defined.
1550+
self._Atilde = BOPDMDOperator(
1551+
self._compute_A,
1552+
self._use_proj,
1553+
self._init_alpha,
1554+
self._proj_basis,
1555+
self._num_trials,
1556+
self._trial_size,
1557+
self._eig_sort,
1558+
self._eig_constraints,
1559+
self._mode_prox,
1560+
self._remove_bad_bags,
1561+
self._bag_warning,
1562+
self._bag_maxfail,
1563+
**self._varpro_opts_dict,
1564+
)
1565+
1566+
# Define the snapshots that will be used for fitting.
1567+
snp = np.diag(s).dot(V)
1568+
1569+
# Fit the data.
1570+
self._b = self.operator.compute_operator(snp.T, self._time)
1571+
1572+
# Store the singular values
1573+
self._singular_values = s
1574+
1575+
return self
1576+
14641577
def forecast(self, t):
14651578
"""
14661579
Predict the output X given the input time t using the fitted DMD model.

pydmd/plotter.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -650,7 +650,10 @@ def plot_summary(
650650
sizes of all other eigenvalues are then scaled according to eigenvalue
651651
significance.
652652
:type max_eig_ms: int
653-
:param max_sval_plot: Maximum number of singular values to plot.
653+
:param max_sval_plot: Maximum number of singular values to plot. If the DMD
654+
instance was fitted via the `fit_econ` method, the maximum number of
655+
singular values that can be plotted is determined by the length of the
656+
array of singular values passed to `fit_econ`.
654657
:type max_sval_plot: int
655658
:param title_fontsize: Fontsize used for subplot titles.
656659
:type title_fontsize: int
@@ -781,7 +784,14 @@ def plot_summary(
781784
else:
782785
# Use input data matrix to compute singular values.
783786
snp = dmd.snapshots
784-
s = np.linalg.svd(snp, full_matrices=False, compute_uv=False)
787+
if snp is not None:
788+
s = np.linalg.svd(snp, full_matrices=False, compute_uv=False)
789+
else:
790+
try:
791+
s = dmd._singular_values
792+
except AttributeError as e:
793+
msg = "Snapshots and singular values are not available."
794+
raise ValueError(msg) from e
785795
# Compute the percent of data variance captured by each singular value.
786796
s_var = s * (100 / np.sum(s))
787797
s_var = s_var[:max_sval_plot]

tests/test_bopdmd.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -641,3 +641,22 @@ def test_getter_errors():
641641

642642
with raises(ValueError):
643643
_ = bopdmd.amplitudes_std
644+
645+
646+
def test_fit_econ():
647+
"""
648+
Test that the fit_econ method gives the same results as the fit method.
649+
"""
650+
svd_rank = 2
651+
bopdmd = BOPDMD(svd_rank=svd_rank, use_proj=True)
652+
bopdmd.fit(Z, t)
653+
654+
U, s, V = np.linalg.svd(Z)
655+
U = U[:, :svd_rank]
656+
s = s[:svd_rank]
657+
V = V[:svd_rank, :]
658+
bopdmd_econ = BOPDMD(svd_rank=svd_rank, use_proj=True, proj_basis=U)
659+
bopdmd_econ.fit_econ(s, V, t)
660+
661+
np.testing.assert_allclose(bopdmd_econ.eigs, bopdmd.eigs)
662+
np.testing.assert_allclose(bopdmd_econ.modes, bopdmd.modes)

tutorials/user-manual1/user-manual-bopdmd.ipynb

Lines changed: 49 additions & 3 deletions
Large diffs are not rendered by default.

tutorials/user-manual1/user-manual-bopdmd.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,25 @@
9595
flip_continuous_axes=True, # Rotate the continuous-time eig plot.
9696
)
9797

98+
# Alternatively, pre-compute the SVD and use the `fit_econ` function.
99+
# This is useful when the SVD is already computed and the snapshots matrix
100+
# X is not available or is too large to store in memory.
101+
U, s, V = np.linalg.svd(X, full_matrices=False)
102+
U = U[:, :11]
103+
s = s[:11]
104+
V = V[:11, :]
105+
106+
bopdmd_econ = BOPDMD(svd_rank=11, num_trials=0, use_proj=True, proj_basis=U)
107+
bopdmd_econ.fit_econ(s, V, t)
108+
109+
plot_summary(
110+
bopdmd_econ,
111+
figsize=(12, 6), # Figure size.
112+
index_modes=(0, 1, 3), # Indices of the modes to plot.
113+
snapshots_shape=(ny, nx), # Shape of the modes.
114+
order="F", # Order to use when reshaping the modes.
115+
flip_continuous_axes=True, # Rotate the continuous-time eig plot.
116+
)
98117

99118
# In[4]:
100119

0 commit comments

Comments
 (0)