Skip to content

Commit a7c6769

Browse files
authored
export tutorials changed in 7bac7aa
1 parent 7bac7aa commit a7c6769

File tree

2 files changed

+218
-194
lines changed

2 files changed

+218
-194
lines changed

docs/source/_tutorials/tutorial1dmd.html

+141-134
Large diffs are not rendered by default.

tutorials/tutorial1/tutorial-1-dmd.py

+77-60
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
from pydmd import DMD, BOPDMD
2323
from pydmd.plotter import plot_eigs, plot_summary
24-
from pydmd.preprocessing.hankel import hankel_preprocessing
24+
from pydmd.preprocessing import hankel_preprocessing
2525

2626

2727
# We create the input data by summing two different functions:<br>
@@ -77,7 +77,7 @@ def f2(x, t):
7777
plt.title(title)
7878
plt.xlabel("Space")
7979
plt.ylabel("Time")
80-
plt.colorbar()
80+
plt.colorbar()
8181
plt.show()
8282

8383

@@ -96,47 +96,79 @@ def f2(x, t):
9696
plt.title(title)
9797
plt.xlabel("Space")
9898
plt.ylabel("Time")
99-
plt.colorbar()
99+
plt.colorbar()
100+
plt.show()
101+
102+
103+
# ## DMD with perfect data (i.e. clean simulation data)
104+
#
105+
# **We begin by presenting the following results so that we know what we should be expecting when we apply DMD to this data set. We will go more in depth about the individual steps on how to use PyDMD later.**
106+
#
107+
# Notice that by construction, our data set is completely real (i.e. it doesn't possess imaginary components) and it contains 2 distinct spatiotemporal features that oscillate in time. Hence a successful DMD model should not only be able to faithfully reconstruct the input data, but it should also be able to disambiguate the 2 spatial modes, as well as recover their respective frequencies of oscillation $\omega_1=2.3$ and $\omega_2=2.8$.
108+
#
109+
# To capture such oscillations from real data sets, we will need 2 DMD eigenvalues for each oscillation: one to capture the frequency of the oscillation and one to capture its complex conjugate. Hence for our particular data set, we need at least 4 DMD eigenvalues / modes in order to capture the full extent of our data. We will also need time-delay data preprocessing in order to recover this rank-4 structure, but more on that later.
110+
#
111+
# Since our data is evenly-spaced in time and sufficiently high-dimensional after we appropriately preprocess our data, exact DMD (implemented by `DMD`) is able to successfully extract the 2 spatiotemporal modes that make up our system as long as we use clean data.
112+
#
113+
# **The results presented below are essentially perfect results.**
114+
115+
# In[5]:
116+
117+
118+
d = 2 # we will use this number of delays throughout the tutorial
119+
dmd = DMD(svd_rank=4)
120+
delay_dmd = hankel_preprocessing(dmd, d=d)
121+
delay_dmd.fit(X.T)
122+
plot_summary(delay_dmd, x=x, t=dt, d=d)
123+
print(
124+
f"Frequencies (imaginary component): {np.round(np.log(delay_dmd.eigs) / dt, decimals=12)}"
125+
)
126+
plt.title("Reconstructed Data")
127+
plt.imshow(delay_dmd.reconstructed_data.real)
128+
plt.show()
129+
plt.title("Clean Ground Truth Data")
130+
plt.imshow(X.T)
100131
plt.show()
101132

102133

103134
# ## DMD steps for handling real data (i.e. data with noise)
104135
#
105-
# Step 1: Do a time-delay embedding (`d` is number of delay embeddings).
136+
# **Step 1:** Do a time-delay embedding (`d` is number of delay embeddings).
106137
#
107-
# Step 2: Apply BOP-DMD (`num_trials` is number of statistical bags).
138+
# **Step 2:** Apply BOP-DMD (`num_trials` is number of statistical bags).
108139
#
109-
# Step 3: OPTIONAL -- Constrain the eigenvalues (i) left-half plane, (ii) imaginary axis, (iii) complex conjugate pairs.
140+
# **Step 3:** OPTIONAL -- Constrain the eigenvalues (i) left-half plane, (ii) imaginary axis, (iii) complex conjugate pairs.
110141

