forked from PyDMD/PyDMD
-
Notifications
You must be signed in to change notification settings - Fork 1
/
developers-help-1.py
266 lines (181 loc) · 9.38 KB
/
developers-help-1.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
#!/usr/bin/env python
# coding: utf-8
# # PyDMD
# ## Developers Tutorial 1: Extending PyDMD
# Contrary to other **PyDMD** tutorials, this notebook illustrates the general structure of the basic classes, aiming to help developers to <u>extend</u> the package functionality in a smooth way.
#
# In this tutorial we will indeed create from scratch a new version of the original DMD; such extension will be totally useless from a scientific perspective, the only purpose is showing the suggested steps to implement new variants inside **PyDMD**.
# ## The Back-end API
# The necessary bricks for building the new DMD version are:
#
# - [`DMDBase`](https://pydmd.github.io/PyDMD/dmdbase.html), the actual backbone of all the different implemented versions;
# - [`DMDTimeDict`](https://github.com/PyDMD/PyDMD/blob/7ac9f3fa855e9a5e7008daad9906eaa6e59ba80a/pydmd/dmdbase.py#L759), the class that manages the time window;
# - [`DMDOperator`](https://pydmd.github.io/PyDMD/dmdoperator.html), the class that manages the so-called DMD operator;
#
# The following schematic outlines the general structure of every `DMDBase` module.
# ![](PyDMD-structure.png)
# In general, all `PyDMD` modules should be capable of the following tasks:
# 1) Accepting and storing DMD algorithm parameters.
# 2) Performing DMD given snapshot data $\mathbf{X}$ via the `fit` method, which often does the following:
# - prepares and stores the input data,
# - sets the `DMDTimeDict`s,
# - computes the DMD operator and its eigendecomposition, and
# - (this is done by invoking the `DMDOperator`'s `compute_operator` function)
# - computes the DMD amplitudes using the computed operator.
# 3) Fetching and utilizing DMD results, such as:
# - the reduced DMD operator $\tilde{\mathbf{A}}$
# - the spatial modes $\mathbf{\Phi}$ (the eigenvectors of $\mathbf{A}$)
# - the temporal frequencies $\mathbf{\Lambda}$ (the eigenvalues of $\mathbf{A}$)
# - the spatiotemporal mode amplitudes $\mathbf{b}$
#
# Hence different modules implement different DMD variants, where any number of these steps may be performed differently.
# ## Building a New PyDMD Module
#
# We start the new module by importing all these classes and the usual math environment (`matplotlib`+`numpy`).
# In[1]:
get_ipython().run_line_magic("matplotlib", "inline")
import matplotlib.pyplot as plt
import numpy as np
from pydmd import DMDBase, DMD
from pydmd.utils import compute_tlsq
from pydmd.snapshots import Snapshots
import matplotlib.colors as colors
# We are now able to create a new class inheriting from `DMDBase`: in this way we just need to implement the new `fit()` method. We can also overload one or more attributes/methods, depending on the algorithm which we are going to follow.
#
# In this case, we also overload the constructor (i.e. the `__init__` method) to present a non-trivial case.
#
# ### Our simple algorithm
# The new functionality we are going to implement is basically a simple DMD which maps the input snapshots using a custom function, provided by the user before of the construction of the operator.
#
# Since the (quite) high number of parameters in the constructor, we suggest to pass explicitly all of them instead of using `args` and `kwargs`. It's redundant, but also much more readable! We can call the `DMDBase` constructor thanks to `super` keyword (more info on `super` [here](https://docs.python.org/3.9/library/functions.html?highlight=super#super)).
# In[2]:
class MyFancyNameDMD(DMDBase):
"""
The MyFancyNameDMD class.
A dummy DMD variant which, useless outside this tutorial.
Make sure to write an exhaustive docstring if you want to
distribute your code, or use it again in some years.
"""
def __init__(self, func, **kwargs):
super().__init__(**kwargs)
self.__func = func
# We just specify that the last line of constructor saves the passed function in the object, giving us the possibility to recall it later.
#
# We are now ready to implement the `fit` method. Define the custom mapping as $f: \mathbb R^n \to \mathbb R^n$ acting on the snapshots $\{\mathbf x_i \in \mathbb R^n\}_{i=1}^m$. Then we just need to pre-process the snapshots in `fit()` and build the DMD operator.
# In[3]:
def fit(self, X):
"""
Compute the DMD to the input data.
:param X: the input snapshots.
:type X: numpy.ndarray or iterable
"""
# adjust the shape of the snapshots
input_snapshots = Snapshots(X).snapshots
# apply the mapping
mapped_snapshots = np.apply_along_axis(self.__func, 0, input_snapshots)
# store the snapshots
self._snapshots_holder = Snapshots(mapped_snapshots)
# build the matrices X and Y
n_samples = self.snapshots.shape[-1]
X = self.snapshots[:, :-1]
Y = self.snapshots[:, 1:]
X, Y = compute_tlsq(X, Y, self._tlsq_rank)
# compute the DMD operator
self._svd_modes, _, _ = self.operator.compute_operator(X, Y)
# Default timesteps
self._set_initial_time_dictionary({"t0": 0, "tend": n_samples - 1, "dt": 1})
# compute DMD amplitudes
self._b = self._compute_amplitudes()
return self
# By recalling the already implemented methods, we just need few lines of code to complete the new version. Of course, in case of doubts, the [documentation](https://mathlab.github.io/PyDMD/index.html) is the best starting point to better understand classes and methods.
#
# We merge the two cells above in the next one in order to have the entire class implemented.
# In[4]:
class MyFancyNameDMD(DMDBase):
"""
The MyFancyNameDMD class.
A dummy DMD variant which, useless outside this tutorial.
Make sure to write an exhaustive docstring if you want to
distribute your code, or use it again in some years.
"""
def __init__(self, func, **kwargs):
super().__init__(**kwargs)
self.__func = func
def fit(self, X):
"""
Compute the DMD to the input data.
:param X: the input snapshots.
:type X: numpy.ndarray or iterable
"""
# adjust the shape of the snapshots
input_snapshots = Snapshots(X).snapshots
# apply the mapping
mapped_snapshots = np.apply_along_axis(self.__func, 0, input_snapshots)
# store the snapshots
self._snapshots_holder = Snapshots(mapped_snapshots)
# build the matrices X and Y
n_samples = self.snapshots.shape[-1]
X = self.snapshots[:, :-1]
Y = self.snapshots[:, 1:]
X, Y = compute_tlsq(X, Y, self._tlsq_rank)
# compute the DMD operator
self._svd_modes, _, _ = self.operator.compute_operator(X, Y)
# Default timesteps
self._set_initial_time_dictionary(
{"t0": 0, "tend": n_samples - 1, "dt": 1}
)
# compute DMD amplitudes
self._b = self._compute_amplitudes()
return self
# And, we're done!
#
# Our new class is implemented, inheriting all the methods to the base class. We have nothing to do but try it on a simple example. First of all, we instantiate a new object, passing as function a simple normalizer.
# In[5]:
def myfunc(snapshot):
return snapshot + np.mean(snapshot) * np.random.rand(len(snapshot))
my_dmd = MyFancyNameDMD(func=myfunc)
# We recycle the dataset from [tutorial 1](https://mathlab.github.io/PyDMD/tutorial1dmd.html):
# In[6]:
def f1(x, t):
return 1.0 / np.cosh(x + 3) * np.exp(2.3j * t)
def f2(x, t):
return 2.0 / np.cosh(x) * np.tanh(x) * np.exp(2.8j * t)
x = np.linspace(-5, 5, 65)
t = np.linspace(0, 4 * np.pi, 129)
xgrid, tgrid = np.meshgrid(x, t)
X1 = f1(xgrid, tgrid)
X2 = f2(xgrid, tgrid)
X = X1 + X2
# The `fit` method we have implemented is now called to perform our algorithm to the input data.
# In[7]:
my_dmd.fit(X.T)
# To check that everything works as expected we compare the results obtained with the new version with respect to the original standard approach. We import the `DMD` class and fit over the same data.
# In[8]:
dmd = DMD()
dmd.fit(X.T)
# We have performed both the algorithms, we can now look at the reconstructed system using the `reconstructed_data` attribute. We note here that, as the other methods and attributes, `reconstructed_data` is inherited from `DMDBase`, giving us the possibility to call it without having implemented explicitly.
# In[9]:
plt.figure(figsize=(18, 6))
plt.subplot(1, 2, 1)
plt.pcolormesh(my_dmd.reconstructed_data.real)
plt.title("MyFancyNameDMD")
plt.xlabel("t")
plt.ylabel("x")
plt.colorbar()
plt.subplot(1, 2, 2)
Z = np.abs(my_dmd.reconstructed_data.real - dmd.reconstructed_data.real)
pcm = plt.pcolormesh(
Z,
norm=colors.LogNorm(vmin=max(1.0e-16, Z.min()), vmax=max(1.0e-15, Z.max())),
)
plt.title("|MyFancyNameDMD - DMD|")
plt.xlabel("t")
plt.ylabel("x")
plt.colorbar(pcm, extend="max")
plt.show()
# Reconstruction looks fine! We are able to note that, since the normalization of the snapshots, the two approaches returns similar results, just a bit rescaled, that is what we expected by preprocessing in this way the input data.
#
# But what about if we want to rescale back the reconstruction (or the modes) in the new version?
# Simple, we have just to overload the `reconstructed_data` or `modes` attributes.
#
# This is the basic guide to implement a new version of DMD. For more complex changes to the algorithm, we recommend to carefully read the documentation of the package, and maybe reporting in the [Github issues page](https://github.com/mathLab/PyDMD/issues) the extension you are going to implement.