modulo-vki 2.0.6__py3-none-any.whl → 2.0.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- modulo_vki/__init__.py +22 -22
- modulo_vki/core/__init__.py +9 -9
- modulo_vki/core/_dft.py +61 -61
- modulo_vki/core/_dmd_s.py +72 -72
- modulo_vki/core/_k_matrix.py +81 -81
- modulo_vki/core/_mpod_space.py +180 -180
- modulo_vki/core/_mpod_time.py +154 -154
- modulo_vki/core/_pod_space.py +184 -184
- modulo_vki/core/_pod_time.py +48 -48
- modulo_vki/core/_spod_s.py +101 -101
- modulo_vki/core/_spod_t.py +104 -104
- modulo_vki/modulo.py +828 -828
- modulo_vki/utils/__init__.py +4 -4
- modulo_vki/utils/_plots.py +51 -51
- modulo_vki/utils/_utils.py +341 -341
- modulo_vki/utils/others.py +452 -452
- modulo_vki/utils/read_db.py +339 -339
- {modulo_vki-2.0.6.dist-info → modulo_vki-2.0.7.dist-info}/LICENSE +21 -21
- {modulo_vki-2.0.6.dist-info → modulo_vki-2.0.7.dist-info}/METADATA +304 -304
- modulo_vki-2.0.7.dist-info/RECORD +22 -0
- modulo_vki-2.0.6.dist-info/RECORD +0 -22
- {modulo_vki-2.0.6.dist-info → modulo_vki-2.0.7.dist-info}/WHEEL +0 -0
- {modulo_vki-2.0.6.dist-info → modulo_vki-2.0.7.dist-info}/top_level.txt +0 -0
modulo_vki/modulo.py
CHANGED
|
@@ -1,828 +1,828 @@
|
|
|
1
|
-
# Functional ones:
|
|
2
|
-
import os
|
|
3
|
-
import numpy as np
|
|
4
|
-
from scipy import linalg
|
|
5
|
-
from sklearn.metrics.pairwise import pairwise_kernels
|
|
6
|
-
# To have fancy loading bar
|
|
7
|
-
from tqdm import tqdm
|
|
8
|
-
|
|
9
|
-
# All the functions from the modulo package
|
|
10
|
-
from modulo_vki.core._dft import dft_fit
|
|
11
|
-
from modulo_vki.core._dmd_s import dmd_s
|
|
12
|
-
from modulo_vki.core._k_matrix import CorrelationMatrix
|
|
13
|
-
from modulo_vki.core._mpod_space import spatial_basis_mPOD
|
|
14
|
-
from modulo_vki.core._mpod_time import temporal_basis_mPOD
|
|
15
|
-
from modulo_vki.core._pod_space import Spatial_basis_POD
|
|
16
|
-
from modulo_vki.core._pod_time import Temporal_basis_POD
|
|
17
|
-
from modulo_vki.core._spod_s import compute_SPOD_s
|
|
18
|
-
from modulo_vki.core._spod_t import compute_SPOD_t
|
|
19
|
-
from modulo_vki.utils._utils import switch_svds
|
|
20
|
-
|
|
21
|
-
from modulo_vki.utils.read_db import ReadData
|
|
22
|
-
|
|
23
|
-
class ModuloVKI:
|
|
24
|
-
"""
|
|
25
|
-
MODULO (MODal mULtiscale pOd) is a software developed at the von Karman Institute to perform Multiscale
|
|
26
|
-
Modal Analysis of numerical and experimental data using the Multiscale Proper Orthogonal Decomposition (mPOD).
|
|
27
|
-
|
|
28
|
-
Theoretical foundation can be found at:
|
|
29
|
-
https://arxiv.org/abs/1804.09646
|
|
30
|
-
|
|
31
|
-
Presentation of the MODULO framework available here:
|
|
32
|
-
https://arxiv.org/pdf/2004.12123.pdf
|
|
33
|
-
|
|
34
|
-
YouTube channel with hands-on tutorials can be found at:
|
|
35
|
-
https://youtube.com/playlist?list=PLEJZLD0-4PeKW6Ze984q08bNz28GTntkR
|
|
36
|
-
|
|
37
|
-
All the codes so far assume that the dataset is equally spaced both in space (i.e. along a Cartesian grid)
|
|
38
|
-
and in time. The extension to non-uniformly sampled data will be included in future releases.
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
"""
|
|
42
|
-
|
|
43
|
-
def __init__(self, data: np.array,
|
|
44
|
-
N_PARTITIONS: int = 1,
|
|
45
|
-
FOLDER_OUT='./',
|
|
46
|
-
SAVE_K: bool = False,
|
|
47
|
-
N_T: int = 100,
|
|
48
|
-
N_S: int = 200,
|
|
49
|
-
n_Modes: int = 10,
|
|
50
|
-
dtype: str = 'float32',
|
|
51
|
-
eig_solver: str = 'eigh',
|
|
52
|
-
svd_solver: str = 'svd_sklearn_truncated',
|
|
53
|
-
weights: np.array = np.array([])):
|
|
54
|
-
"""
|
|
55
|
-
This function initializes the main parameters needed by MODULO.
|
|
56
|
-
|
|
57
|
-
Attributes:
|
|
58
|
-
|
|
59
|
-
:param data: This is the data matrix to factorize. It is a np.array with
|
|
60
|
-
shape ((N_S, N_T)). If the data has not yet been prepared in the form of a np.array,
|
|
61
|
-
the method ReadData in MODULO can be used (see ReadData). If the memory saving is active (N_PARTITIONS >1), the folder with partitions should be prepared.
|
|
62
|
-
If the memory saving is active, this entry = None. The data matrix is assumed to big to be saved and the
|
|
63
|
-
|
|
64
|
-
:param N_PARTITIONS: If memory saving feature is active, this parameter sets the number of partitions
|
|
65
|
-
that will be used to store the data matrices during the computations.
|
|
66
|
-
|
|
67
|
-
:param FOLDER_OUT: Folder in which the output will be stored.The output includes the matrices Phi, Sigma and Psi (optional) and temporary files
|
|
68
|
-
used for some of the calculations (e.g.: for memory saving).
|
|
69
|
-
|
|
70
|
-
:param SAVE_K: A flag deciding if the matrix will be stored in the disk (in FOLDER_OUT/correlation_matrix) or not.
|
|
71
|
-
Default option is 'False'.
|
|
72
|
-
|
|
73
|
-
:param N_T: Number of time steps, must be given when N_PARTITIONS >1
|
|
74
|
-
|
|
75
|
-
:param N_S: Number of grid points, must be given when N_PARTITIONS >1
|
|
76
|
-
|
|
77
|
-
:param n_Modes: Number of Modes to be computed
|
|
78
|
-
|
|
79
|
-
:param dtype: Cast "data" with type dtype
|
|
80
|
-
|
|
81
|
-
:param eig_solver: Numerical solver to compute the eigen values
|
|
82
|
-
|
|
83
|
-
:param svd_solver: Numerical solver to compute the Single Value Decomposition
|
|
84
|
-
|
|
85
|
-
:param weights: weight vector [w_i,....,w_{N_s}] where w_i = area_cell_i/area_grid
|
|
86
|
-
Only needed if grid is non-uniform.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
"""
|
|
90
|
-
|
|
91
|
-
print("MODULO (MODal mULtiscale pOd) is a software developed at the von Karman Institute to perform "
|
|
92
|
-
"data driven modal decomposition of numerical and experimental data. \n")
|
|
93
|
-
|
|
94
|
-
if not isinstance(data, np.ndarray) and N_PARTITIONS == 1:
|
|
95
|
-
raise TypeError(
|
|
96
|
-
"Please check that your database is in an numpy array format. If D=None, then you must have memory saving (N_PARTITIONS>1)")
|
|
97
|
-
|
|
98
|
-
# Load the data matrix
|
|
99
|
-
if isinstance(data, np.ndarray):
|
|
100
|
-
# Number of points in time and space
|
|
101
|
-
self.N_T = data.shape[1]
|
|
102
|
-
self.N_S = data.shape[0]
|
|
103
|
-
# Check the data type
|
|
104
|
-
self.D = data.astype(dtype)
|
|
105
|
-
else:
|
|
106
|
-
self.D = None # D is never saved when N_partitions >1
|
|
107
|
-
self.N_S = N_S # so N_S and N_t must be given as parameters of modulo
|
|
108
|
-
self.N_T = N_T
|
|
109
|
-
|
|
110
|
-
# Load and applied the weights to the D matrix
|
|
111
|
-
if weights.size != 0:
|
|
112
|
-
if len(weights) == self.N_S:
|
|
113
|
-
print("The weights you have input have the size of the columns of D \n"
|
|
114
|
-
"MODULO has considered that you have already duplicated the dimensions of the weights "
|
|
115
|
-
"to match the dimensions of the D columns \n")
|
|
116
|
-
self.weights = weights
|
|
117
|
-
elif 2 * len(weights) == self.N_S: # 2D computation only
|
|
118
|
-
self.weights = np.concatenate((weights, weights))
|
|
119
|
-
print("Modulo assumes you have a 2D domain and has duplicated the weight "
|
|
120
|
-
"array to match the size of the D columns \n")
|
|
121
|
-
print(weights)
|
|
122
|
-
else:
|
|
123
|
-
raise AttributeError("Make sure the size of the weight array is twice smaller than the size of D")
|
|
124
|
-
# Dstar is used to compute the K matrix
|
|
125
|
-
if isinstance(data, np.ndarray):
|
|
126
|
-
# Apply the weights only if D exist.
|
|
127
|
-
# If not (i.e. N_partitions >1), weights are applied in _k_matrix.py when loading partitions of D
|
|
128
|
-
self.Dstar = np.transpose(np.transpose(self.D) * np.sqrt(self.weights))
|
|
129
|
-
else:
|
|
130
|
-
self.Dstar = None
|
|
131
|
-
else:
|
|
132
|
-
print("Modulo assumes you have a uniform grid. "
|
|
133
|
-
"If not, please give the weights as parameters of MODULO!")
|
|
134
|
-
self.weights = weights
|
|
135
|
-
self.Dstar = self.D
|
|
136
|
-
|
|
137
|
-
if N_PARTITIONS > 1:
|
|
138
|
-
self.MEMORY_SAVING = True
|
|
139
|
-
else:
|
|
140
|
-
self.MEMORY_SAVING = False
|
|
141
|
-
|
|
142
|
-
# Assign the number of modes
|
|
143
|
-
self.n_Modes = n_Modes
|
|
144
|
-
# If particular needs, override choice for svd and eigen solve
|
|
145
|
-
self.svd_solver = svd_solver.lower()
|
|
146
|
-
self.eig_solver = eig_solver.lower()
|
|
147
|
-
possible_svds = ['svd_numpy', 'svd_scipy_sparse', 'svd_sklearn_randomized', 'svd_sklearn_truncated']
|
|
148
|
-
possible_eigs = ['svd_sklearn_randomized', 'eigsh', 'eigh']
|
|
149
|
-
|
|
150
|
-
if self.svd_solver not in possible_svds:
|
|
151
|
-
raise NotImplementedError("The requested SVD solver is not implemented. Please pick one of the following:"
|
|
152
|
-
"which belongs to: \n {}".format(possible_svds))
|
|
153
|
-
|
|
154
|
-
if self.eig_solver not in possible_eigs:
|
|
155
|
-
raise NotImplementedError("The requested EIG solver is not implemented. Please pick one of the following: "
|
|
156
|
-
" \n {}".format(possible_eigs))
|
|
157
|
-
|
|
158
|
-
# if N_PARTITIONS >= self.N_T:
|
|
159
|
-
# raise AttributeError("The number of requested partitions is greater of the total columns (N_T). Please,"
|
|
160
|
-
# "try again.")
|
|
161
|
-
|
|
162
|
-
self.N_PARTITIONS = N_PARTITIONS
|
|
163
|
-
|
|
164
|
-
self.FOLDER_OUT = FOLDER_OUT
|
|
165
|
-
|
|
166
|
-
self.SAVE_K = SAVE_K
|
|
167
|
-
|
|
168
|
-
if self.MEMORY_SAVING:
|
|
169
|
-
os.makedirs(self.FOLDER_OUT, exist_ok=True)
|
|
170
|
-
|
|
171
|
-
def _temporal_basis_POD(self,
|
|
172
|
-
SAVE_T_POD: bool = False):
|
|
173
|
-
"""
|
|
174
|
-
This method computes the temporal structure for the Proper Orthogonal Decomposition (POD) computation.
|
|
175
|
-
The theoretical background of the POD is briefly recalled here:
|
|
176
|
-
|
|
177
|
-
https://youtu.be/8fhupzhAR_M
|
|
178
|
-
|
|
179
|
-
The diagonalization of K is computed via Singular Value Decomposition (SVD).
|
|
180
|
-
A speedup is available if the user is on Linux machine, in which case MODULO
|
|
181
|
-
exploits the power of JAX and its Numpy implementation.
|
|
182
|
-
|
|
183
|
-
For more on JAX:
|
|
184
|
-
|
|
185
|
-
https://github.com/google/jax
|
|
186
|
-
https://jax.readthedocs.io/en/latest/jax.numpy.html
|
|
187
|
-
|
|
188
|
-
If the user is on a Win machine, Linux OS can be used using
|
|
189
|
-
the Windows Subsystem for Linux.
|
|
190
|
-
|
|
191
|
-
For more on WSL:
|
|
192
|
-
https://docs.microsoft.com/en-us/windows/wsl/install-win10
|
|
193
|
-
|
|
194
|
-
:param SAVE_T_POD: bool
|
|
195
|
-
Flag deciding if the results will be stored on the disk.
|
|
196
|
-
Default value is True, to limit the RAM's usage.
|
|
197
|
-
Note that this might cause a minor slowdown for the loading,
|
|
198
|
-
but the tradeoff seems worthy.
|
|
199
|
-
This attribute is passed to the MODULO class.
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
POD temporal basis are returned if MEMORY_SAVING is not active. Otherwise all the results are saved on disk.
|
|
203
|
-
|
|
204
|
-
:return Psi_P: np.array
|
|
205
|
-
POD Psis
|
|
206
|
-
|
|
207
|
-
:return Sigma_P: np.array
|
|
208
|
-
POD Sigmas. If needed, Lambdas can be easily computed recalling that: Sigma_P = np.sqrt(Lambda_P)
|
|
209
|
-
"""
|
|
210
|
-
|
|
211
|
-
if self.MEMORY_SAVING:
|
|
212
|
-
K = np.load(self.FOLDER_OUT + "/correlation_matrix/k_matrix.npz")['K']
|
|
213
|
-
SAVE_T_POD = True
|
|
214
|
-
else:
|
|
215
|
-
K = self.K
|
|
216
|
-
|
|
217
|
-
Psi_P, Sigma_P = Temporal_basis_POD(K, SAVE_T_POD,
|
|
218
|
-
self.FOLDER_OUT, self.n_Modes, self.eig_solver)
|
|
219
|
-
|
|
220
|
-
del K
|
|
221
|
-
return Psi_P, Sigma_P if not self.MEMORY_SAVING else None
|
|
222
|
-
|
|
223
|
-
def _spatial_basis_POD(self, Psi_P, Sigma_P,
|
|
224
|
-
SAVE_SPATIAL_POD: bool = True):
|
|
225
|
-
"""
|
|
226
|
-
This method computes the spatial structure for the Proper Orthogonal Decomposition (POD) computation.
|
|
227
|
-
The theoretical background of the POD is briefly recalled here:
|
|
228
|
-
|
|
229
|
-
https://youtu.be/8fhupzhAR_M
|
|
230
|
-
|
|
231
|
-
:param Psi_P: np.array
|
|
232
|
-
POD temporal basis
|
|
233
|
-
:param Sigma_P: np.array
|
|
234
|
-
POD Sigmas
|
|
235
|
-
:param SAVE_SPATIAL_POD: bool
|
|
236
|
-
Flag deciding if the results will be stored on the disk.
|
|
237
|
-
Default value is True, to limit the RAM's usage.
|
|
238
|
-
Note that this might cause a minor slowdown for the loading,
|
|
239
|
-
but the tradeoff seems worthy.
|
|
240
|
-
This attribute is passed to the MODULO class.
|
|
241
|
-
|
|
242
|
-
:return Phi_P: np.array
|
|
243
|
-
POD Phis
|
|
244
|
-
|
|
245
|
-
"""
|
|
246
|
-
|
|
247
|
-
self.SAVE_SPATIAL_POD = SAVE_SPATIAL_POD
|
|
248
|
-
|
|
249
|
-
if self.MEMORY_SAVING:
|
|
250
|
-
'''Loading temporal basis from disk. They're already in memory otherwise.'''
|
|
251
|
-
Psi_P = np.load(self.FOLDER_OUT + 'POD/temporal_basis.npz')['Psis']
|
|
252
|
-
Sigma_P = np.load(self.FOLDER_OUT + 'POD/temporal_basis.npz')['Sigmas']
|
|
253
|
-
|
|
254
|
-
Phi_P = Spatial_basis_POD(self.D, N_T=self.N_T, PSI_P=Psi_P, Sigma_P=Sigma_P,
|
|
255
|
-
MEMORY_SAVING=self.MEMORY_SAVING, FOLDER_OUT=self.FOLDER_OUT,
|
|
256
|
-
N_PARTITIONS=self.N_PARTITIONS, SAVE_SPATIAL_POD=SAVE_SPATIAL_POD)
|
|
257
|
-
|
|
258
|
-
return Phi_P if not self.MEMORY_SAVING else None
|
|
259
|
-
|
|
260
|
-
def _temporal_basis_mPOD(self, K, Nf, Ex, F_V, Keep, boundaries, MODE, dt, K_S=False):
|
|
261
|
-
"""
|
|
262
|
-
This function computes the temporal structures of each scale in the mPOD, as in step 4 of the algorithm
|
|
263
|
-
ref: Multi-Scale Proper Orthogonal Decomposition of Complex Fluid Flows - M. A. Mendez et al.
|
|
264
|
-
|
|
265
|
-
:param K: np.array
|
|
266
|
-
Temporal correlation matrix
|
|
267
|
-
:param Nf: np.array
|
|
268
|
-
Order of the FIR filters that are used to isolate each of the scales
|
|
269
|
-
:param Ex: int
|
|
270
|
-
Extension at the boundaries of K to impose the boundary conditions (see boundaries)
|
|
271
|
-
It must be at least as Nf.
|
|
272
|
-
:param F_V: np.array
|
|
273
|
-
Frequency splitting vector, containing the frequencies of each scale (see article).
|
|
274
|
-
If the time axis is in seconds, these frequencies are in Hz.
|
|
275
|
-
:param Keep: np.array
|
|
276
|
-
Scale keep
|
|
277
|
-
:param boundaries: str -> {'nearest', 'reflect', 'wrap' or 'extrap'}
|
|
278
|
-
Define the boundary conditions for the filtering process, in order to avoid edge effects.
|
|
279
|
-
The available boundary conditions are the classic ones implemented for image processing:
|
|
280
|
-
nearest', 'reflect', 'wrap' or 'extrap'. See also https://docs.scipy.org/doc/scipy/reference/tutorial/ndimage.html
|
|
281
|
-
:param MODE: str -> {‘reduced’, ‘complete’, ‘r’, ‘raw’}
|
|
282
|
-
A QR factorization is used to enforce the orthonormality of the mPOD basis, to compensate
|
|
283
|
-
for the non-ideal frequency response of the filters.
|
|
284
|
-
The option MODE from np.linalg.qr carries out this operation.
|
|
285
|
-
|
|
286
|
-
:return PSI_M: np.array
|
|
287
|
-
Multiscale POD temporal basis
|
|
288
|
-
|
|
289
|
-
"""
|
|
290
|
-
|
|
291
|
-
if self.MEMORY_SAVING:
|
|
292
|
-
K = np.load(self.FOLDER_OUT + "/correlation_matrix/k_matrix.npz")['K']
|
|
293
|
-
|
|
294
|
-
PSI_M = temporal_basis_mPOD(K=K, Nf=Nf, Ex=Ex, F_V=F_V, Keep=Keep, boundaries=boundaries,
|
|
295
|
-
MODE=MODE, dt=dt, FOLDER_OUT=self.FOLDER_OUT,
|
|
296
|
-
n_Modes=self.n_Modes, K_S=False,
|
|
297
|
-
MEMORY_SAVING=self.MEMORY_SAVING, SAT=self.SAT, eig_solver=self.eig_solver)
|
|
298
|
-
|
|
299
|
-
return PSI_M if not self.MEMORY_SAVING else None
|
|
300
|
-
|
|
301
|
-
def _spatial_basis_mPOD(self, D, PSI_M, SAVE):
|
|
302
|
-
"""
|
|
303
|
-
This function implements the last step of the mPOD algorithm:
|
|
304
|
-
completing the decomposition. Here we project from psis, to get phis and sigmas
|
|
305
|
-
|
|
306
|
-
:param D: np.array
|
|
307
|
-
data matrix
|
|
308
|
-
:param PSI_M: np.array
|
|
309
|
-
temporal basis for the mPOD. Remember that it is not possible to impose both basis matrices
|
|
310
|
-
phis and psis: given one of the two, the other is univocally determined.
|
|
311
|
-
:param SAVE: bool
|
|
312
|
-
if True, MODULO saves the results on disk.
|
|
313
|
-
|
|
314
|
-
:return Phi_M: np.array
|
|
315
|
-
mPOD Phis (Matrix of spatial structures)
|
|
316
|
-
:return Psi_M: np.array
|
|
317
|
-
mPOD Psis (Matrix of temporal structures)
|
|
318
|
-
:return Sigma_M: np.array
|
|
319
|
-
mPOD Sigmas (vector of amplitudes, i.e. the diagonal of Sigma_M)
|
|
320
|
-
|
|
321
|
-
"""
|
|
322
|
-
|
|
323
|
-
Phi_M, Psi_M, Sigma_M = spatial_basis_mPOD(D, PSI_M, N_T=self.N_T, N_PARTITIONS=self.N_PARTITIONS,
|
|
324
|
-
N_S=self.N_S, MEMORY_SAVING=self.MEMORY_SAVING,
|
|
325
|
-
FOLDER_OUT=self.FOLDER_OUT,
|
|
326
|
-
SAVE=SAVE)
|
|
327
|
-
|
|
328
|
-
return Phi_M, Psi_M, Sigma_M
|
|
329
|
-
|
|
330
|
-
def compute_mPOD(self, Nf, Ex, F_V, Keep, SAT, boundaries, MODE, dt, SAVE=False):
|
|
331
|
-
"""
|
|
332
|
-
This function computes the temporal structures of each scale in the mPOD, as in step 4 of the algorithm
|
|
333
|
-
ref: Multi-Scale Proper Orthogonal Decomposition of Complex Fluid Flows - M. A. Mendez et al.
|
|
334
|
-
|
|
335
|
-
:param K: np.array
|
|
336
|
-
Temporal correlation matrix
|
|
337
|
-
|
|
338
|
-
:param Nf: np.array
|
|
339
|
-
Order of the FIR filters that are used to isolate each of the scales
|
|
340
|
-
|
|
341
|
-
:param Ex: int
|
|
342
|
-
Extension at the boundaries of K to impose the boundary conditions (see boundaries)
|
|
343
|
-
It must be at least as Nf.
|
|
344
|
-
|
|
345
|
-
:param F_V: np.array
|
|
346
|
-
Frequency splitting vector, containing the frequencies of each scale (see article).
|
|
347
|
-
If the time axis is in seconds, these frequencies are in Hz.
|
|
348
|
-
|
|
349
|
-
:param Keep: np.array
|
|
350
|
-
Scale keep
|
|
351
|
-
|
|
352
|
-
:param boundaries: str -> {'nearest', 'reflect', 'wrap' or 'extrap'}
|
|
353
|
-
Define the boundary conditions for the filtering process, in order to avoid edge effects.
|
|
354
|
-
The available boundary conditions are the classic ones implemented for image processing:
|
|
355
|
-
nearest', 'reflect', 'wrap' or 'extrap'. See also https://docs.scipy.org/doc/scipy/reference/tutorial/ndimage.html
|
|
356
|
-
|
|
357
|
-
:param MODE: str -> {‘reduced’, ‘complete’, ‘r’, ‘raw’}
|
|
358
|
-
A QR factorization is used to enforce the orthonormality of the mPOD basis, to compensate
|
|
359
|
-
for the non-ideal frequency response of the filters.
|
|
360
|
-
The option MODE from np.linalg.qr carries out this operation.
|
|
361
|
-
|
|
362
|
-
:param SAT: Maximum number of modes per scale.
|
|
363
|
-
Only used for mPOD (max number of modes per scale)
|
|
364
|
-
|
|
365
|
-
:param dt: float
|
|
366
|
-
temporal step
|
|
367
|
-
|
|
368
|
-
:return Phi_M: np.array
|
|
369
|
-
mPOD Phis (Matrix of spatial structures)
|
|
370
|
-
:return Psi_M: np.array
|
|
371
|
-
mPOD Psis (Matrix of temporal structures)
|
|
372
|
-
:return Sigma_M: np.array
|
|
373
|
-
mPOD Sigmas (vector of amplitudes, i.e. the diagonal of Sigma_M
|
|
374
|
-
|
|
375
|
-
"""
|
|
376
|
-
|
|
377
|
-
print('Computing correlation matrix D matrix...')
|
|
378
|
-
self.K = CorrelationMatrix(self.N_T, self.N_PARTITIONS,
|
|
379
|
-
self.MEMORY_SAVING,
|
|
380
|
-
self.FOLDER_OUT, self.SAVE_K, D=self.Dstar)
|
|
381
|
-
|
|
382
|
-
if self.MEMORY_SAVING:
|
|
383
|
-
self.K = np.load(self.FOLDER_OUT + '/correlation_matrix/k_matrix.npz')['K']
|
|
384
|
-
|
|
385
|
-
print("Computing Temporal Basis...")
|
|
386
|
-
|
|
387
|
-
PSI_M = temporal_basis_mPOD(K=self.K, Nf=Nf, Ex=Ex, F_V=F_V, Keep=Keep, boundaries=boundaries,
|
|
388
|
-
MODE=MODE, dt=dt, FOLDER_OUT=self.FOLDER_OUT,
|
|
389
|
-
n_Modes=self.n_Modes, MEMORY_SAVING=self.MEMORY_SAVING, SAT=SAT,
|
|
390
|
-
eig_solver=self.eig_solver)
|
|
391
|
-
|
|
392
|
-
print("Done.")
|
|
393
|
-
|
|
394
|
-
if hasattr(self, 'D'): # if self.D is available:
|
|
395
|
-
print('Computing Phi from D...')
|
|
396
|
-
Phi_M, Psi_M, Sigma_M = spatial_basis_mPOD(self.D, PSI_M, N_T=self.N_T, N_PARTITIONS=self.N_PARTITIONS,
|
|
397
|
-
N_S=self.N_S, MEMORY_SAVING=self.MEMORY_SAVING,
|
|
398
|
-
FOLDER_OUT=self.FOLDER_OUT,
|
|
399
|
-
SAVE=SAVE)
|
|
400
|
-
|
|
401
|
-
else: # if not, the memory saving is on and D will not be used. We pass a dummy D
|
|
402
|
-
print('Computing Phi from partitions...')
|
|
403
|
-
Phi_M, Psi_M, Sigma_M = spatial_basis_mPOD(np.array([1]), PSI_M, N_T=self.N_T,
|
|
404
|
-
N_PARTITIONS=self.N_PARTITIONS,
|
|
405
|
-
N_S=self.N_S, MEMORY_SAVING=self.MEMORY_SAVING,
|
|
406
|
-
FOLDER_OUT=self.FOLDER_OUT,
|
|
407
|
-
SAVE=SAVE)
|
|
408
|
-
|
|
409
|
-
print("Done.")
|
|
410
|
-
|
|
411
|
-
return Phi_M, Psi_M, Sigma_M
|
|
412
|
-
|
|
413
|
-
def compute_POD_K(self, SAVE_T_POD: bool = False):
|
|
414
|
-
"""
|
|
415
|
-
This method computes the Proper Orthogonal Decomposition (POD) of a dataset
|
|
416
|
-
using the snapshot approach, i.e. working on the temporal correlation matrix.
|
|
417
|
-
The eig solver for K is defined in 'eig_solver'
|
|
418
|
-
The theoretical background of the POD is briefly recalled here:
|
|
419
|
-
|
|
420
|
-
https://youtu.be/8fhupzhAR_M
|
|
421
|
-
|
|
422
|
-
:return Psi_P: np.array
|
|
423
|
-
POD Psis
|
|
424
|
-
|
|
425
|
-
:return Sigma_P: np.array
|
|
426
|
-
POD Sigmas. If needed, Lambdas can be easily computed recalling that: Sigma_P = np.sqrt(Lambda_P)
|
|
427
|
-
|
|
428
|
-
:return Phi_P: np.array
|
|
429
|
-
POD Phis
|
|
430
|
-
"""
|
|
431
|
-
|
|
432
|
-
print('Computing correlation matrix...')
|
|
433
|
-
self.K = CorrelationMatrix(self.N_T, self.N_PARTITIONS,
|
|
434
|
-
self.MEMORY_SAVING,
|
|
435
|
-
self.FOLDER_OUT, self.SAVE_K, D=self.Dstar, weights=self.weights)
|
|
436
|
-
|
|
437
|
-
if self.MEMORY_SAVING:
|
|
438
|
-
self.K = np.load(self.FOLDER_OUT + '/correlation_matrix/k_matrix.npz')['K']
|
|
439
|
-
|
|
440
|
-
print("Computing Temporal Basis...")
|
|
441
|
-
Psi_P, Sigma_P = Temporal_basis_POD(self.K, SAVE_T_POD,
|
|
442
|
-
self.FOLDER_OUT, self.n_Modes, eig_solver=self.eig_solver)
|
|
443
|
-
print("Done.")
|
|
444
|
-
print("Computing Spatial Basis...")
|
|
445
|
-
|
|
446
|
-
if self.MEMORY_SAVING: # if self.D is available:
|
|
447
|
-
print('Computing Phi from partitions...')
|
|
448
|
-
Phi_P = Spatial_basis_POD(np.array([1]), N_T=self.N_T,
|
|
449
|
-
PSI_P=Psi_P,
|
|
450
|
-
Sigma_P=Sigma_P,
|
|
451
|
-
MEMORY_SAVING=self.MEMORY_SAVING,
|
|
452
|
-
FOLDER_OUT=self.FOLDER_OUT,
|
|
453
|
-
N_PARTITIONS=self.N_PARTITIONS)
|
|
454
|
-
|
|
455
|
-
else: # if not, the memory saving is on and D will not be used. We pass a dummy D
|
|
456
|
-
print('Computing Phi from D...')
|
|
457
|
-
Phi_P = Spatial_basis_POD(self.D, N_T=self.N_T,
|
|
458
|
-
PSI_P=Psi_P,
|
|
459
|
-
Sigma_P=Sigma_P,
|
|
460
|
-
MEMORY_SAVING=self.MEMORY_SAVING,
|
|
461
|
-
FOLDER_OUT=self.FOLDER_OUT,
|
|
462
|
-
N_PARTITIONS=self.N_PARTITIONS)
|
|
463
|
-
print("Done.")
|
|
464
|
-
|
|
465
|
-
return Phi_P, Psi_P, Sigma_P
|
|
466
|
-
|
|
467
|
-
def compute_POD_svd(self, SAVE_T_POD: bool = False):
|
|
468
|
-
"""
|
|
469
|
-
This method computes the Proper Orthogonal Decomposition (POD) of a dataset
|
|
470
|
-
using the SVD decomposition. The svd solver is defined by 'svd_solver'.
|
|
471
|
-
Note that in this case, the memory saving option is of no help, since
|
|
472
|
-
the SVD must be performed over the entire dataset.
|
|
473
|
-
|
|
474
|
-
https://youtu.be/8fhupzhAR_M
|
|
475
|
-
|
|
476
|
-
:return Psi_P: np.array
|
|
477
|
-
POD Psis
|
|
478
|
-
|
|
479
|
-
:return Sigma_P: np.array
|
|
480
|
-
POD Sigmas. If needed, Lambdas can be easily computed recalling that: Sigma_P = np.sqrt(Lambda_P)
|
|
481
|
-
|
|
482
|
-
:return Phi_P: np.array
|
|
483
|
-
POD Phis
|
|
484
|
-
"""
|
|
485
|
-
# If Memory saving is active, we must load back the data.
|
|
486
|
-
# This process is memory demanding. Different SVD solver will handle this differently.
|
|
487
|
-
|
|
488
|
-
if self.MEMORY_SAVING:
|
|
489
|
-
if self.N_T % self.N_PARTITIONS != 0:
|
|
490
|
-
tot_blocks_col = self.N_PARTITIONS + 1
|
|
491
|
-
else:
|
|
492
|
-
tot_blocks_col = self.N_PARTITIONS
|
|
493
|
-
|
|
494
|
-
# Prepare the D matrix again
|
|
495
|
-
D = np.zeros((self.N_S, self.N_T))
|
|
496
|
-
R1 = 0
|
|
497
|
-
|
|
498
|
-
# print(' \n Reloading D from tmp...')
|
|
499
|
-
for k in tqdm(range(tot_blocks_col)):
|
|
500
|
-
di = np.load(self.FOLDER_OUT + f"/data_partitions/di_{k + 1}.npz")['di']
|
|
501
|
-
R2 = R1 + np.shape(di)[1]
|
|
502
|
-
D[:, R1:R2] = di
|
|
503
|
-
R1 = R2
|
|
504
|
-
|
|
505
|
-
# Now that we have D back, we can proceed with the SVD approach
|
|
506
|
-
Phi_P, Psi_P, Sigma_P = switch_svds(D, self.n_Modes, self.svd_solver)
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
else: # self.MEMORY_SAVING:
|
|
510
|
-
Phi_P, Psi_P, Sigma_P = switch_svds(self.D, self.n_Modes, self.svd_solver)
|
|
511
|
-
|
|
512
|
-
return Phi_P, Psi_P, Sigma_P
|
|
513
|
-
|
|
514
|
-
def compute_DMD_PIP(self, SAVE_T_DMD: bool = True, F_S=1):
|
|
515
|
-
"""
|
|
516
|
-
This method computes the Dynamic Mode Decomposition of the data
|
|
517
|
-
using the algorithm in https://arxiv.org/abs/1312.0041, which is basically the same as
|
|
518
|
-
the PIP algorithm proposed in https://www.sciencedirect.com/science/article/abs/pii/0167278996001248
|
|
519
|
-
See v1 of this paper https://arxiv.org/abs/2001.01971 for more details (yes, reviewers did ask to omit this detail in v2).
|
|
520
|
-
|
|
521
|
-
:return Phi_D: np.array
|
|
522
|
-
DMD Phis. As for the DFT, these are complex.
|
|
523
|
-
|
|
524
|
-
:return Lambda_D: np.array
|
|
525
|
-
DMD Eigenvalues (of the reduced propagator). These are complex.
|
|
526
|
-
|
|
527
|
-
:return freqs: np.array
|
|
528
|
-
Frequencies (in Hz, associated to the DMD modes)
|
|
529
|
-
|
|
530
|
-
:return a0s: np.array
|
|
531
|
-
Initial Coefficients of the Modes
|
|
532
|
-
|
|
533
|
-
"""
|
|
534
|
-
|
|
535
|
-
# If Memory saving is active, we must load back the data
|
|
536
|
-
if self.MEMORY_SAVING:
|
|
537
|
-
if self.N_T % self.N_PARTITIONS != 0:
|
|
538
|
-
tot_blocks_col = self.N_PARTITIONS + 1
|
|
539
|
-
else:
|
|
540
|
-
tot_blocks_col = self.N_PARTITIONS
|
|
541
|
-
|
|
542
|
-
# Prepare the D matrix again
|
|
543
|
-
D = np.zeros((self.N_S, self.N_T))
|
|
544
|
-
R1 = 0
|
|
545
|
-
|
|
546
|
-
# print(' \n Reloading D from tmp...')
|
|
547
|
-
for k in tqdm(range(tot_blocks_col)):
|
|
548
|
-
di = np.load(self.FOLDER_OUT + f"/data_partitions/di_{k + 1}.npz")['di']
|
|
549
|
-
R2 = R1 + np.shape(di)[1]
|
|
550
|
-
D[:, R1:R2] = di
|
|
551
|
-
R1 = R2
|
|
552
|
-
|
|
553
|
-
# Compute the DMD
|
|
554
|
-
Phi_D, Lambda, freqs, a0s = dmd_s(D[:, 0:self.N_T - 1],
|
|
555
|
-
D[:, 1:self.N_T], self.n_Modes, F_S, svd_solver=self.svd_solver)
|
|
556
|
-
|
|
557
|
-
else:
|
|
558
|
-
Phi_D, Lambda, freqs, a0s = dmd_s(self.D[:, 0:self.N_T - 1],
|
|
559
|
-
self.D[:, 1:self.N_T], self.n_Modes, F_S, SAVE_T_DMD=SAVE_T_DMD,
|
|
560
|
-
svd_solver=self.svd_solver, FOLDER_OUT=self.FOLDER_OUT)
|
|
561
|
-
|
|
562
|
-
return Phi_D, Lambda, freqs, a0s
|
|
563
|
-
|
|
564
|
-
def compute_DFT(self, F_S, SAVE_DFT=False):
|
|
565
|
-
"""
|
|
566
|
-
This method computes the Discrete Fourier Transform of your data.
|
|
567
|
-
|
|
568
|
-
Check out this tutorial: https://www.youtube.com/watch?v=8fhupzhAR_M&list=PLEJZLD0-4PeKW6Ze984q08bNz28GTntkR&index=2
|
|
569
|
-
|
|
570
|
-
:param F_S: float,
|
|
571
|
-
Sampling Frequency [Hz]
|
|
572
|
-
:param SAVE_DFT: bool,
|
|
573
|
-
If True, MODULO will save the output in self.FOLDER OUT/MODULO_tmp
|
|
574
|
-
|
|
575
|
-
:return: Sorted_Freqs: np.array,
|
|
576
|
-
Sorted Frequencies
|
|
577
|
-
:return Phi_F: np.array,
|
|
578
|
-
DFT Phis
|
|
579
|
-
:return Sigma_F: np.array,
|
|
580
|
-
DFT Sigmas
|
|
581
|
-
"""
|
|
582
|
-
if self.D is None:
|
|
583
|
-
D = np.load(self.FOLDER_OUT + '/MODULO_tmp/data_matrix/database.npz')['D']
|
|
584
|
-
SAVE_DFT = True
|
|
585
|
-
Sorted_Freqs, Phi_F, SIGMA_F = dft_fit(self.N_T, F_S, D, self.FOLDER_OUT, SAVE_DFT=SAVE_DFT)
|
|
586
|
-
|
|
587
|
-
else:
|
|
588
|
-
Sorted_Freqs, Phi_F, SIGMA_F = dft_fit(self.N_T, F_S, self.D, self.FOLDER_OUT, SAVE_DFT=SAVE_DFT)
|
|
589
|
-
|
|
590
|
-
return Sorted_Freqs, Phi_F, SIGMA_F
|
|
591
|
-
|
|
592
|
-
def compute_SPOD_t(self, F_S, L_B=500, O_B=250, n_Modes=10, SAVE_SPOD=True):
|
|
593
|
-
"""
|
|
594
|
-
This method computes the Spectral POD of your data. This is the one by Towne et al
|
|
595
|
-
(https://www.cambridge.org/core/journals/journal-of-fluid-mechanics/article/abs/spectral-proper-orthogonal-decomposition-and-its-relationship-to-dynamic-mode-decomposition-and-resolvent-analysis/EC2A6DF76490A0B9EB208CC2CA037717)
|
|
596
|
-
|
|
597
|
-
:param F_S: float,
|
|
598
|
-
Sampling Frequency [Hz]
|
|
599
|
-
:param L_B: float,
|
|
600
|
-
lenght of the chunks
|
|
601
|
-
:param O_B: float,
|
|
602
|
-
Overlapping between blocks in the chunk
|
|
603
|
-
:param n_Modes: float,
|
|
604
|
-
number of modes to be computed for each frequency
|
|
605
|
-
:param SAVE_SPOD: bool,
|
|
606
|
-
If True, MODULO will save the output in self.FOLDER OUT/MODULO_tmp
|
|
607
|
-
:return Psi_P_hat: np.array
|
|
608
|
-
Spectra of the SPOD Modes
|
|
609
|
-
:return Sigma_P: np.array
|
|
610
|
-
Amplitudes of the SPOD Modes.
|
|
611
|
-
:return Phi_P: np.array
|
|
612
|
-
SPOD Phis
|
|
613
|
-
:return freq: float
|
|
614
|
-
frequency bins for the Spectral POD
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
"""
|
|
618
|
-
if self.D is None:
|
|
619
|
-
D = np.load(self.FOLDER_OUT + '/MODULO_tmp/data_matrix/database.npz')['D']
|
|
620
|
-
Phi_SP, Sigma_SP, Freqs_Pos = compute_SPOD_t(D, F_S, L_B=L_B, O_B=O_B,
|
|
621
|
-
n_Modes=n_Modes, SAVE_SPOD=SAVE_SPOD,
|
|
622
|
-
FOLDER_OUT=self.FOLDER_OUT, possible_svds=self.svd_solver)
|
|
623
|
-
else:
|
|
624
|
-
Phi_SP, Sigma_SP, Freqs_Pos = compute_SPOD_t(self.D, F_S, L_B=L_B, O_B=O_B,
|
|
625
|
-
n_Modes=n_Modes, SAVE_SPOD=SAVE_SPOD,
|
|
626
|
-
FOLDER_OUT=self.FOLDER_OUT, possible_svds=self.svd_solver)
|
|
627
|
-
|
|
628
|
-
return Phi_SP, Sigma_SP, Freqs_Pos
|
|
629
|
-
|
|
630
|
-
# New Decomposition: SPOD f
|
|
631
|
-
|
|
632
|
-
def compute_SPOD_s(self, F_S, N_O=100, f_c=0.3, n_Modes=10, SAVE_SPOD=True):
|
|
633
|
-
"""
|
|
634
|
-
This method computes the Spectral POD of your data.
|
|
635
|
-
This is the one by Sieber
|
|
636
|
-
et al (https://www.cambridge.org/core/journals/journal-of-fluid-mechanics/article/abs/spectral-proper-orthogonal-decomposition/DCD8A6EDEFD56F5A9715DBAD38BD461A)
|
|
637
|
-
|
|
638
|
-
:param F_S: float,
|
|
639
|
-
Sampling Frequency [Hz]
|
|
640
|
-
:param N_o: float,
|
|
641
|
-
Semi-Order of the diagonal filter.
|
|
642
|
-
Note that the filter order will be 2 N_o +1 (to make sure it is odd)
|
|
643
|
-
:param f_c: float,
|
|
644
|
-
cut-off frequency of the diagonal filter
|
|
645
|
-
:param n_Modes: float,
|
|
646
|
-
number of modes to be computed
|
|
647
|
-
:param SAVE_SPOD: bool,
|
|
648
|
-
If True, MODULO will save the output in self.FOLDER OUT/MODULO_tmp
|
|
649
|
-
:return Psi_P: np.array
|
|
650
|
-
SPOD Psis
|
|
651
|
-
:return Sigma_P: np.array
|
|
652
|
-
SPOD Sigmas.
|
|
653
|
-
:return Phi_P: np.array
|
|
654
|
-
SPOD Phis
|
|
655
|
-
"""
|
|
656
|
-
|
|
657
|
-
if self.D is None:
|
|
658
|
-
D = np.load(self.FOLDER_OUT + '/MODULO_tmp/data_matrix/database.npz')['D']
|
|
659
|
-
|
|
660
|
-
self.K = CorrelationMatrix(self.N_T, self.N_PARTITIONS, self.MEMORY_SAVING,
|
|
661
|
-
self.FOLDER_OUT, self.SAVE_K, D=D)
|
|
662
|
-
|
|
663
|
-
Phi_sP, Psi_sP, Sigma_sP = compute_SPOD_s(D, self.K, F_S, self.N_S, self.N_T, N_O, f_c,
|
|
664
|
-
n_Modes, SAVE_SPOD, self.FOLDER_OUT, self.MEMORY_SAVING,
|
|
665
|
-
self.N_PARTITIONS)
|
|
666
|
-
|
|
667
|
-
else:
|
|
668
|
-
self.K = CorrelationMatrix(self.N_T, self.N_PARTITIONS, self.MEMORY_SAVING,
|
|
669
|
-
self.FOLDER_OUT, self.SAVE_K, D=self.D)
|
|
670
|
-
|
|
671
|
-
Phi_sP, Psi_sP, Sigma_sP = compute_SPOD_s(self.D, self.K, F_S, self.N_S, self.N_T, N_O, f_c,
|
|
672
|
-
n_Modes, SAVE_SPOD, self.FOLDER_OUT, self.MEMORY_SAVING,
|
|
673
|
-
self.N_PARTITIONS)
|
|
674
|
-
|
|
675
|
-
# if self.D is None:
|
|
676
|
-
# D = np.load(self.FOLDER_OUT + '/MODULO_tmp/data_matrix/database.npz')['D']
|
|
677
|
-
# SAVE_SPOD = True
|
|
678
|
-
# # TODO : Lorenzo check this stuff
|
|
679
|
-
# else:
|
|
680
|
-
# D = self.D
|
|
681
|
-
#
|
|
682
|
-
# n_s = self.N_S # Repeat variable for debugging compatibility
|
|
683
|
-
# n_t = self.N_T
|
|
684
|
-
#
|
|
685
|
-
# print('Computing Correlation Matrix \n')
|
|
686
|
-
#
|
|
687
|
-
# # The first step is the same as the POD: we compute the correlation matrix
|
|
688
|
-
# K = CorrelationMatrix(self.N_T, self.N_PARTITIONS, self.MEMORY_SAVING,
|
|
689
|
-
# self.FOLDER_OUT, D=self.D)
|
|
690
|
-
#
|
|
691
|
-
# # 1. Initialize the extended
|
|
692
|
-
# K_e = np.zeros((n_t + 2 * N_o, n_t + 2 * N_o))
|
|
693
|
-
# # From which we clearly know that:
|
|
694
|
-
# K_e[N_o:n_t + N_o, N_o:n_t + N_o] = K
|
|
695
|
-
#
|
|
696
|
-
# # 2. We fill the edges ( a bit of repetition but ok.. )
|
|
697
|
-
#
|
|
698
|
-
# # Row-wise, Upper part
|
|
699
|
-
# for i in range(0, N_o):
|
|
700
|
-
# K_e[i, i:i + n_t] = K[0, :]
|
|
701
|
-
#
|
|
702
|
-
# # Row-wise, bottom part
|
|
703
|
-
# for i in range(N_o + n_t, n_t + 2 * N_o):
|
|
704
|
-
# K_e[i, i - n_t + 1:i + 1] = K[-1, :]
|
|
705
|
-
#
|
|
706
|
-
# # Column-wise, left part
|
|
707
|
-
# for j in range(0, N_o):
|
|
708
|
-
# K_e[j:j + n_t, j] = K[:, 0]
|
|
709
|
-
#
|
|
710
|
-
# # Column-wise, right part
|
|
711
|
-
# for j in range(N_o + n_t, 2 * N_o + n_t):
|
|
712
|
-
# K_e[j - n_t + 1:j + 1, j] = K[:, -1]
|
|
713
|
-
#
|
|
714
|
-
# # Now you create the diagonal kernel in 2D
|
|
715
|
-
# h_f = firwin(N_o, f_c) # Kernel in 1D
|
|
716
|
-
# # This is also something that must be put in a separate file:
|
|
717
|
-
# # To cancel the phase lag we make this non-causal with a symmetric
|
|
718
|
-
# # shift, hence with zero padding as equal as possible on both sides
|
|
719
|
-
# n_padd_l = round((n_t - N_o) / 2);
|
|
720
|
-
# n_padd_r = n_t - N_o - n_padd_l
|
|
721
|
-
#
|
|
722
|
-
# h_f_pad = np.pad(h_f, (n_padd_l, n_padd_r)) # symmetrically padded kernel in 1D
|
|
723
|
-
# h_f_2 = np.diag(h_f_pad)
|
|
724
|
-
#
|
|
725
|
-
# # Finally the filtered K is just
|
|
726
|
-
# K_F = signal.fftconvolve(K_e, h_f_2, mode='same')[N_o:n_t + N_o, N_o:n_t + N_o]
|
|
727
|
-
# # plt.plot(np.diag(K),'b--'); plt.plot(np.diag(K_F_e),'r')
|
|
728
|
-
#
|
|
729
|
-
# # From now on it's just POD:
|
|
730
|
-
# Psi_P, Sigma_P = Temporal_basis_POD(K_F, SAVE_SPOD,
|
|
731
|
-
# self.FOLDER_OUT, self.n_Modes)
|
|
732
|
-
#
|
|
733
|
-
# Phi_P = Spatial_basis_POD(self.D, N_T=self.N_T, PSI_P=Psi_P, Sigma_P=Sigma_P,
|
|
734
|
-
# MEMORY_SAVING=self.MEMORY_SAVING, FOLDER_OUT=self.FOLDER_OUT,
|
|
735
|
-
# N_PARTITIONS=self.N_PARTITIONS)
|
|
736
|
-
|
|
737
|
-
return Phi_sP, Psi_sP, Sigma_sP
|
|
738
|
-
|
|
739
|
-
def compute_kPOD(self, M_DIST=[1, 10], k_m=0.1, cent=True,
|
|
740
|
-
n_Modes=10, alpha=1e-6, metric='rbf', K_out=False):
|
|
741
|
-
"""
|
|
742
|
-
This function implements the kernel PCA as described in the VKI course https://www.vki.ac.be/index.php/events-ls/events/eventdetail/552/-/online-on-site-hands-on-machine-learning-for-fluid-dynamics-2023
|
|
743
|
-
|
|
744
|
-
The computation of the kernel function is carried out as in https://arxiv.org/pdf/2208.07746.pdf.
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
:param M_DIST: array,
|
|
748
|
-
position of the two snapshots that will be considered to
|
|
749
|
-
estimate the minimal k. They should be the most different ones.
|
|
750
|
-
:param k_m: float,
|
|
751
|
-
minimum value for the kernelized correlation
|
|
752
|
-
:param alpha: float
|
|
753
|
-
regularization for K_zeta
|
|
754
|
-
:param cent: bool,
|
|
755
|
-
if True, the matrix K is centered. Else it is not
|
|
756
|
-
:param n_Modes: float,
|
|
757
|
-
number of modes to be computed
|
|
758
|
-
:param metric: string,
|
|
759
|
-
This identifies the metric for the kernel matrix. It is a wrapper to 'pairwise_kernels' from sklearn.metrics.pairwise
|
|
760
|
-
Note that different metrics would need different set of parameters. For the moment, only rbf was tested; use any other option at your peril !
|
|
761
|
-
:param K_out: bool,
|
|
762
|
-
If true, the matrix K is also exported as a fourth output.
|
|
763
|
-
:return Psi_xi: np.array
|
|
764
|
-
kPOD's Psis
|
|
765
|
-
:return Sigma_xi: np.array
|
|
766
|
-
kPOD's Sigmas.
|
|
767
|
-
:return Phi_xi: np.array
|
|
768
|
-
kPOD's Phis
|
|
769
|
-
:return K_zeta: np.array
|
|
770
|
-
Kernel Function from which the decomposition is computed.
|
|
771
|
-
(exported only if K_out=True)
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
"""
|
|
775
|
-
if self.D is None:
|
|
776
|
-
D = np.load(self.FOLDER_OUT + '/MODULO_tmp/data_matrix/database.npz')['D']
|
|
777
|
-
else:
|
|
778
|
-
D = self.D
|
|
779
|
-
|
|
780
|
-
# Compute Eucledean distances
|
|
781
|
-
i, j = M_DIST;
|
|
782
|
-
n_s, n_t = np.shape(D)
|
|
783
|
-
M_ij = np.linalg.norm(D[:, i] - D[:, j]) ** 2
|
|
784
|
-
|
|
785
|
-
gamma = -np.log(k_m) / M_ij
|
|
786
|
-
|
|
787
|
-
K_zeta = pairwise_kernels(D.T, metric='rbf', gamma=gamma)
|
|
788
|
-
print('Kernel K ready')
|
|
789
|
-
|
|
790
|
-
# Compute the Kernel Matrix
|
|
791
|
-
n_t = np.shape(D)[1]
|
|
792
|
-
# Center the Kernel Matrix (if cent is True):
|
|
793
|
-
if cent:
|
|
794
|
-
H = np.eye(n_t) - 1 / n_t * np.ones_like(K_zeta)
|
|
795
|
-
K_zeta = H @ K_zeta @ H.T
|
|
796
|
-
print('K_zeta centered')
|
|
797
|
-
# Diagonalize and Sort
|
|
798
|
-
lambdas, Psi_xi = linalg.eigh(K_zeta + alpha * np.eye(n_t), subset_by_index=[n_t - n_Modes, n_t - 1])
|
|
799
|
-
lambdas, Psi_xi = lambdas[::-1], Psi_xi[:, ::-1];
|
|
800
|
-
Sigma_xi = np.sqrt(lambdas);
|
|
801
|
-
print('K_zeta diagonalized')
|
|
802
|
-
# Encode
|
|
803
|
-
# Z_xi=np.diag(Sigma_xi)@Psi_xi.T
|
|
804
|
-
# We compute the spatial structures as projections of the data
|
|
805
|
-
# onto the Psi_xi!
|
|
806
|
-
R = Psi_xi.shape[1]
|
|
807
|
-
PHI_xi_SIGMA_xi = np.dot(D, (Psi_xi))
|
|
808
|
-
# Initialize the output
|
|
809
|
-
PHI_xi = np.zeros((n_s, R))
|
|
810
|
-
SIGMA_xi = np.zeros((R))
|
|
811
|
-
|
|
812
|
-
for i in tqdm(range(0, R)):
|
|
813
|
-
# Assign the norm as amplitude
|
|
814
|
-
SIGMA_xi[i] = np.linalg.norm(PHI_xi_SIGMA_xi[:, i])
|
|
815
|
-
# Normalize the columns of C to get spatial modes
|
|
816
|
-
PHI_xi[:, i] = PHI_xi_SIGMA_xi[:, i] / SIGMA_xi[i]
|
|
817
|
-
|
|
818
|
-
Indices = np.flipud(np.argsort(SIGMA_xi)) # find indices for sorting in decreasing order
|
|
819
|
-
Sorted_Sigmas = SIGMA_xi[Indices] # Sort all the sigmas
|
|
820
|
-
Phi_xi = PHI_xi[:, Indices] # Sorted Spatial Structures Matrix
|
|
821
|
-
Psi_xi = Psi_xi[:, Indices] # Sorted Temporal Structures Matrix
|
|
822
|
-
Sigma_xi = Sorted_Sigmas # Sorted Amplitude Matrix
|
|
823
|
-
print('Phi_xi computed')
|
|
824
|
-
|
|
825
|
-
if K_out:
|
|
826
|
-
return Phi_xi, Psi_xi, Sigma_xi, K_zeta
|
|
827
|
-
else:
|
|
828
|
-
return Phi_xi, Psi_xi, Sigma_xi
|
|
1
|
+
# Functional ones:
|
|
2
|
+
import os
|
|
3
|
+
import numpy as np
|
|
4
|
+
from scipy import linalg
|
|
5
|
+
from sklearn.metrics.pairwise import pairwise_kernels
|
|
6
|
+
# To have fancy loading bar
|
|
7
|
+
from tqdm import tqdm
|
|
8
|
+
|
|
9
|
+
# All the functions from the modulo package
|
|
10
|
+
from modulo_vki.core._dft import dft_fit
|
|
11
|
+
from modulo_vki.core._dmd_s import dmd_s
|
|
12
|
+
from modulo_vki.core._k_matrix import CorrelationMatrix
|
|
13
|
+
from modulo_vki.core._mpod_space import spatial_basis_mPOD
|
|
14
|
+
from modulo_vki.core._mpod_time import temporal_basis_mPOD
|
|
15
|
+
from modulo_vki.core._pod_space import Spatial_basis_POD
|
|
16
|
+
from modulo_vki.core._pod_time import Temporal_basis_POD
|
|
17
|
+
from modulo_vki.core._spod_s import compute_SPOD_s
|
|
18
|
+
from modulo_vki.core._spod_t import compute_SPOD_t
|
|
19
|
+
from modulo_vki.utils._utils import switch_svds
|
|
20
|
+
|
|
21
|
+
from modulo_vki.utils.read_db import ReadData
|
|
22
|
+
|
|
23
|
+
class ModuloVKI:
|
|
24
|
+
"""
|
|
25
|
+
MODULO (MODal mULtiscale pOd) is a software developed at the von Karman Institute to perform Multiscale
|
|
26
|
+
Modal Analysis of numerical and experimental data using the Multiscale Proper Orthogonal Decomposition (mPOD).
|
|
27
|
+
|
|
28
|
+
Theoretical foundation can be found at:
|
|
29
|
+
https://arxiv.org/abs/1804.09646
|
|
30
|
+
|
|
31
|
+
Presentation of the MODULO framework available here:
|
|
32
|
+
https://arxiv.org/pdf/2004.12123.pdf
|
|
33
|
+
|
|
34
|
+
YouTube channel with hands-on tutorials can be found at:
|
|
35
|
+
https://youtube.com/playlist?list=PLEJZLD0-4PeKW6Ze984q08bNz28GTntkR
|
|
36
|
+
|
|
37
|
+
All the codes so far assume that the dataset is equally spaced both in space (i.e. along a Cartesian grid)
|
|
38
|
+
and in time. The extension to non-uniformly sampled data will be included in future releases.
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(self, data: np.array,
|
|
44
|
+
N_PARTITIONS: int = 1,
|
|
45
|
+
FOLDER_OUT='./',
|
|
46
|
+
SAVE_K: bool = False,
|
|
47
|
+
N_T: int = 100,
|
|
48
|
+
N_S: int = 200,
|
|
49
|
+
n_Modes: int = 10,
|
|
50
|
+
dtype: str = 'float32',
|
|
51
|
+
eig_solver: str = 'eigh',
|
|
52
|
+
svd_solver: str = 'svd_sklearn_truncated',
|
|
53
|
+
weights: np.array = np.array([])):
|
|
54
|
+
"""
|
|
55
|
+
This function initializes the main parameters needed by MODULO.
|
|
56
|
+
|
|
57
|
+
Attributes:
|
|
58
|
+
|
|
59
|
+
:param data: This is the data matrix to factorize. It is a np.array with
|
|
60
|
+
shape ((N_S, N_T)). If the data has not yet been prepared in the form of a np.array,
|
|
61
|
+
the method ReadData in MODULO can be used (see ReadData). If the memory saving is active (N_PARTITIONS >1), the folder with partitions should be prepared.
|
|
62
|
+
If the memory saving is active, this entry = None. The data matrix is assumed to big to be saved and the
|
|
63
|
+
|
|
64
|
+
:param N_PARTITIONS: If memory saving feature is active, this parameter sets the number of partitions
|
|
65
|
+
that will be used to store the data matrices during the computations.
|
|
66
|
+
|
|
67
|
+
:param FOLDER_OUT: Folder in which the output will be stored.The output includes the matrices Phi, Sigma and Psi (optional) and temporary files
|
|
68
|
+
used for some of the calculations (e.g.: for memory saving).
|
|
69
|
+
|
|
70
|
+
:param SAVE_K: A flag deciding if the matrix will be stored in the disk (in FOLDER_OUT/correlation_matrix) or not.
|
|
71
|
+
Default option is 'False'.
|
|
72
|
+
|
|
73
|
+
:param N_T: Number of time steps, must be given when N_PARTITIONS >1
|
|
74
|
+
|
|
75
|
+
:param N_S: Number of grid points, must be given when N_PARTITIONS >1
|
|
76
|
+
|
|
77
|
+
:param n_Modes: Number of Modes to be computed
|
|
78
|
+
|
|
79
|
+
:param dtype: Cast "data" with type dtype
|
|
80
|
+
|
|
81
|
+
:param eig_solver: Numerical solver to compute the eigen values
|
|
82
|
+
|
|
83
|
+
:param svd_solver: Numerical solver to compute the Single Value Decomposition
|
|
84
|
+
|
|
85
|
+
:param weights: weight vector [w_i,....,w_{N_s}] where w_i = area_cell_i/area_grid
|
|
86
|
+
Only needed if grid is non-uniform.
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
print("MODULO (MODal mULtiscale pOd) is a software developed at the von Karman Institute to perform "
|
|
92
|
+
"data driven modal decomposition of numerical and experimental data. \n")
|
|
93
|
+
|
|
94
|
+
if not isinstance(data, np.ndarray) and N_PARTITIONS == 1:
|
|
95
|
+
raise TypeError(
|
|
96
|
+
"Please check that your database is in an numpy array format. If D=None, then you must have memory saving (N_PARTITIONS>1)")
|
|
97
|
+
|
|
98
|
+
# Load the data matrix
|
|
99
|
+
if isinstance(data, np.ndarray):
|
|
100
|
+
# Number of points in time and space
|
|
101
|
+
self.N_T = data.shape[1]
|
|
102
|
+
self.N_S = data.shape[0]
|
|
103
|
+
# Check the data type
|
|
104
|
+
self.D = data.astype(dtype)
|
|
105
|
+
else:
|
|
106
|
+
self.D = None # D is never saved when N_partitions >1
|
|
107
|
+
self.N_S = N_S # so N_S and N_t must be given as parameters of modulo
|
|
108
|
+
self.N_T = N_T
|
|
109
|
+
|
|
110
|
+
# Load and applied the weights to the D matrix
|
|
111
|
+
if weights.size != 0:
|
|
112
|
+
if len(weights) == self.N_S:
|
|
113
|
+
print("The weights you have input have the size of the columns of D \n"
|
|
114
|
+
"MODULO has considered that you have already duplicated the dimensions of the weights "
|
|
115
|
+
"to match the dimensions of the D columns \n")
|
|
116
|
+
self.weights = weights
|
|
117
|
+
elif 2 * len(weights) == self.N_S: # 2D computation only
|
|
118
|
+
self.weights = np.concatenate((weights, weights))
|
|
119
|
+
print("Modulo assumes you have a 2D domain and has duplicated the weight "
|
|
120
|
+
"array to match the size of the D columns \n")
|
|
121
|
+
print(weights)
|
|
122
|
+
else:
|
|
123
|
+
raise AttributeError("Make sure the size of the weight array is twice smaller than the size of D")
|
|
124
|
+
# Dstar is used to compute the K matrix
|
|
125
|
+
if isinstance(data, np.ndarray):
|
|
126
|
+
# Apply the weights only if D exist.
|
|
127
|
+
# If not (i.e. N_partitions >1), weights are applied in _k_matrix.py when loading partitions of D
|
|
128
|
+
self.Dstar = np.transpose(np.transpose(self.D) * np.sqrt(self.weights))
|
|
129
|
+
else:
|
|
130
|
+
self.Dstar = None
|
|
131
|
+
else:
|
|
132
|
+
print("Modulo assumes you have a uniform grid. "
|
|
133
|
+
"If not, please give the weights as parameters of MODULO!")
|
|
134
|
+
self.weights = weights
|
|
135
|
+
self.Dstar = self.D
|
|
136
|
+
|
|
137
|
+
if N_PARTITIONS > 1:
|
|
138
|
+
self.MEMORY_SAVING = True
|
|
139
|
+
else:
|
|
140
|
+
self.MEMORY_SAVING = False
|
|
141
|
+
|
|
142
|
+
# Assign the number of modes
|
|
143
|
+
self.n_Modes = n_Modes
|
|
144
|
+
# If particular needs, override choice for svd and eigen solve
|
|
145
|
+
self.svd_solver = svd_solver.lower()
|
|
146
|
+
self.eig_solver = eig_solver.lower()
|
|
147
|
+
possible_svds = ['svd_numpy', 'svd_scipy_sparse', 'svd_sklearn_randomized', 'svd_sklearn_truncated']
|
|
148
|
+
possible_eigs = ['svd_sklearn_randomized', 'eigsh', 'eigh']
|
|
149
|
+
|
|
150
|
+
if self.svd_solver not in possible_svds:
|
|
151
|
+
raise NotImplementedError("The requested SVD solver is not implemented. Please pick one of the following:"
|
|
152
|
+
"which belongs to: \n {}".format(possible_svds))
|
|
153
|
+
|
|
154
|
+
if self.eig_solver not in possible_eigs:
|
|
155
|
+
raise NotImplementedError("The requested EIG solver is not implemented. Please pick one of the following: "
|
|
156
|
+
" \n {}".format(possible_eigs))
|
|
157
|
+
|
|
158
|
+
# if N_PARTITIONS >= self.N_T:
|
|
159
|
+
# raise AttributeError("The number of requested partitions is greater of the total columns (N_T). Please,"
|
|
160
|
+
# "try again.")
|
|
161
|
+
|
|
162
|
+
self.N_PARTITIONS = N_PARTITIONS
|
|
163
|
+
|
|
164
|
+
self.FOLDER_OUT = FOLDER_OUT
|
|
165
|
+
|
|
166
|
+
self.SAVE_K = SAVE_K
|
|
167
|
+
|
|
168
|
+
if self.MEMORY_SAVING:
|
|
169
|
+
os.makedirs(self.FOLDER_OUT, exist_ok=True)
|
|
170
|
+
|
|
171
|
+
def _temporal_basis_POD(self,
|
|
172
|
+
SAVE_T_POD: bool = False):
|
|
173
|
+
"""
|
|
174
|
+
This method computes the temporal structure for the Proper Orthogonal Decomposition (POD) computation.
|
|
175
|
+
The theoretical background of the POD is briefly recalled here:
|
|
176
|
+
|
|
177
|
+
https://youtu.be/8fhupzhAR_M
|
|
178
|
+
|
|
179
|
+
The diagonalization of K is computed via Singular Value Decomposition (SVD).
|
|
180
|
+
A speedup is available if the user is on Linux machine, in which case MODULO
|
|
181
|
+
exploits the power of JAX and its Numpy implementation.
|
|
182
|
+
|
|
183
|
+
For more on JAX:
|
|
184
|
+
|
|
185
|
+
https://github.com/google/jax
|
|
186
|
+
https://jax.readthedocs.io/en/latest/jax.numpy.html
|
|
187
|
+
|
|
188
|
+
If the user is on a Win machine, Linux OS can be used using
|
|
189
|
+
the Windows Subsystem for Linux.
|
|
190
|
+
|
|
191
|
+
For more on WSL:
|
|
192
|
+
https://docs.microsoft.com/en-us/windows/wsl/install-win10
|
|
193
|
+
|
|
194
|
+
:param SAVE_T_POD: bool
|
|
195
|
+
Flag deciding if the results will be stored on the disk.
|
|
196
|
+
Default value is True, to limit the RAM's usage.
|
|
197
|
+
Note that this might cause a minor slowdown for the loading,
|
|
198
|
+
but the tradeoff seems worthy.
|
|
199
|
+
This attribute is passed to the MODULO class.
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
POD temporal basis are returned if MEMORY_SAVING is not active. Otherwise all the results are saved on disk.
|
|
203
|
+
|
|
204
|
+
:return Psi_P: np.array
|
|
205
|
+
POD Psis
|
|
206
|
+
|
|
207
|
+
:return Sigma_P: np.array
|
|
208
|
+
POD Sigmas. If needed, Lambdas can be easily computed recalling that: Sigma_P = np.sqrt(Lambda_P)
|
|
209
|
+
"""
|
|
210
|
+
|
|
211
|
+
if self.MEMORY_SAVING:
|
|
212
|
+
K = np.load(self.FOLDER_OUT + "/correlation_matrix/k_matrix.npz")['K']
|
|
213
|
+
SAVE_T_POD = True
|
|
214
|
+
else:
|
|
215
|
+
K = self.K
|
|
216
|
+
|
|
217
|
+
Psi_P, Sigma_P = Temporal_basis_POD(K, SAVE_T_POD,
|
|
218
|
+
self.FOLDER_OUT, self.n_Modes, self.eig_solver)
|
|
219
|
+
|
|
220
|
+
del K
|
|
221
|
+
return Psi_P, Sigma_P if not self.MEMORY_SAVING else None
|
|
222
|
+
|
|
223
|
+
def _spatial_basis_POD(self, Psi_P, Sigma_P,
|
|
224
|
+
SAVE_SPATIAL_POD: bool = True):
|
|
225
|
+
"""
|
|
226
|
+
This method computes the spatial structure for the Proper Orthogonal Decomposition (POD) computation.
|
|
227
|
+
The theoretical background of the POD is briefly recalled here:
|
|
228
|
+
|
|
229
|
+
https://youtu.be/8fhupzhAR_M
|
|
230
|
+
|
|
231
|
+
:param Psi_P: np.array
|
|
232
|
+
POD temporal basis
|
|
233
|
+
:param Sigma_P: np.array
|
|
234
|
+
POD Sigmas
|
|
235
|
+
:param SAVE_SPATIAL_POD: bool
|
|
236
|
+
Flag deciding if the results will be stored on the disk.
|
|
237
|
+
Default value is True, to limit the RAM's usage.
|
|
238
|
+
Note that this might cause a minor slowdown for the loading,
|
|
239
|
+
but the tradeoff seems worthy.
|
|
240
|
+
This attribute is passed to the MODULO class.
|
|
241
|
+
|
|
242
|
+
:return Phi_P: np.array
|
|
243
|
+
POD Phis
|
|
244
|
+
|
|
245
|
+
"""
|
|
246
|
+
|
|
247
|
+
self.SAVE_SPATIAL_POD = SAVE_SPATIAL_POD
|
|
248
|
+
|
|
249
|
+
if self.MEMORY_SAVING:
|
|
250
|
+
'''Loading temporal basis from disk. They're already in memory otherwise.'''
|
|
251
|
+
Psi_P = np.load(self.FOLDER_OUT + 'POD/temporal_basis.npz')['Psis']
|
|
252
|
+
Sigma_P = np.load(self.FOLDER_OUT + 'POD/temporal_basis.npz')['Sigmas']
|
|
253
|
+
|
|
254
|
+
Phi_P = Spatial_basis_POD(self.D, N_T=self.N_T, PSI_P=Psi_P, Sigma_P=Sigma_P,
|
|
255
|
+
MEMORY_SAVING=self.MEMORY_SAVING, FOLDER_OUT=self.FOLDER_OUT,
|
|
256
|
+
N_PARTITIONS=self.N_PARTITIONS, SAVE_SPATIAL_POD=SAVE_SPATIAL_POD)
|
|
257
|
+
|
|
258
|
+
return Phi_P if not self.MEMORY_SAVING else None
|
|
259
|
+
|
|
260
|
+
def _temporal_basis_mPOD(self, K, Nf, Ex, F_V, Keep, boundaries, MODE, dt, K_S=False):
|
|
261
|
+
"""
|
|
262
|
+
This function computes the temporal structures of each scale in the mPOD, as in step 4 of the algorithm
|
|
263
|
+
ref: Multi-Scale Proper Orthogonal Decomposition of Complex Fluid Flows - M. A. Mendez et al.
|
|
264
|
+
|
|
265
|
+
:param K: np.array
|
|
266
|
+
Temporal correlation matrix
|
|
267
|
+
:param Nf: np.array
|
|
268
|
+
Order of the FIR filters that are used to isolate each of the scales
|
|
269
|
+
:param Ex: int
|
|
270
|
+
Extension at the boundaries of K to impose the boundary conditions (see boundaries)
|
|
271
|
+
It must be at least as Nf.
|
|
272
|
+
:param F_V: np.array
|
|
273
|
+
Frequency splitting vector, containing the frequencies of each scale (see article).
|
|
274
|
+
If the time axis is in seconds, these frequencies are in Hz.
|
|
275
|
+
:param Keep: np.array
|
|
276
|
+
Scale keep
|
|
277
|
+
:param boundaries: str -> {'nearest', 'reflect', 'wrap' or 'extrap'}
|
|
278
|
+
Define the boundary conditions for the filtering process, in order to avoid edge effects.
|
|
279
|
+
The available boundary conditions are the classic ones implemented for image processing:
|
|
280
|
+
nearest', 'reflect', 'wrap' or 'extrap'. See also https://docs.scipy.org/doc/scipy/reference/tutorial/ndimage.html
|
|
281
|
+
:param MODE: str -> {‘reduced’, ‘complete’, ‘r’, ‘raw’}
|
|
282
|
+
A QR factorization is used to enforce the orthonormality of the mPOD basis, to compensate
|
|
283
|
+
for the non-ideal frequency response of the filters.
|
|
284
|
+
The option MODE from np.linalg.qr carries out this operation.
|
|
285
|
+
|
|
286
|
+
:return PSI_M: np.array
|
|
287
|
+
Multiscale POD temporal basis
|
|
288
|
+
|
|
289
|
+
"""
|
|
290
|
+
|
|
291
|
+
if self.MEMORY_SAVING:
|
|
292
|
+
K = np.load(self.FOLDER_OUT + "/correlation_matrix/k_matrix.npz")['K']
|
|
293
|
+
|
|
294
|
+
PSI_M = temporal_basis_mPOD(K=K, Nf=Nf, Ex=Ex, F_V=F_V, Keep=Keep, boundaries=boundaries,
|
|
295
|
+
MODE=MODE, dt=dt, FOLDER_OUT=self.FOLDER_OUT,
|
|
296
|
+
n_Modes=self.n_Modes, K_S=False,
|
|
297
|
+
MEMORY_SAVING=self.MEMORY_SAVING, SAT=self.SAT, eig_solver=self.eig_solver)
|
|
298
|
+
|
|
299
|
+
return PSI_M if not self.MEMORY_SAVING else None
|
|
300
|
+
|
|
301
|
+
def _spatial_basis_mPOD(self, D, PSI_M, SAVE):
|
|
302
|
+
"""
|
|
303
|
+
This function implements the last step of the mPOD algorithm:
|
|
304
|
+
completing the decomposition. Here we project from psis, to get phis and sigmas
|
|
305
|
+
|
|
306
|
+
:param D: np.array
|
|
307
|
+
data matrix
|
|
308
|
+
:param PSI_M: np.array
|
|
309
|
+
temporal basis for the mPOD. Remember that it is not possible to impose both basis matrices
|
|
310
|
+
phis and psis: given one of the two, the other is univocally determined.
|
|
311
|
+
:param SAVE: bool
|
|
312
|
+
if True, MODULO saves the results on disk.
|
|
313
|
+
|
|
314
|
+
:return Phi_M: np.array
|
|
315
|
+
mPOD Phis (Matrix of spatial structures)
|
|
316
|
+
:return Psi_M: np.array
|
|
317
|
+
mPOD Psis (Matrix of temporal structures)
|
|
318
|
+
:return Sigma_M: np.array
|
|
319
|
+
mPOD Sigmas (vector of amplitudes, i.e. the diagonal of Sigma_M)
|
|
320
|
+
|
|
321
|
+
"""
|
|
322
|
+
|
|
323
|
+
Phi_M, Psi_M, Sigma_M = spatial_basis_mPOD(D, PSI_M, N_T=self.N_T, N_PARTITIONS=self.N_PARTITIONS,
|
|
324
|
+
N_S=self.N_S, MEMORY_SAVING=self.MEMORY_SAVING,
|
|
325
|
+
FOLDER_OUT=self.FOLDER_OUT,
|
|
326
|
+
SAVE=SAVE)
|
|
327
|
+
|
|
328
|
+
return Phi_M, Psi_M, Sigma_M
|
|
329
|
+
|
|
330
|
+
def compute_mPOD(self, Nf, Ex, F_V, Keep, SAT, boundaries, MODE, dt, SAVE=False):
|
|
331
|
+
"""
|
|
332
|
+
This function computes the temporal structures of each scale in the mPOD, as in step 4 of the algorithm
|
|
333
|
+
ref: Multi-Scale Proper Orthogonal Decomposition of Complex Fluid Flows - M. A. Mendez et al.
|
|
334
|
+
|
|
335
|
+
:param K: np.array
|
|
336
|
+
Temporal correlation matrix
|
|
337
|
+
|
|
338
|
+
:param Nf: np.array
|
|
339
|
+
Order of the FIR filters that are used to isolate each of the scales
|
|
340
|
+
|
|
341
|
+
:param Ex: int
|
|
342
|
+
Extension at the boundaries of K to impose the boundary conditions (see boundaries)
|
|
343
|
+
It must be at least as Nf.
|
|
344
|
+
|
|
345
|
+
:param F_V: np.array
|
|
346
|
+
Frequency splitting vector, containing the frequencies of each scale (see article).
|
|
347
|
+
If the time axis is in seconds, these frequencies are in Hz.
|
|
348
|
+
|
|
349
|
+
:param Keep: np.array
|
|
350
|
+
Scale keep
|
|
351
|
+
|
|
352
|
+
:param boundaries: str -> {'nearest', 'reflect', 'wrap' or 'extrap'}
|
|
353
|
+
Define the boundary conditions for the filtering process, in order to avoid edge effects.
|
|
354
|
+
The available boundary conditions are the classic ones implemented for image processing:
|
|
355
|
+
nearest', 'reflect', 'wrap' or 'extrap'. See also https://docs.scipy.org/doc/scipy/reference/tutorial/ndimage.html
|
|
356
|
+
|
|
357
|
+
:param MODE: str -> {‘reduced’, ‘complete’, ‘r’, ‘raw’}
|
|
358
|
+
A QR factorization is used to enforce the orthonormality of the mPOD basis, to compensate
|
|
359
|
+
for the non-ideal frequency response of the filters.
|
|
360
|
+
The option MODE from np.linalg.qr carries out this operation.
|
|
361
|
+
|
|
362
|
+
:param SAT: Maximum number of modes per scale.
|
|
363
|
+
Only used for mPOD (max number of modes per scale)
|
|
364
|
+
|
|
365
|
+
:param dt: float
|
|
366
|
+
temporal step
|
|
367
|
+
|
|
368
|
+
:return Phi_M: np.array
|
|
369
|
+
mPOD Phis (Matrix of spatial structures)
|
|
370
|
+
:return Psi_M: np.array
|
|
371
|
+
mPOD Psis (Matrix of temporal structures)
|
|
372
|
+
:return Sigma_M: np.array
|
|
373
|
+
mPOD Sigmas (vector of amplitudes, i.e. the diagonal of Sigma_M
|
|
374
|
+
|
|
375
|
+
"""
|
|
376
|
+
|
|
377
|
+
print('Computing correlation matrix D matrix...')
|
|
378
|
+
self.K = CorrelationMatrix(self.N_T, self.N_PARTITIONS,
|
|
379
|
+
self.MEMORY_SAVING,
|
|
380
|
+
self.FOLDER_OUT, self.SAVE_K, D=self.Dstar)
|
|
381
|
+
|
|
382
|
+
if self.MEMORY_SAVING:
|
|
383
|
+
self.K = np.load(self.FOLDER_OUT + '/correlation_matrix/k_matrix.npz')['K']
|
|
384
|
+
|
|
385
|
+
print("Computing Temporal Basis...")
|
|
386
|
+
|
|
387
|
+
PSI_M = temporal_basis_mPOD(K=self.K, Nf=Nf, Ex=Ex, F_V=F_V, Keep=Keep, boundaries=boundaries,
|
|
388
|
+
MODE=MODE, dt=dt, FOLDER_OUT=self.FOLDER_OUT,
|
|
389
|
+
n_Modes=self.n_Modes, MEMORY_SAVING=self.MEMORY_SAVING, SAT=SAT,
|
|
390
|
+
eig_solver=self.eig_solver)
|
|
391
|
+
|
|
392
|
+
print("Done.")
|
|
393
|
+
|
|
394
|
+
if hasattr(self, 'D'): # if self.D is available:
|
|
395
|
+
print('Computing Phi from D...')
|
|
396
|
+
Phi_M, Psi_M, Sigma_M = spatial_basis_mPOD(self.D, PSI_M, N_T=self.N_T, N_PARTITIONS=self.N_PARTITIONS,
|
|
397
|
+
N_S=self.N_S, MEMORY_SAVING=self.MEMORY_SAVING,
|
|
398
|
+
FOLDER_OUT=self.FOLDER_OUT,
|
|
399
|
+
SAVE=SAVE)
|
|
400
|
+
|
|
401
|
+
else: # if not, the memory saving is on and D will not be used. We pass a dummy D
|
|
402
|
+
print('Computing Phi from partitions...')
|
|
403
|
+
Phi_M, Psi_M, Sigma_M = spatial_basis_mPOD(np.array([1]), PSI_M, N_T=self.N_T,
|
|
404
|
+
N_PARTITIONS=self.N_PARTITIONS,
|
|
405
|
+
N_S=self.N_S, MEMORY_SAVING=self.MEMORY_SAVING,
|
|
406
|
+
FOLDER_OUT=self.FOLDER_OUT,
|
|
407
|
+
SAVE=SAVE)
|
|
408
|
+
|
|
409
|
+
print("Done.")
|
|
410
|
+
|
|
411
|
+
return Phi_M, Psi_M, Sigma_M
|
|
412
|
+
|
|
413
|
+
def compute_POD_K(self, SAVE_T_POD: bool = False):
|
|
414
|
+
"""
|
|
415
|
+
This method computes the Proper Orthogonal Decomposition (POD) of a dataset
|
|
416
|
+
using the snapshot approach, i.e. working on the temporal correlation matrix.
|
|
417
|
+
The eig solver for K is defined in 'eig_solver'
|
|
418
|
+
The theoretical background of the POD is briefly recalled here:
|
|
419
|
+
|
|
420
|
+
https://youtu.be/8fhupzhAR_M
|
|
421
|
+
|
|
422
|
+
:return Psi_P: np.array
|
|
423
|
+
POD Psis
|
|
424
|
+
|
|
425
|
+
:return Sigma_P: np.array
|
|
426
|
+
POD Sigmas. If needed, Lambdas can be easily computed recalling that: Sigma_P = np.sqrt(Lambda_P)
|
|
427
|
+
|
|
428
|
+
:return Phi_P: np.array
|
|
429
|
+
POD Phis
|
|
430
|
+
"""
|
|
431
|
+
|
|
432
|
+
print('Computing correlation matrix...')
|
|
433
|
+
self.K = CorrelationMatrix(self.N_T, self.N_PARTITIONS,
|
|
434
|
+
self.MEMORY_SAVING,
|
|
435
|
+
self.FOLDER_OUT, self.SAVE_K, D=self.Dstar, weights=self.weights)
|
|
436
|
+
|
|
437
|
+
if self.MEMORY_SAVING:
|
|
438
|
+
self.K = np.load(self.FOLDER_OUT + '/correlation_matrix/k_matrix.npz')['K']
|
|
439
|
+
|
|
440
|
+
print("Computing Temporal Basis...")
|
|
441
|
+
Psi_P, Sigma_P = Temporal_basis_POD(self.K, SAVE_T_POD,
|
|
442
|
+
self.FOLDER_OUT, self.n_Modes, eig_solver=self.eig_solver)
|
|
443
|
+
print("Done.")
|
|
444
|
+
print("Computing Spatial Basis...")
|
|
445
|
+
|
|
446
|
+
if self.MEMORY_SAVING: # if self.D is available:
|
|
447
|
+
print('Computing Phi from partitions...')
|
|
448
|
+
Phi_P = Spatial_basis_POD(np.array([1]), N_T=self.N_T,
|
|
449
|
+
PSI_P=Psi_P,
|
|
450
|
+
Sigma_P=Sigma_P,
|
|
451
|
+
MEMORY_SAVING=self.MEMORY_SAVING,
|
|
452
|
+
FOLDER_OUT=self.FOLDER_OUT,
|
|
453
|
+
N_PARTITIONS=self.N_PARTITIONS)
|
|
454
|
+
|
|
455
|
+
else: # if not, the memory saving is on and D will not be used. We pass a dummy D
|
|
456
|
+
print('Computing Phi from D...')
|
|
457
|
+
Phi_P = Spatial_basis_POD(self.D, N_T=self.N_T,
|
|
458
|
+
PSI_P=Psi_P,
|
|
459
|
+
Sigma_P=Sigma_P,
|
|
460
|
+
MEMORY_SAVING=self.MEMORY_SAVING,
|
|
461
|
+
FOLDER_OUT=self.FOLDER_OUT,
|
|
462
|
+
N_PARTITIONS=self.N_PARTITIONS)
|
|
463
|
+
print("Done.")
|
|
464
|
+
|
|
465
|
+
return Phi_P, Psi_P, Sigma_P
|
|
466
|
+
|
|
467
|
+
def compute_POD_svd(self, SAVE_T_POD: bool = False):
|
|
468
|
+
"""
|
|
469
|
+
This method computes the Proper Orthogonal Decomposition (POD) of a dataset
|
|
470
|
+
using the SVD decomposition. The svd solver is defined by 'svd_solver'.
|
|
471
|
+
Note that in this case, the memory saving option is of no help, since
|
|
472
|
+
the SVD must be performed over the entire dataset.
|
|
473
|
+
|
|
474
|
+
https://youtu.be/8fhupzhAR_M
|
|
475
|
+
|
|
476
|
+
:return Psi_P: np.array
|
|
477
|
+
POD Psis
|
|
478
|
+
|
|
479
|
+
:return Sigma_P: np.array
|
|
480
|
+
POD Sigmas. If needed, Lambdas can be easily computed recalling that: Sigma_P = np.sqrt(Lambda_P)
|
|
481
|
+
|
|
482
|
+
:return Phi_P: np.array
|
|
483
|
+
POD Phis
|
|
484
|
+
"""
|
|
485
|
+
# If Memory saving is active, we must load back the data.
|
|
486
|
+
# This process is memory demanding. Different SVD solver will handle this differently.
|
|
487
|
+
|
|
488
|
+
if self.MEMORY_SAVING:
|
|
489
|
+
if self.N_T % self.N_PARTITIONS != 0:
|
|
490
|
+
tot_blocks_col = self.N_PARTITIONS + 1
|
|
491
|
+
else:
|
|
492
|
+
tot_blocks_col = self.N_PARTITIONS
|
|
493
|
+
|
|
494
|
+
# Prepare the D matrix again
|
|
495
|
+
D = np.zeros((self.N_S, self.N_T))
|
|
496
|
+
R1 = 0
|
|
497
|
+
|
|
498
|
+
# print(' \n Reloading D from tmp...')
|
|
499
|
+
for k in tqdm(range(tot_blocks_col)):
|
|
500
|
+
di = np.load(self.FOLDER_OUT + f"/data_partitions/di_{k + 1}.npz")['di']
|
|
501
|
+
R2 = R1 + np.shape(di)[1]
|
|
502
|
+
D[:, R1:R2] = di
|
|
503
|
+
R1 = R2
|
|
504
|
+
|
|
505
|
+
# Now that we have D back, we can proceed with the SVD approach
|
|
506
|
+
Phi_P, Psi_P, Sigma_P = switch_svds(D, self.n_Modes, self.svd_solver)
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
else: # self.MEMORY_SAVING:
|
|
510
|
+
Phi_P, Psi_P, Sigma_P = switch_svds(self.D, self.n_Modes, self.svd_solver)
|
|
511
|
+
|
|
512
|
+
return Phi_P, Psi_P, Sigma_P
|
|
513
|
+
|
|
514
|
+
def compute_DMD_PIP(self, SAVE_T_DMD: bool = True, F_S=1):
|
|
515
|
+
"""
|
|
516
|
+
This method computes the Dynamic Mode Decomposition of the data
|
|
517
|
+
using the algorithm in https://arxiv.org/abs/1312.0041, which is basically the same as
|
|
518
|
+
the PIP algorithm proposed in https://www.sciencedirect.com/science/article/abs/pii/0167278996001248
|
|
519
|
+
See v1 of this paper https://arxiv.org/abs/2001.01971 for more details (yes, reviewers did ask to omit this detail in v2).
|
|
520
|
+
|
|
521
|
+
:return Phi_D: np.array
|
|
522
|
+
DMD Phis. As for the DFT, these are complex.
|
|
523
|
+
|
|
524
|
+
:return Lambda_D: np.array
|
|
525
|
+
DMD Eigenvalues (of the reduced propagator). These are complex.
|
|
526
|
+
|
|
527
|
+
:return freqs: np.array
|
|
528
|
+
Frequencies (in Hz, associated to the DMD modes)
|
|
529
|
+
|
|
530
|
+
:return a0s: np.array
|
|
531
|
+
Initial Coefficients of the Modes
|
|
532
|
+
|
|
533
|
+
"""
|
|
534
|
+
|
|
535
|
+
# If Memory saving is active, we must load back the data
|
|
536
|
+
if self.MEMORY_SAVING:
|
|
537
|
+
if self.N_T % self.N_PARTITIONS != 0:
|
|
538
|
+
tot_blocks_col = self.N_PARTITIONS + 1
|
|
539
|
+
else:
|
|
540
|
+
tot_blocks_col = self.N_PARTITIONS
|
|
541
|
+
|
|
542
|
+
# Prepare the D matrix again
|
|
543
|
+
D = np.zeros((self.N_S, self.N_T))
|
|
544
|
+
R1 = 0
|
|
545
|
+
|
|
546
|
+
# print(' \n Reloading D from tmp...')
|
|
547
|
+
for k in tqdm(range(tot_blocks_col)):
|
|
548
|
+
di = np.load(self.FOLDER_OUT + f"/data_partitions/di_{k + 1}.npz")['di']
|
|
549
|
+
R2 = R1 + np.shape(di)[1]
|
|
550
|
+
D[:, R1:R2] = di
|
|
551
|
+
R1 = R2
|
|
552
|
+
|
|
553
|
+
# Compute the DMD
|
|
554
|
+
Phi_D, Lambda, freqs, a0s = dmd_s(D[:, 0:self.N_T - 1],
|
|
555
|
+
D[:, 1:self.N_T], self.n_Modes, F_S, svd_solver=self.svd_solver)
|
|
556
|
+
|
|
557
|
+
else:
|
|
558
|
+
Phi_D, Lambda, freqs, a0s = dmd_s(self.D[:, 0:self.N_T - 1],
|
|
559
|
+
self.D[:, 1:self.N_T], self.n_Modes, F_S, SAVE_T_DMD=SAVE_T_DMD,
|
|
560
|
+
svd_solver=self.svd_solver, FOLDER_OUT=self.FOLDER_OUT)
|
|
561
|
+
|
|
562
|
+
return Phi_D, Lambda, freqs, a0s
|
|
563
|
+
|
|
564
|
+
def compute_DFT(self, F_S, SAVE_DFT=False):
|
|
565
|
+
"""
|
|
566
|
+
This method computes the Discrete Fourier Transform of your data.
|
|
567
|
+
|
|
568
|
+
Check out this tutorial: https://www.youtube.com/watch?v=8fhupzhAR_M&list=PLEJZLD0-4PeKW6Ze984q08bNz28GTntkR&index=2
|
|
569
|
+
|
|
570
|
+
:param F_S: float,
|
|
571
|
+
Sampling Frequency [Hz]
|
|
572
|
+
:param SAVE_DFT: bool,
|
|
573
|
+
If True, MODULO will save the output in self.FOLDER OUT/MODULO_tmp
|
|
574
|
+
|
|
575
|
+
:return: Sorted_Freqs: np.array,
|
|
576
|
+
Sorted Frequencies
|
|
577
|
+
:return Phi_F: np.array,
|
|
578
|
+
DFT Phis
|
|
579
|
+
:return Sigma_F: np.array,
|
|
580
|
+
DFT Sigmas
|
|
581
|
+
"""
|
|
582
|
+
if self.D is None:
|
|
583
|
+
D = np.load(self.FOLDER_OUT + '/MODULO_tmp/data_matrix/database.npz')['D']
|
|
584
|
+
SAVE_DFT = True
|
|
585
|
+
Sorted_Freqs, Phi_F, SIGMA_F = dft_fit(self.N_T, F_S, D, self.FOLDER_OUT, SAVE_DFT=SAVE_DFT)
|
|
586
|
+
|
|
587
|
+
else:
|
|
588
|
+
Sorted_Freqs, Phi_F, SIGMA_F = dft_fit(self.N_T, F_S, self.D, self.FOLDER_OUT, SAVE_DFT=SAVE_DFT)
|
|
589
|
+
|
|
590
|
+
return Sorted_Freqs, Phi_F, SIGMA_F
|
|
591
|
+
|
|
592
|
+
def compute_SPOD_t(self, F_S, L_B=500, O_B=250, n_Modes=10, SAVE_SPOD=True):
|
|
593
|
+
"""
|
|
594
|
+
This method computes the Spectral POD of your data. This is the one by Towne et al
|
|
595
|
+
(https://www.cambridge.org/core/journals/journal-of-fluid-mechanics/article/abs/spectral-proper-orthogonal-decomposition-and-its-relationship-to-dynamic-mode-decomposition-and-resolvent-analysis/EC2A6DF76490A0B9EB208CC2CA037717)
|
|
596
|
+
|
|
597
|
+
:param F_S: float,
|
|
598
|
+
Sampling Frequency [Hz]
|
|
599
|
+
:param L_B: float,
|
|
600
|
+
lenght of the chunks
|
|
601
|
+
:param O_B: float,
|
|
602
|
+
Overlapping between blocks in the chunk
|
|
603
|
+
:param n_Modes: float,
|
|
604
|
+
number of modes to be computed for each frequency
|
|
605
|
+
:param SAVE_SPOD: bool,
|
|
606
|
+
If True, MODULO will save the output in self.FOLDER OUT/MODULO_tmp
|
|
607
|
+
:return Psi_P_hat: np.array
|
|
608
|
+
Spectra of the SPOD Modes
|
|
609
|
+
:return Sigma_P: np.array
|
|
610
|
+
Amplitudes of the SPOD Modes.
|
|
611
|
+
:return Phi_P: np.array
|
|
612
|
+
SPOD Phis
|
|
613
|
+
:return freq: float
|
|
614
|
+
frequency bins for the Spectral POD
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
"""
|
|
618
|
+
if self.D is None:
|
|
619
|
+
D = np.load(self.FOLDER_OUT + '/MODULO_tmp/data_matrix/database.npz')['D']
|
|
620
|
+
Phi_SP, Sigma_SP, Freqs_Pos = compute_SPOD_t(D, F_S, L_B=L_B, O_B=O_B,
|
|
621
|
+
n_Modes=n_Modes, SAVE_SPOD=SAVE_SPOD,
|
|
622
|
+
FOLDER_OUT=self.FOLDER_OUT, possible_svds=self.svd_solver)
|
|
623
|
+
else:
|
|
624
|
+
Phi_SP, Sigma_SP, Freqs_Pos = compute_SPOD_t(self.D, F_S, L_B=L_B, O_B=O_B,
|
|
625
|
+
n_Modes=n_Modes, SAVE_SPOD=SAVE_SPOD,
|
|
626
|
+
FOLDER_OUT=self.FOLDER_OUT, possible_svds=self.svd_solver)
|
|
627
|
+
|
|
628
|
+
return Phi_SP, Sigma_SP, Freqs_Pos
|
|
629
|
+
|
|
630
|
+
# New Decomposition: SPOD f
|
|
631
|
+
|
|
632
|
+
def compute_SPOD_s(self, F_S, N_O=100, f_c=0.3, n_Modes=10, SAVE_SPOD=True):
|
|
633
|
+
"""
|
|
634
|
+
This method computes the Spectral POD of your data.
|
|
635
|
+
This is the one by Sieber
|
|
636
|
+
et al (https://www.cambridge.org/core/journals/journal-of-fluid-mechanics/article/abs/spectral-proper-orthogonal-decomposition/DCD8A6EDEFD56F5A9715DBAD38BD461A)
|
|
637
|
+
|
|
638
|
+
:param F_S: float,
|
|
639
|
+
Sampling Frequency [Hz]
|
|
640
|
+
:param N_o: float,
|
|
641
|
+
Semi-Order of the diagonal filter.
|
|
642
|
+
Note that the filter order will be 2 N_o +1 (to make sure it is odd)
|
|
643
|
+
:param f_c: float,
|
|
644
|
+
cut-off frequency of the diagonal filter
|
|
645
|
+
:param n_Modes: float,
|
|
646
|
+
number of modes to be computed
|
|
647
|
+
:param SAVE_SPOD: bool,
|
|
648
|
+
If True, MODULO will save the output in self.FOLDER OUT/MODULO_tmp
|
|
649
|
+
:return Psi_P: np.array
|
|
650
|
+
SPOD Psis
|
|
651
|
+
:return Sigma_P: np.array
|
|
652
|
+
SPOD Sigmas.
|
|
653
|
+
:return Phi_P: np.array
|
|
654
|
+
SPOD Phis
|
|
655
|
+
"""
|
|
656
|
+
|
|
657
|
+
if self.D is None:
|
|
658
|
+
D = np.load(self.FOLDER_OUT + '/MODULO_tmp/data_matrix/database.npz')['D']
|
|
659
|
+
|
|
660
|
+
self.K = CorrelationMatrix(self.N_T, self.N_PARTITIONS, self.MEMORY_SAVING,
|
|
661
|
+
self.FOLDER_OUT, self.SAVE_K, D=D)
|
|
662
|
+
|
|
663
|
+
Phi_sP, Psi_sP, Sigma_sP = compute_SPOD_s(D, self.K, F_S, self.N_S, self.N_T, N_O, f_c,
|
|
664
|
+
n_Modes, SAVE_SPOD, self.FOLDER_OUT, self.MEMORY_SAVING,
|
|
665
|
+
self.N_PARTITIONS)
|
|
666
|
+
|
|
667
|
+
else:
|
|
668
|
+
self.K = CorrelationMatrix(self.N_T, self.N_PARTITIONS, self.MEMORY_SAVING,
|
|
669
|
+
self.FOLDER_OUT, self.SAVE_K, D=self.D)
|
|
670
|
+
|
|
671
|
+
Phi_sP, Psi_sP, Sigma_sP = compute_SPOD_s(self.D, self.K, F_S, self.N_S, self.N_T, N_O, f_c,
|
|
672
|
+
n_Modes, SAVE_SPOD, self.FOLDER_OUT, self.MEMORY_SAVING,
|
|
673
|
+
self.N_PARTITIONS)
|
|
674
|
+
|
|
675
|
+
# if self.D is None:
|
|
676
|
+
# D = np.load(self.FOLDER_OUT + '/MODULO_tmp/data_matrix/database.npz')['D']
|
|
677
|
+
# SAVE_SPOD = True
|
|
678
|
+
# # TODO : Lorenzo check this stuff
|
|
679
|
+
# else:
|
|
680
|
+
# D = self.D
|
|
681
|
+
#
|
|
682
|
+
# n_s = self.N_S # Repeat variable for debugging compatibility
|
|
683
|
+
# n_t = self.N_T
|
|
684
|
+
#
|
|
685
|
+
# print('Computing Correlation Matrix \n')
|
|
686
|
+
#
|
|
687
|
+
# # The first step is the same as the POD: we compute the correlation matrix
|
|
688
|
+
# K = CorrelationMatrix(self.N_T, self.N_PARTITIONS, self.MEMORY_SAVING,
|
|
689
|
+
# self.FOLDER_OUT, D=self.D)
|
|
690
|
+
#
|
|
691
|
+
# # 1. Initialize the extended
|
|
692
|
+
# K_e = np.zeros((n_t + 2 * N_o, n_t + 2 * N_o))
|
|
693
|
+
# # From which we clearly know that:
|
|
694
|
+
# K_e[N_o:n_t + N_o, N_o:n_t + N_o] = K
|
|
695
|
+
#
|
|
696
|
+
# # 2. We fill the edges ( a bit of repetition but ok.. )
|
|
697
|
+
#
|
|
698
|
+
# # Row-wise, Upper part
|
|
699
|
+
# for i in range(0, N_o):
|
|
700
|
+
# K_e[i, i:i + n_t] = K[0, :]
|
|
701
|
+
#
|
|
702
|
+
# # Row-wise, bottom part
|
|
703
|
+
# for i in range(N_o + n_t, n_t + 2 * N_o):
|
|
704
|
+
# K_e[i, i - n_t + 1:i + 1] = K[-1, :]
|
|
705
|
+
#
|
|
706
|
+
# # Column-wise, left part
|
|
707
|
+
# for j in range(0, N_o):
|
|
708
|
+
# K_e[j:j + n_t, j] = K[:, 0]
|
|
709
|
+
#
|
|
710
|
+
# # Column-wise, right part
|
|
711
|
+
# for j in range(N_o + n_t, 2 * N_o + n_t):
|
|
712
|
+
# K_e[j - n_t + 1:j + 1, j] = K[:, -1]
|
|
713
|
+
#
|
|
714
|
+
# # Now you create the diagonal kernel in 2D
|
|
715
|
+
# h_f = firwin(N_o, f_c) # Kernel in 1D
|
|
716
|
+
# # This is also something that must be put in a separate file:
|
|
717
|
+
# # To cancel the phase lag we make this non-causal with a symmetric
|
|
718
|
+
# # shift, hence with zero padding as equal as possible on both sides
|
|
719
|
+
# n_padd_l = round((n_t - N_o) / 2);
|
|
720
|
+
# n_padd_r = n_t - N_o - n_padd_l
|
|
721
|
+
#
|
|
722
|
+
# h_f_pad = np.pad(h_f, (n_padd_l, n_padd_r)) # symmetrically padded kernel in 1D
|
|
723
|
+
# h_f_2 = np.diag(h_f_pad)
|
|
724
|
+
#
|
|
725
|
+
# # Finally the filtered K is just
|
|
726
|
+
# K_F = signal.fftconvolve(K_e, h_f_2, mode='same')[N_o:n_t + N_o, N_o:n_t + N_o]
|
|
727
|
+
# # plt.plot(np.diag(K),'b--'); plt.plot(np.diag(K_F_e),'r')
|
|
728
|
+
#
|
|
729
|
+
# # From now on it's just POD:
|
|
730
|
+
# Psi_P, Sigma_P = Temporal_basis_POD(K_F, SAVE_SPOD,
|
|
731
|
+
# self.FOLDER_OUT, self.n_Modes)
|
|
732
|
+
#
|
|
733
|
+
# Phi_P = Spatial_basis_POD(self.D, N_T=self.N_T, PSI_P=Psi_P, Sigma_P=Sigma_P,
|
|
734
|
+
# MEMORY_SAVING=self.MEMORY_SAVING, FOLDER_OUT=self.FOLDER_OUT,
|
|
735
|
+
# N_PARTITIONS=self.N_PARTITIONS)
|
|
736
|
+
|
|
737
|
+
return Phi_sP, Psi_sP, Sigma_sP
|
|
738
|
+
|
|
739
|
+
def compute_kPOD(self, M_DIST=[1, 10], k_m=0.1, cent=True,
|
|
740
|
+
n_Modes=10, alpha=1e-6, metric='rbf', K_out=False):
|
|
741
|
+
"""
|
|
742
|
+
This function implements the kernel PCA as described in the VKI course https://www.vki.ac.be/index.php/events-ls/events/eventdetail/552/-/online-on-site-hands-on-machine-learning-for-fluid-dynamics-2023
|
|
743
|
+
|
|
744
|
+
The computation of the kernel function is carried out as in https://arxiv.org/pdf/2208.07746.pdf.
|
|
745
|
+
|
|
746
|
+
|
|
747
|
+
:param M_DIST: array,
|
|
748
|
+
position of the two snapshots that will be considered to
|
|
749
|
+
estimate the minimal k. They should be the most different ones.
|
|
750
|
+
:param k_m: float,
|
|
751
|
+
minimum value for the kernelized correlation
|
|
752
|
+
:param alpha: float
|
|
753
|
+
regularization for K_zeta
|
|
754
|
+
:param cent: bool,
|
|
755
|
+
if True, the matrix K is centered. Else it is not
|
|
756
|
+
:param n_Modes: float,
|
|
757
|
+
number of modes to be computed
|
|
758
|
+
:param metric: string,
|
|
759
|
+
This identifies the metric for the kernel matrix. It is a wrapper to 'pairwise_kernels' from sklearn.metrics.pairwise
|
|
760
|
+
Note that different metrics would need different set of parameters. For the moment, only rbf was tested; use any other option at your peril !
|
|
761
|
+
:param K_out: bool,
|
|
762
|
+
If true, the matrix K is also exported as a fourth output.
|
|
763
|
+
:return Psi_xi: np.array
|
|
764
|
+
kPOD's Psis
|
|
765
|
+
:return Sigma_xi: np.array
|
|
766
|
+
kPOD's Sigmas.
|
|
767
|
+
:return Phi_xi: np.array
|
|
768
|
+
kPOD's Phis
|
|
769
|
+
:return K_zeta: np.array
|
|
770
|
+
Kernel Function from which the decomposition is computed.
|
|
771
|
+
(exported only if K_out=True)
|
|
772
|
+
|
|
773
|
+
|
|
774
|
+
"""
|
|
775
|
+
if self.D is None:
|
|
776
|
+
D = np.load(self.FOLDER_OUT + '/MODULO_tmp/data_matrix/database.npz')['D']
|
|
777
|
+
else:
|
|
778
|
+
D = self.D
|
|
779
|
+
|
|
780
|
+
# Compute Eucledean distances
|
|
781
|
+
i, j = M_DIST;
|
|
782
|
+
n_s, n_t = np.shape(D)
|
|
783
|
+
M_ij = np.linalg.norm(D[:, i] - D[:, j]) ** 2
|
|
784
|
+
|
|
785
|
+
gamma = -np.log(k_m) / M_ij
|
|
786
|
+
|
|
787
|
+
K_zeta = pairwise_kernels(D.T, metric='rbf', gamma=gamma)
|
|
788
|
+
print('Kernel K ready')
|
|
789
|
+
|
|
790
|
+
# Compute the Kernel Matrix
|
|
791
|
+
n_t = np.shape(D)[1]
|
|
792
|
+
# Center the Kernel Matrix (if cent is True):
|
|
793
|
+
if cent:
|
|
794
|
+
H = np.eye(n_t) - 1 / n_t * np.ones_like(K_zeta)
|
|
795
|
+
K_zeta = H @ K_zeta @ H.T
|
|
796
|
+
print('K_zeta centered')
|
|
797
|
+
# Diagonalize and Sort
|
|
798
|
+
lambdas, Psi_xi = linalg.eigh(K_zeta + alpha * np.eye(n_t), subset_by_index=[n_t - n_Modes, n_t - 1])
|
|
799
|
+
lambdas, Psi_xi = lambdas[::-1], Psi_xi[:, ::-1];
|
|
800
|
+
Sigma_xi = np.sqrt(lambdas);
|
|
801
|
+
print('K_zeta diagonalized')
|
|
802
|
+
# Encode
|
|
803
|
+
# Z_xi=np.diag(Sigma_xi)@Psi_xi.T
|
|
804
|
+
# We compute the spatial structures as projections of the data
|
|
805
|
+
# onto the Psi_xi!
|
|
806
|
+
R = Psi_xi.shape[1]
|
|
807
|
+
PHI_xi_SIGMA_xi = np.dot(D, (Psi_xi))
|
|
808
|
+
# Initialize the output
|
|
809
|
+
PHI_xi = np.zeros((n_s, R))
|
|
810
|
+
SIGMA_xi = np.zeros((R))
|
|
811
|
+
|
|
812
|
+
for i in tqdm(range(0, R)):
|
|
813
|
+
# Assign the norm as amplitude
|
|
814
|
+
SIGMA_xi[i] = np.linalg.norm(PHI_xi_SIGMA_xi[:, i])
|
|
815
|
+
# Normalize the columns of C to get spatial modes
|
|
816
|
+
PHI_xi[:, i] = PHI_xi_SIGMA_xi[:, i] / SIGMA_xi[i]
|
|
817
|
+
|
|
818
|
+
Indices = np.flipud(np.argsort(SIGMA_xi)) # find indices for sorting in decreasing order
|
|
819
|
+
Sorted_Sigmas = SIGMA_xi[Indices] # Sort all the sigmas
|
|
820
|
+
Phi_xi = PHI_xi[:, Indices] # Sorted Spatial Structures Matrix
|
|
821
|
+
Psi_xi = Psi_xi[:, Indices] # Sorted Temporal Structures Matrix
|
|
822
|
+
Sigma_xi = Sorted_Sigmas # Sorted Amplitude Matrix
|
|
823
|
+
print('Phi_xi computed')
|
|
824
|
+
|
|
825
|
+
if K_out:
|
|
826
|
+
return Phi_xi, Psi_xi, Sigma_xi, K_zeta
|
|
827
|
+
else:
|
|
828
|
+
return Phi_xi, Psi_xi, Sigma_xi
|