111-
# ## Steps 1 and 2:
142+
# ## Steps 1 and 2: Using PyDMD on real data
112143
#
113144
# We currently have the temporal snapshots in the input matrix rows. We can easily create a new DMD instance and exploit it in order to compute DMD on the data. Since the snapshots must be arranged by columns, we need to transpose the data matrix in this case.
114145
#
115-
# Starting with Step 1, we apply a time-delay embedding to our data before applying our DMD method of choice. In order to do that, we wrap our DMD instance in the `hankel_preprocessing` routine and provide our desired number of delays `d`. We will dive more into *why* we need the time-delay embedding later in the tutorial.
146+
# Starting with **Step 1**, we apply a time-delay embedding to our data before applying our DMD method of choice. In order to do that, we wrap our DMD instance in the `hankel_preprocessing` routine and provide our desired number of delays `d`. We will dive more into *why* we need the time-delay embeddings later in the tutorial.
116147
#
117-
# Continuing on to Step 2, we note that in order to apply the BOP-DMD method in particular, all we need to do is build `BOPDMD` model as our particular DMD instance. Once the instance is wrapped, we can go ahead with the fit.
148+
# Continuing on to **Step 2**, we note that in order to apply the BOP-DMD method in particular, all we need to do is build `BOPDMD` model as our particular DMD instance. Once the instance is wrapped, we can go ahead with the fit.
118149
#
119150
# A summary of the DMD results can then be plotted using the `plot_summary` function.
151+
#
152+
# Notice that from this process alone, we are able to obtain fairly accurate spatial modes, a good approximation of the temporal frequencies, and a good reconstruction of our data, even in the presence of high amounts of noise.
120153

121-
# In[5]:
154+
# In[6]:
122155

123156

124157
# Build the Optimized DMD model.
125158
# num_trials=0 gives Optimized DMD, without bagging.
126159
optdmd = BOPDMD(svd_rank=4, num_trials=0)
127160

128161
# Wrap the model with the preprocessing routine.
129-
delays = 2
130-
delay_optdmd = hankel_preprocessing(optdmd, d=delays)
162+
delay_optdmd = hankel_preprocessing(optdmd, d=d)
131163

132164
# Fit the model to the noisy data.
133165
# Note: BOPDMD models need the data X and the times of data collection t for fitting.
134166
# Hence if we apply time-delay, we must adjust the length of our time vector accordingly.
135-
num_t = len(t) - delays + 1
136-
delay_optdmd.fit(Xn.T, t=t[:num_t])
167+
delay_t = t[: -d + 1]
168+
delay_optdmd.fit(Xn.T, t=delay_t)
137169

138170
# Plot a summary of the DMD results.
139-
plot_summary(delay_optdmd, d=delays)
171+
plot_summary(delay_optdmd, x=x, d=d)
140172

141173
# Print computed eigenvalues (frequencies are given by imaginary components).
142174
# Also plot the resulting data reconstruction.
@@ -146,7 +178,7 @@ def f2(x, t):
146178
plt.title("Reconstructed Data")
147179
plt.imshow(delay_optdmd.reconstructed_data.real)
148180
plt.show()
149-
plt.title("Ground Truth Data")
181+
plt.title("Clean Ground Truth Data")
150182
plt.imshow(X.T)
151183
plt.show()
152184

@@ -160,7 +192,7 @@ def f2(x, t):
160192
#
161193
# Although these attributes may be accessed directly from a fitted DMD object as demonstrated below, we note that the `plot_summary` function plots a summarizing view of many of these attributes automatically.
162194

163-
# In[6]:
195+
# In[7]:
164196

165197

166198
colors = ["tab:blue", "tab:orange", "tab:green", "tab:red"]
@@ -169,7 +201,7 @@ def f2(x, t):
169201
plt.figure(figsize=(14, 3))
170202
for i, mode in enumerate(delay_optdmd.modes.T):
171203
# Get the average across delays, since we used time-delay.
172-
mode = np.average(mode.reshape(delays, len(mode) // delays), axis=0)
204+
mode = np.average(mode.reshape(d, len(mode) // d), axis=0)
173205
plt.subplot(1, len(delay_optdmd.modes.T), i + 1)
174206
plt.plot(mode.real, c=colors[i])
175207
plt.title(f"Mode {i + 1}")
@@ -180,7 +212,7 @@ def f2(x, t):
180212
plt.figure(figsize=(14, 3))
181213
for i, dynamic in enumerate(delay_optdmd.dynamics):
182214
plt.subplot(1, len(delay_optdmd.dynamics), i + 1)
183-
plt.plot(t[:num_t], dynamic.real, c=colors[i])
215+
plt.plot(delay_t, dynamic.real, c=colors[i])
184216
plt.title(f"Dynamics {i + 1}")
185217
plt.tight_layout()
186218
plt.show()
@@ -196,13 +228,15 @@ def f2(x, t):
196228
#
197229
# `BOPDMD` models also have the option to specify the structure of the eigenvalues that they compute. More specifically, users can impose the following constraints, as well as any valid combination of them.
198230
#
199-
# - Stable: constrain eigenvalues to have non-positive real parts.
200-
# - Imaginary: constrain eigenvalues to be purely imaginary.
201-
# - Conjugate pairs: constrain eigenvalues to always appear with their complex conjugate.
231+
# - **Stable:** constrain eigenvalues to have non-positive real parts.
232+
# - **Imaginary:** constrain eigenvalues to be purely imaginary.
233+
# - **Conjugate pairs:** constrain eigenvalues to always appear with their complex conjugate.
202234
#
203235
# This can be especially helpful for dealing with noise and preventing growth/decay of your dynamics.
236+
#
237+
# Notice that by taking this extra step, the eigenvalues computed by BOP-DMD are now perfectly imaginary, and they now come in perfect complex conjugate pairs. Also notice that this is automatically detected by `plot_summary`, which now plots the complex conjugate eigenvalue pairs and their respective mode in the same color.
204238

205-
# In[7]:
239+
# In[8]:
206240

207241

208242
# CONSTRAINTS
@@ -222,51 +256,28 @@ def f2(x, t):
222256
optdmd = BOPDMD(
223257
svd_rank=4, num_trials=0, eig_constraints={"imag", "conjugate_pairs"}
224258
)
225-
delay_optdmd = hankel_preprocessing(optdmd, d=delays)
226-
delay_optdmd.fit(Xn.T, t=t[:num_t])
227-
plot_summary(delay_optdmd, d=delays)
259+
delay_optdmd = hankel_preprocessing(optdmd, d=d)
260+
delay_optdmd.fit(Xn.T, t=delay_t)
261+
plot_summary(delay_optdmd, x=x, d=d)
228262

229263
print(
230264
f"Frequencies (imaginary component): {np.round(delay_optdmd.eigs, decimals=3)}"
231265
)
232266
plt.title("Reconstructed Data")
233267
plt.imshow(delay_optdmd.reconstructed_data.real)
234268
plt.show()
235-
plt.title("Ground Truth Data")
236-
plt.imshow(X.T)
237-
plt.show()
238-
239-
240-
# ## Why do we use BOP-DMD?
241-
#
242-
# Put simply, **BOP-DMD is extremely robust to measurement noise, hence making it the preferred method when dealing with real-world data.** By contrast, the results of exact DMD (which is implemented by the `DMD` module) are extremely sensitive to measurement noise, as we demonstrate here. Note the decay of the dynamics onset by the bias in the eigenvalues. Also note how when we previously performed this fit but with BOP-DMD instead, we did not observe such decay, but rather we recovered the true oscillations.
243-
244-
# ### This is what happens when we use exact DMD instead of BOP-DMD:
245-
246-
# In[8]:
247-
248-
249-
dmd = DMD(svd_rank=4)
250-
delay_dmd = hankel_preprocessing(dmd, d=delays)
251-
delay_dmd.fit(Xn.T)
252-
plot_summary(delay_dmd, d=delays)
253-
254-
print(
255-
f"Frequencies (imaginary component): {np.round(np.log(delay_dmd.eigs) / dt, decimals=3)}"
256-
)
257-
plt.title("Reconstructed Data")
258-
plt.imshow(delay_dmd.reconstructed_data.real)
259-
plt.show()
260-
plt.title("Ground Truth Data")
269+
plt.title("Clean Ground Truth Data")
261270
plt.imshow(X.T)
262271
plt.show()
263272

264273

265274
# ## Why do we need time-delay?
266275
#
267-
# Notice that by construction, our data set is completely real (i.e. it doesn't possess imaginary components) and it contains 2 distinct spatiotemporal features that oscillate in time. To capture such oscillations from real data sets, we need 2 DMD eigenvalues for each oscillation: one to capture the frequency of the oscillation and one to capture its complex conjugate. Hence for our particular data set, we need at least 4 DMD eigenvalues / modes in order to capture the full extent of our data. You may have noticed this as we consistently used `svd_rank=4`.
276+
# **Because our data is real *and* because the underlying spatial modes are stationary, we cannot always obtain correct results if we apply DMD directly to our data set, even if we use the proper rank truncation.**
277+
#
278+
# Time-delay helps mitigate this by giving us more observations to work with. As you will see below, our clean data reveals 2 dominant singular values, and if we try to apply DMD without time-delay, we obtain nonsensical results.
268279
#
269-
# However, **because our data is real *and* because the underlying spatial modes are stationary, we cannot always obtain correct results if we apply DMD directly to our data set, even if we use the proper rank truncation.** Time-delay helps mitigate this by giving us more observations to work with. As you will see below, our clean data reveals 2 dominant singular values, but applying any number of time-delay embeddings will lift this number of singular values from 2 to 4, hence allowing us to more-consistently extract the rank-4 structure that we would expect. This is also why we use `d=2` -- any number of delays greater than 1 suffices.
280+
# However, if we apply any number of time-delay embeddings, it will lift this number of singular values from 2 to 4, hence allowing us to more consistently extract the rank-4 structure that we expect. This is why we use `d=2` throughout this tutorial. Any number of delays greater than 1 suffice, granted we stil have enough temporal snapshots.
270281
#
271282
# Note that this preprocessing step may or may not be necessary depending on your particular data set. Hence the most practical thing to do during any DMD application is to **examine the singular value spectrum of you data as you apply time-delay embeddings.**
272283

@@ -285,28 +296,34 @@ def f2(x, t):
285296
plt.title("Reconstructed Data")
286297
plt.imshow(dmd.reconstructed_data.real)
287298
plt.show()
288-
plt.title("Ground Truth Data")
299+
plt.title("Clean Ground Truth Data")
289300
plt.imshow(X.T)
290301
plt.show()
291302

292303

293-
# ### This is what happens with time-delay (using clean data and exact DMD):
304+
# ## Why do we use BOP-DMD?
305+
#
306+
# **BOP-DMD is extremely robust to measurement noise, which is why it the preferred method when dealing with real-world data.** By contrast, the results of exact DMD are extremely sensitive to measurement noise.
307+
#
308+
# Below, we demonstrate what might happen if you apply exact DMD to non-perfect data. Note the decay of the dynamics onset by the bias in the eigenvalues. Also notice how when we previously performed this fit but with BOP-DMD instead, we did not observe such decay, but rather we recovered the expected oscillatory dynamics.
309+
310+
# ### This is what happens when we use exact DMD instead of BOP-DMD:
294311

295312
# In[10]:
296313

297314

298315
dmd = DMD(svd_rank=4)
299-
delay_dmd = hankel_preprocessing(dmd, d=2)
300-
delay_dmd.fit(X.T)
301-
plot_summary(delay_dmd, d=2)
316+
delay_dmd = hankel_preprocessing(dmd, d=d)
317+
delay_dmd.fit(Xn.T)
318+
plot_summary(delay_dmd, x=x, d=d)
302319

303320
print(
304321
f"Frequencies (imaginary component): {np.round(np.log(delay_dmd.eigs) / dt, decimals=3)}"
305322
)
306323
plt.title("Reconstructed Data")
307324
plt.imshow(delay_dmd.reconstructed_data.real)
308325
plt.show()
309-
plt.title("Ground Truth Data")
326+
plt.title("Clean Ground Truth Data")
310327
plt.imshow(X.T)
311328
plt.show()
312329

0 commit comments

Comments
 (0)