modulo-vki 2.0.7__py3-none-any.whl → 2.1.0__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.
@@ -0,0 +1,1368 @@
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.temporal_structures import dft, temporal_basis_mPOD
12
+ from modulo_vki.core._dmd_s import dmd_s
13
+ from modulo_vki.core._k_matrix import CorrelationMatrix, spectral_filter, kernelized_K
14
+ from modulo_vki.core._mpod_space import spatial_basis_mPOD
15
+ # from modulo_vki.core._mpod_time import temporal_basis_mPOD
16
+ from modulo_vki.core._pod_space import Spatial_basis_POD
17
+ from modulo_vki.core._pod_time import Temporal_basis_POD
18
+ from modulo_vki.core._spod_s import compute_SPOD_s
19
+ from modulo_vki.core._spod_t import compute_SPOD_t
20
+ from modulo_vki.utils._utils import switch_svds
21
+
22
+ from modulo_vki.utils.read_db import ReadData
23
+ from modulo_vki.core.utils import segment_and_fft, pod_from_dhat
24
+
25
+ class ModuloVKI:
26
+ """
27
+ MODULO (MODal mULtiscale pOd) is a software developed at the von Karman Institute
28
+ to perform Multiscale Modal Analysis using Multiscale Proper Orthogonal Decomposition (mPOD)
29
+ on numerical and experimental data.
30
+
31
+ References
32
+ ----------
33
+ - Theoretical foundation:
34
+ https://arxiv.org/abs/1804.09646
35
+
36
+ - MODULO framework presentation:
37
+ https://arxiv.org/pdf/2004.12123.pdf
38
+
39
+ - Hands-on tutorial videos:
40
+ https://youtube.com/playlist?list=PLEJZLD0-4PeKW6Ze984q08bNz28GTntkR
41
+
42
+ Notes
43
+ -----
44
+ MODULO operations assume the dataset is uniformly spaced in both space
45
+ (Cartesian grid) and time. For non-cartesian grids, the user must
46
+ provide a weights vector `[w_1, w_2, ..., w_Ns]` where `w_i = area_cell_i / area_grid`.
47
+ """
48
+
49
+ def __init__(self,
50
+ data: np.ndarray,
51
+ N_PARTITIONS: int = 1,
52
+ FOLDER_OUT: str = './',
53
+ SAVE_K: bool = False,
54
+ N_T: int = 100,
55
+ N_S: int = 200,
56
+ n_Modes: int = 10,
57
+ dtype: str = 'float32',
58
+ eig_solver: str = 'eigh',
59
+ svd_solver: str = 'svd_sklearn_truncated',
60
+ weights: np.ndarray = np.array([])):
61
+ """
62
+ Initialize the MODULO analysis.
63
+
64
+ Parameters
65
+ ----------
66
+ data : np.ndarray
67
+ Data matrix of shape (N_S, N_T) to factorize. If not yet formatted, use the `ReadData`
68
+ method provided by MODULO. When memory saving mode (N_PARTITIONS > 1) is active,
69
+ set this parameter to None and use prepared partitions instead.
70
+
71
+ N_PARTITIONS : int, default=1
72
+ Number of partitions used for memory-saving computation. If set greater than 1,
73
+ data must be partitioned in advance and `data` set to None.
74
+
75
+ FOLDER_OUT : str, default='./'
76
+ Directory path to store output (Phi, Sigma, Psi matrices) and intermediate
77
+ calculation files (e.g., partitions, correlation matrix).
78
+
79
+ SAVE_K : bool, default=False
80
+ Whether to store the correlation matrix K to disk in
81
+ `FOLDER_OUT/correlation_matrix`.
82
+
83
+ N_T : int, default=100
84
+ Number of temporal snapshots. Mandatory when using partitions (N_PARTITIONS > 1).
85
+
86
+ N_S : int, default=200
87
+ Number of spatial grid points. Mandatory when using partitions (N_PARTITIONS > 1).
88
+
89
+ n_Modes : int, default=10
90
+ Number of modes to compute.
91
+
92
+ dtype : str, default='float32'
93
+ Data type for casting input data.
94
+
95
+ eig_solver : str, default='eigh'
96
+ Solver for eigenvalue decomposition.
97
+
98
+ svd_solver : str, default='svd_sklearn_truncated'
99
+ Solver for Singular Value Decomposition (SVD).
100
+
101
+ weights : np.ndarray, default=np.array([])
102
+ Weights vector `[w_1, w_2, ..., w_Ns]` to account for non-uniform spatial grids.
103
+ Defined as `w_i = area_cell_i / area_grid`. Leave empty for uniform grids.
104
+ """
105
+
106
+ print("MODULO (MODal mULtiscale pOd) is a software developed at the von Karman Institute to perform "
107
+ "data driven modal decomposition of numerical and experimental data. \n")
108
+
109
+ if not isinstance(data, np.ndarray) and N_PARTITIONS == 1:
110
+ raise TypeError(
111
+ "Please check that your database is in an numpy array format. If D=None, then you must have memory saving (N_PARTITIONS>1)")
112
+
113
+ if N_PARTITIONS > 1:
114
+ self.MEMORY_SAVING = True
115
+ else:
116
+ self.MEMORY_SAVING = False
117
+
118
+ # Assign the number of modes
119
+ self.n_Modes = n_Modes
120
+ # If particular needs, override choice for svd and eigen solve
121
+ self.svd_solver = svd_solver.lower()
122
+ self.eig_solver = eig_solver.lower()
123
+ possible_svds = ['svd_numpy', 'svd_scipy_sparse', 'svd_sklearn_randomized', 'svd_sklearn_truncated']
124
+ possible_eigs = ['svd_sklearn_randomized', 'eigsh', 'eigh']
125
+
126
+ if self.svd_solver not in possible_svds:
127
+ raise NotImplementedError("The requested SVD solver is not implemented. Please pick one of the following:"
128
+ "which belongs to: \n {}".format(possible_svds))
129
+
130
+ if self.eig_solver not in possible_eigs:
131
+ raise NotImplementedError("The requested EIG solver is not implemented. Please pick one of the following: "
132
+ " \n {}".format(possible_eigs))
133
+
134
+ # if N_PARTITIONS >= self.N_T:
135
+ # raise AttributeError("The number of requested partitions is greater of the total columns (N_T). Please,"
136
+ # "try again.")
137
+
138
+ self.N_PARTITIONS = N_PARTITIONS
139
+ self.FOLDER_OUT = FOLDER_OUT
140
+ self.SAVE_K = SAVE_K
141
+
142
+ if self.MEMORY_SAVING:
143
+ os.makedirs(self.FOLDER_OUT, exist_ok=True)
144
+
145
+ # Load the data matrix
146
+ if isinstance(data, np.ndarray):
147
+ # Number of points in time and space
148
+ self.N_T = data.shape[1]
149
+ self.N_S = data.shape[0]
150
+ # Check the data type
151
+ self.D = data.astype(dtype)
152
+ else:
153
+ self.D = None # D is never saved when N_partitions >1
154
+ self.N_S = N_S # so N_S and N_t must be given as parameters of modulo
155
+ self.N_T = N_T
156
+
157
+ '''If the grid is not cartesian, ensure inner product is properly defined using weights.'''
158
+
159
+ if weights.size == 0:
160
+ print('Modulo assumes you have a uniform grid. If not, please provide weights as parameters.')
161
+ else:
162
+ if len(weights) == self.N_S:
163
+ print("The weights you have input have the size of the columns of D \n"
164
+ "MODULO has considered that you have already duplicated the dimensions of the weights "
165
+ "to match the dimensions of the D columns \n")
166
+ self.weights = weights
167
+ elif len(weights) == 2 * self.N_S:
168
+ print("Assuming 2D domain. Automatically duplicating the weights to match the dimension of the D columns \n")
169
+ self.weights = np.concatenate((weights, weights))
170
+ else:
171
+ raise AttributeError("Make sure the size of the weight array is twice smaller than the size of D")
172
+
173
+ if isinstance(data, np.ndarray):
174
+ # Apply the weights only if D exist.
175
+ # If not (i.e. N_partitions >1), weights are applied in _k_matrix.py when loading partitions of D
176
+ self.Dstar = np.transpose(np.transpose(self.D) * np.sqrt(self.weights))
177
+ else:
178
+ self.Dstar = None
179
+
180
+ # # Load and applied the weights to the D matrix
181
+ # if weights.size != 0:
182
+ # if len(weights) == self.N_S:
183
+ # print("The weights you have input have the size of the columns of D \n"
184
+ # "MODULO has considered that you have already duplicated the dimensions of the weights "
185
+ # "to match the dimensions of the D columns \n")
186
+ # self.weights = weights
187
+ # elif 2 * len(weights) == self.N_S: # 2D computation only
188
+ # self.weights = np.concatenate((weights, weights))
189
+ # print("Modulo assumes you have a 2D domain and has duplicated the weight "
190
+ # "array to match the size of the D columns \n")
191
+ # print(weights)
192
+ # else:
193
+ # raise AttributeError("Make sure the size of the weight array is twice smaller than the size of D")
194
+ # # Dstar is used to compute the K matrix
195
+ # if isinstance(data, np.ndarray):
196
+ # # Apply the weights only if D exist.
197
+ # # If not (i.e. N_partitions >1), weights are applied in _k_matrix.py when loading partitions of D
198
+ # self.Dstar = np.transpose(np.transpose(self.D) * np.sqrt(self.weights))
199
+ # else:
200
+ # self.Dstar = None
201
+ # else:
202
+ # print("Modulo assumes you have a uniform grid. "
203
+ # "If not, please give the weights as parameters of MODULO!")
204
+ # self.weights = weights
205
+ # self.Dstar = self.D
206
+
207
+ pass
208
+
209
+
210
+
211
+ def _temporal_basis_POD(self,
212
+ SAVE_T_POD: bool = False):
213
+ """
214
+ This method computes the temporal structure for the Proper Orthogonal Decomposition (POD) computation.
215
+ The theoretical background of the POD is briefly recalled here:
216
+
217
+ https://youtu.be/8fhupzhAR_M
218
+
219
+ The diagonalization of K is computed via Singular Value Decomposition (SVD).
220
+ A speedup is available if the user is on Linux machine, in which case MODULO
221
+ exploits the power of JAX and its Numpy implementation.
222
+
223
+ For more on JAX:
224
+
225
+ https://github.com/google/jax
226
+ https://jax.readthedocs.io/en/latest/jax.numpy.html
227
+
228
+ If the user is on a Win machine, Linux OS can be used using
229
+ the Windows Subsystem for Linux.
230
+
231
+ For more on WSL:
232
+ https://docs.microsoft.com/en-us/windows/wsl/install-win10
233
+
234
+ :param SAVE_T_POD: bool
235
+ Flag deciding if the results will be stored on the disk.
236
+ Default value is True, to limit the RAM's usage.
237
+ Note that this might cause a minor slowdown for the loading,
238
+ but the tradeoff seems worthy.
239
+ This attribute is passed to the MODULO class.
240
+
241
+
242
+ POD temporal basis are returned if MEMORY_SAVING is not active. Otherwise all the results are saved on disk.
243
+
244
+ :return Psi_P: np.array
245
+ POD Psis
246
+
247
+ :return Sigma_P: np.array
248
+ POD Sigmas. If needed, Lambdas can be easily computed recalling that: Sigma_P = np.sqrt(Lambda_P)
249
+ """
250
+
251
+ if self.MEMORY_SAVING:
252
+ K = np.load(self.FOLDER_OUT + "/correlation_matrix/k_matrix.npz")['K']
253
+ SAVE_T_POD = True
254
+ else:
255
+ K = self.K
256
+
257
+ Psi_P, Sigma_P = Temporal_basis_POD(K, SAVE_T_POD,
258
+ self.FOLDER_OUT, self.n_Modes, self.eig_solver)
259
+
260
+ del K
261
+ return Psi_P, Sigma_P if not self.MEMORY_SAVING else None
262
+
263
+ def _spatial_basis_POD(self, Psi_P, Sigma_P,
264
+ SAVE_SPATIAL_POD: bool = True):
265
+ """
266
+ This method computes the spatial structure for the Proper Orthogonal Decomposition (POD) computation.
267
+ The theoretical background of the POD is briefly recalled here:
268
+
269
+ https://youtu.be/8fhupzhAR_M
270
+
271
+ :param Psi_P: np.array
272
+ POD temporal basis
273
+ :param Sigma_P: np.array
274
+ POD Sigmas
275
+ :param SAVE_SPATIAL_POD: bool
276
+ Flag deciding if the results will be stored on the disk.
277
+ Default value is True, to limit the RAM's usage.
278
+ Note that this might cause a minor slowdown for the loading,
279
+ but the tradeoff seems worthy.
280
+ This attribute is passed to the MODULO class.
281
+
282
+ :return Phi_P: np.array
283
+ POD Phis
284
+
285
+ """
286
+
287
+ self.SAVE_SPATIAL_POD = SAVE_SPATIAL_POD
288
+
289
+ if self.MEMORY_SAVING:
290
+ '''Loading temporal basis from disk. They're already in memory otherwise.'''
291
+ Psi_P = np.load(self.FOLDER_OUT + 'POD/temporal_basis.npz')['Psis']
292
+ Sigma_P = np.load(self.FOLDER_OUT + 'POD/temporal_basis.npz')['Sigmas']
293
+
294
+ Phi_P = Spatial_basis_POD(self.D, N_T=self.N_T, PSI_P=Psi_P, Sigma_P=Sigma_P,
295
+ MEMORY_SAVING=self.MEMORY_SAVING, FOLDER_OUT=self.FOLDER_OUT,
296
+ N_PARTITIONS=self.N_PARTITIONS, SAVE_SPATIAL_POD=SAVE_SPATIAL_POD)
297
+
298
+ return Phi_P if not self.MEMORY_SAVING else None
299
+
300
+ def _temporal_basis_mPOD(self, K, Nf, Ex, F_V, Keep, boundaries, MODE, dt, K_S=False):
301
+ """
302
+ This function computes the temporal structures of each scale in the mPOD, as in step 4 of the algorithm
303
+ ref: Multi-Scale Proper Orthogonal Decomposition of Complex Fluid Flows - M. A. Mendez et al.
304
+
305
+ :param K: np.array
306
+ Temporal correlation matrix
307
+ :param Nf: np.array
308
+ Order of the FIR filters that are used to isolate each of the scales
309
+ :param Ex: int
310
+ Extension at the boundaries of K to impose the boundary conditions (see boundaries)
311
+ It must be at least as Nf.
312
+ :param F_V: np.array
313
+ Frequency splitting vector, containing the frequencies of each scale (see article).
314
+ If the time axis is in seconds, these frequencies are in Hz.
315
+ :param Keep: np.array
316
+ Scale keep
317
+ :param boundaries: str -> {'nearest', 'reflect', 'wrap' or 'extrap'}
318
+ Define the boundary conditions for the filtering process, in order to avoid edge effects.
319
+ The available boundary conditions are the classic ones implemented for image processing:
320
+ nearest', 'reflect', 'wrap' or 'extrap'. See also https://docs.scipy.org/doc/scipy/reference/tutorial/ndimage.html
321
+ :param MODE: str -> {‘reduced’, ‘complete’, ‘r’, ‘raw’}
322
+ A QR factorization is used to enforce the orthonormality of the mPOD basis, to compensate
323
+ for the non-ideal frequency response of the filters.
324
+ The option MODE from np.linalg.qr carries out this operation.
325
+
326
+ :return PSI_M: np.array
327
+ Multiscale POD temporal basis
328
+
329
+ """
330
+
331
+ if self.MEMORY_SAVING:
332
+ K = np.load(self.FOLDER_OUT + "/correlation_matrix/k_matrix.npz")['K']
333
+
334
+ PSI_M = temporal_basis_mPOD(K=K, Nf=Nf, Ex=Ex, F_V=F_V, Keep=Keep, boundaries=boundaries,
335
+ MODE=MODE, dt=dt, FOLDER_OUT=self.FOLDER_OUT,
336
+ n_Modes=self.n_Modes, K_S=False,
337
+ MEMORY_SAVING=self.MEMORY_SAVING, SAT=self.SAT, eig_solver=self.eig_solver)
338
+
339
+ return PSI_M if not self.MEMORY_SAVING else None
340
+
341
+ def _spatial_basis_mPOD(self, D, PSI_M, SAVE):
342
+ """
343
+ This function implements the last step of the mPOD algorithm:
344
+ completing the decomposition. Here we project from psis, to get phis and sigmas
345
+
346
+ :param D: np.array
347
+ data matrix
348
+ :param PSI_M: np.array
349
+ temporal basis for the mPOD. Remember that it is not possible to impose both basis matrices
350
+ phis and psis: given one of the two, the other is univocally determined.
351
+ :param SAVE: bool
352
+ if True, MODULO saves the results on disk.
353
+
354
+ :return Phi_M: np.array
355
+ mPOD Phis (Matrix of spatial structures)
356
+ :return Psi_M: np.array
357
+ mPOD Psis (Matrix of temporal structures)
358
+ :return Sigma_M: np.array
359
+ mPOD Sigmas (vector of amplitudes, i.e. the diagonal of Sigma_M)
360
+
361
+ """
362
+
363
+ Phi_M, Psi_M, Sigma_M = spatial_basis_mPOD(D, PSI_M, N_T=self.N_T, N_PARTITIONS=self.N_PARTITIONS,
364
+ N_S=self.N_S, MEMORY_SAVING=self.MEMORY_SAVING,
365
+ FOLDER_OUT=self.FOLDER_OUT,
366
+ SAVE=SAVE)
367
+
368
+ return Phi_M, Psi_M, Sigma_M
369
+
370
+ def compute_mPOD(self, Nf, Ex, F_V, Keep, SAT, boundaries, MODE, dt, SAVE=False):
371
+ """
372
+ This function computes the temporal structures of each scale in the mPOD, as in step 4 of the algorithm
373
+ ref: Multi-Scale Proper Orthogonal Decomposition of Complex Fluid Flows - M. A. Mendez et al.
374
+
375
+ :param K: np.array
376
+ Temporal correlation matrix
377
+
378
+ :param Nf: np.array
379
+ Order of the FIR filters that are used to isolate each of the scales
380
+
381
+ :param Ex: int
382
+ Extension at the boundaries of K to impose the boundary conditions (see boundaries)
383
+ It must be at least as Nf.
384
+
385
+ :param F_V: np.array
386
+ Frequency splitting vector, containing the frequencies of each scale (see article).
387
+ If the time axis is in seconds, these frequencies are in Hz.
388
+
389
+ :param Keep: np.array
390
+ Scale keep
391
+
392
+ :param boundaries: str -> {'nearest', 'reflect', 'wrap' or 'extrap'}
393
+ Define the boundary conditions for the filtering process, in order to avoid edge effects.
394
+ The available boundary conditions are the classic ones implemented for image processing:
395
+ nearest', 'reflect', 'wrap' or 'extrap'. See also https://docs.scipy.org/doc/scipy/reference/tutorial/ndimage.html
396
+
397
+ :param MODE: str -> {‘reduced’, ‘complete’, ‘r’, ‘raw’}
398
+ A QR factorization is used to enforce the orthonormality of the mPOD basis, to compensate
399
+ for the non-ideal frequency response of the filters.
400
+ The option MODE from np.linalg.qr carries out this operation.
401
+
402
+ :param SAT: Maximum number of modes per scale.
403
+ Only used for mPOD (max number of modes per scale)
404
+
405
+ :param dt: float
406
+ temporal step
407
+
408
+ :return Phi_M: np.array
409
+ mPOD Phis (Matrix of spatial structures)
410
+ :return Psi_M: np.array
411
+ mPOD Psis (Matrix of temporal structures)
412
+ :return Sigma_M: np.array
413
+ mPOD Sigmas (vector of amplitudes, i.e. the diagonal of Sigma_M
414
+
415
+ """
416
+
417
+ print('Computing correlation matrix D matrix...')
418
+ self.K = CorrelationMatrix(self.N_T, self.N_PARTITIONS,
419
+ self.MEMORY_SAVING,
420
+ self.FOLDER_OUT, self.SAVE_K, D=self.Dstar)
421
+
422
+ if self.MEMORY_SAVING:
423
+ self.K = np.load(self.FOLDER_OUT + '/correlation_matrix/k_matrix.npz')['K']
424
+
425
+ print("Computing Temporal Basis...")
426
+
427
+ PSI_M = temporal_basis_mPOD(K=self.K, Nf=Nf, Ex=Ex, F_V=F_V, Keep=Keep, boundaries=boundaries,
428
+ MODE=MODE, dt=dt, FOLDER_OUT=self.FOLDER_OUT,
429
+ n_Modes=self.n_Modes, MEMORY_SAVING=self.MEMORY_SAVING, SAT=SAT,
430
+ eig_solver=self.eig_solver)
431
+
432
+ print("Done.")
433
+
434
+ if hasattr(self, 'D'): # if self.D is available:
435
+ print('Computing Phi from D...')
436
+ Phi_M, Psi_M, Sigma_M = spatial_basis_mPOD(self.D, PSI_M, N_T=self.N_T, N_PARTITIONS=self.N_PARTITIONS,
437
+ N_S=self.N_S, MEMORY_SAVING=self.MEMORY_SAVING,
438
+ FOLDER_OUT=self.FOLDER_OUT,
439
+ SAVE=SAVE)
440
+
441
+ else: # if not, the memory saving is on and D will not be used. We pass a dummy D
442
+ print('Computing Phi from partitions...')
443
+ Phi_M, Psi_M, Sigma_M = spatial_basis_mPOD(np.array([1]), PSI_M, N_T=self.N_T,
444
+ N_PARTITIONS=self.N_PARTITIONS,
445
+ N_S=self.N_S, MEMORY_SAVING=self.MEMORY_SAVING,
446
+ FOLDER_OUT=self.FOLDER_OUT,
447
+ SAVE=SAVE)
448
+
449
+ print("Done.")
450
+
451
+ return Phi_M, Psi_M, Sigma_M
452
+
453
+
454
+ def compute_POD_K(self, SAVE_T_POD: bool = False):
455
+ """
456
+ This method computes the Proper Orthogonal Decomposition (POD) of a dataset
457
+ using the snapshot approach, i.e. working on the temporal correlation matrix.
458
+ The eig solver for K is defined in 'eig_solver'
459
+ The theoretical background of the POD is briefly recalled here:
460
+
461
+ https://youtu.be/8fhupzhAR_M
462
+
463
+ :return Psi_P: np.array
464
+ POD Psis
465
+
466
+ :return Sigma_P: np.array
467
+ POD Sigmas. If needed, Lambdas can be easily computed recalling that: Sigma_P = np.sqrt(Lambda_P)
468
+
469
+ :return Phi_P: np.array
470
+ POD Phis
471
+ """
472
+
473
+ print('Computing correlation matrix...')
474
+ self.K = CorrelationMatrix(self.N_T, self.N_PARTITIONS,
475
+ self.MEMORY_SAVING,
476
+ self.FOLDER_OUT, self.SAVE_K,
477
+ D=self.Dstar, weights=self.weights)
478
+
479
+ if self.MEMORY_SAVING:
480
+ self.K = np.load(self.FOLDER_OUT + '/correlation_matrix/k_matrix.npz')['K']
481
+
482
+ print("Computing Temporal Basis...")
483
+ Psi_P, Sigma_P = Temporal_basis_POD(self.K, SAVE_T_POD,
484
+ self.FOLDER_OUT, self.n_Modes, eig_solver=self.eig_solver)
485
+ print("Done.")
486
+ print("Computing Spatial Basis...")
487
+
488
+ if self.MEMORY_SAVING: # if self.D is available:
489
+ print('Computing Phi from partitions...')
490
+ Phi_P = Spatial_basis_POD(np.array([1]), N_T=self.N_T,
491
+ PSI_P=Psi_P,
492
+ Sigma_P=Sigma_P,
493
+ MEMORY_SAVING=self.MEMORY_SAVING,
494
+ FOLDER_OUT=self.FOLDER_OUT,
495
+ N_PARTITIONS=self.N_PARTITIONS)
496
+
497
+ else: # if not, the memory saving is on and D will not be used. We pass a dummy D
498
+ print('Computing Phi from D...')
499
+ Phi_P = Spatial_basis_POD(self.D, N_T=self.N_T,
500
+ PSI_P=Psi_P,
501
+ Sigma_P=Sigma_P,
502
+ MEMORY_SAVING=self.MEMORY_SAVING,
503
+ FOLDER_OUT=self.FOLDER_OUT,
504
+ N_PARTITIONS=self.N_PARTITIONS)
505
+ print("Done.")
506
+
507
+ return Phi_P, Psi_P, Sigma_P
508
+
509
+ def compute_POD_svd(self, SAVE_T_POD: bool = False):
510
+ """
511
+ This method computes the Proper Orthogonal Decomposition (POD) of a dataset
512
+ using the SVD decomposition. The svd solver is defined by 'svd_solver'.
513
+ Note that in this case, the memory saving option is of no help, since
514
+ the SVD must be performed over the entire dataset.
515
+
516
+ https://youtu.be/8fhupzhAR_M
517
+
518
+ :return Psi_P: np.array
519
+ POD Psis
520
+
521
+ :return Sigma_P: np.array
522
+ POD Sigmas. If needed, Lambdas can be easily computed recalling that: Sigma_P = np.sqrt(Lambda_P)
523
+
524
+ :return Phi_P: np.array
525
+ POD Phis
526
+ """
527
+ # If Memory saving is active, we must load back the data.
528
+ # This process is memory demanding. Different SVD solver will handle this differently.
529
+
530
+ if self.MEMORY_SAVING:
531
+ if self.N_T % self.N_PARTITIONS != 0:
532
+ tot_blocks_col = self.N_PARTITIONS + 1
533
+ else:
534
+ tot_blocks_col = self.N_PARTITIONS
535
+
536
+ # Prepare the D matrix again
537
+ D = np.zeros((self.N_S, self.N_T))
538
+ R1 = 0
539
+
540
+ # print(' \n Reloading D from tmp...')
541
+ for k in tqdm(range(tot_blocks_col)):
542
+ di = np.load(self.FOLDER_OUT + f"/data_partitions/di_{k + 1}.npz")['di']
543
+ R2 = R1 + np.shape(di)[1]
544
+ D[:, R1:R2] = di
545
+ R1 = R2
546
+
547
+ # Now that we have D back, we can proceed with the SVD approach
548
+ Phi_P, Psi_P, Sigma_P = switch_svds(D, self.n_Modes, self.svd_solver)
549
+
550
+
551
+ else: # self.MEMORY_SAVING:
552
+ Phi_P, Psi_P, Sigma_P = switch_svds(self.D, self.n_Modes, self.svd_solver)
553
+
554
+ return Phi_P, Psi_P, Sigma_P
555
+
556
+ def compute_DMD_PIP(self, SAVE_T_DMD: bool = True, F_S=1):
557
+ """
558
+ This method computes the Dynamic Mode Decomposition of the data
559
+ using the algorithm in https://arxiv.org/abs/1312.0041, which is basically the same as
560
+ the PIP algorithm proposed in https://www.sciencedirect.com/science/article/abs/pii/0167278996001248
561
+ See v1 of this paper https://arxiv.org/abs/2001.01971 for more details (yes, reviewers did ask to omit this detail in v2).
562
+
563
+ :return Phi_D: np.array
564
+ DMD Phis. As for the DFT, these are complex.
565
+
566
+ :return Lambda_D: np.array
567
+ DMD Eigenvalues (of the reduced propagator). These are complex.
568
+
569
+ :return freqs: np.array
570
+ Frequencies (in Hz, associated to the DMD modes)
571
+
572
+ :return a0s: np.array
573
+ Initial Coefficients of the Modes
574
+
575
+ """
576
+
577
+ # If Memory saving is active, we must load back the data
578
+ if self.MEMORY_SAVING:
579
+ if self.N_T % self.N_PARTITIONS != 0:
580
+ tot_blocks_col = self.N_PARTITIONS + 1
581
+ else:
582
+ tot_blocks_col = self.N_PARTITIONS
583
+
584
+ # Prepare the D matrix again
585
+ D = np.zeros((self.N_S, self.N_T))
586
+ R1 = 0
587
+
588
+ # print(' \n Reloading D from tmp...')
589
+ for k in tqdm(range(tot_blocks_col)):
590
+ di = np.load(self.FOLDER_OUT + f"/data_partitions/di_{k + 1}.npz")['di']
591
+ R2 = R1 + np.shape(di)[1]
592
+ D[:, R1:R2] = di
593
+ R1 = R2
594
+
595
+ # Compute the DMD
596
+ Phi_D, Lambda, freqs, a0s = dmd_s(D[:, 0:self.N_T - 1],
597
+ D[:, 1:self.N_T], self.n_Modes, F_S, svd_solver=self.svd_solver)
598
+
599
+ else:
600
+ Phi_D, Lambda, freqs, a0s = dmd_s(self.D[:, 0:self.N_T - 1],
601
+ self.D[:, 1:self.N_T], self.n_Modes, F_S, SAVE_T_DMD=SAVE_T_DMD,
602
+ svd_solver=self.svd_solver, FOLDER_OUT=self.FOLDER_OUT)
603
+
604
+ return Phi_D, Lambda, freqs, a0s
605
+
606
+ def compute_DFT(self, F_S, SAVE_DFT=False):
607
+ """
608
+ This method computes the Discrete Fourier Transform of your data.
609
+
610
+ Check out this tutorial: https://www.youtube.com/watch?v=8fhupzhAR_M&list=PLEJZLD0-4PeKW6Ze984q08bNz28GTntkR&index=2
611
+
612
+ :param F_S: float,
613
+ Sampling Frequency [Hz]
614
+ :param SAVE_DFT: bool,
615
+ If True, MODULO will save the output in self.FOLDER OUT/MODULO_tmp
616
+
617
+ :return: Sorted_Freqs: np.array,
618
+ Sorted Frequencies
619
+ :return Phi_F: np.array,
620
+ DFT Phis
621
+ :return Sigma_F: np.array,
622
+ DFT Sigmas
623
+ """
624
+ if self.D is None:
625
+ D = np.load(self.FOLDER_OUT + '/MODULO_tmp/data_matrix/database.npz')['D']
626
+ SAVE_DFT = True
627
+ Sorted_Freqs, Phi_F, SIGMA_F = dft_fit(self.N_T, F_S, D, self.FOLDER_OUT, SAVE_DFT=SAVE_DFT)
628
+
629
+ else:
630
+ Sorted_Freqs, Phi_F, SIGMA_F = dft_fit(self.N_T, F_S, self.D, self.FOLDER_OUT, SAVE_DFT=SAVE_DFT)
631
+
632
+ return Sorted_Freqs, Phi_F, SIGMA_F
633
+
634
+ def compute_SPOD_t(self, F_S, L_B=500, O_B=250, n_Modes=10, SAVE_SPOD=True):
635
+ """
636
+ This method computes the Spectral POD of your data. This is the one by Towne et al
637
+ (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)
638
+
639
+ :param F_S: float,
640
+ Sampling Frequency [Hz]
641
+ :param L_B: float,
642
+ lenght of the chunks
643
+ :param O_B: float,
644
+ Overlapping between blocks in the chunk
645
+ :param n_Modes: float,
646
+ number of modes to be computed for each frequency
647
+ :param SAVE_SPOD: bool,
648
+ If True, MODULO will save the output in self.FOLDER OUT/MODULO_tmp
649
+ :return Psi_P_hat: np.array
650
+ Spectra of the SPOD Modes
651
+ :return Sigma_P: np.array
652
+ Amplitudes of the SPOD Modes.
653
+ :return Phi_P: np.array
654
+ SPOD Phis
655
+ :return freq: float
656
+ frequency bins for the Spectral POD
657
+
658
+
659
+ """
660
+ if self.D is None:
661
+ D = np.load(self.FOLDER_OUT + '/MODULO_tmp/data_matrix/database.npz')['D']
662
+ Phi_SP, Sigma_SP, Freqs_Pos = compute_SPOD_t(D, F_S, L_B=L_B, O_B=O_B,
663
+ n_Modes=n_Modes, SAVE_SPOD=SAVE_SPOD,
664
+ FOLDER_OUT=self.FOLDER_OUT, possible_svds=self.svd_solver)
665
+ else:
666
+ Phi_SP, Sigma_SP, Freqs_Pos = compute_SPOD_t(self.D, F_S, L_B=L_B, O_B=O_B,
667
+ n_Modes=n_Modes, SAVE_SPOD=SAVE_SPOD,
668
+ FOLDER_OUT=self.FOLDER_OUT, possible_svds=self.svd_solver)
669
+
670
+ return Phi_SP, Sigma_SP, Freqs_Pos
671
+
672
+ # New Decomposition: SPOD f
673
+
674
+ def compute_SPOD_s(self, F_S, N_O=100, f_c=0.3, n_Modes=10, SAVE_SPOD=True):
675
+ """
676
+ This method computes the Spectral POD of your data.
677
+ This is the one by Sieber
678
+ et al (https://www.cambridge.org/core/journals/journal-of-fluid-mechanics/article/abs/spectral-proper-orthogonal-decomposition/DCD8A6EDEFD56F5A9715DBAD38BD461A)
679
+
680
+ :param F_S: float,
681
+ Sampling Frequency [Hz]
682
+ :param N_o: float,
683
+ Semi-Order of the diagonal filter.
684
+ Note that the filter order will be 2 N_o +1 (to make sure it is odd)
685
+ :param f_c: float,
686
+ cut-off frequency of the diagonal filter
687
+ :param n_Modes: float,
688
+ number of modes to be computed
689
+ :param SAVE_SPOD: bool,
690
+ If True, MODULO will save the output in self.FOLDER OUT/MODULO_tmp
691
+ :return Psi_P: np.array
692
+ SPOD Psis
693
+ :return Sigma_P: np.array
694
+ SPOD Sigmas.
695
+ :return Phi_P: np.array
696
+ SPOD Phis
697
+ """
698
+
699
+ if self.D is None:
700
+ D = np.load(self.FOLDER_OUT + '/MODULO_tmp/data_matrix/database.npz')['D']
701
+
702
+ self.K = CorrelationMatrix(self.N_T, self.N_PARTITIONS, self.MEMORY_SAVING,
703
+ self.FOLDER_OUT, self.SAVE_K, D=D)
704
+
705
+ Phi_sP, Psi_sP, Sigma_sP = compute_SPOD_s(D, self.K, F_S, self.N_S, self.N_T, N_O, f_c,
706
+ n_Modes, SAVE_SPOD, self.FOLDER_OUT, self.MEMORY_SAVING,
707
+ self.N_PARTITIONS)
708
+
709
+ else:
710
+ self.K = CorrelationMatrix(self.N_T, self.N_PARTITIONS, self.MEMORY_SAVING,
711
+ self.FOLDER_OUT, self.SAVE_K, D=self.D)
712
+
713
+ 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,
714
+ n_Modes, SAVE_SPOD, self.FOLDER_OUT, self.MEMORY_SAVING,
715
+ self.N_PARTITIONS)
716
+
717
+ return Phi_sP, Psi_sP, Sigma_sP
718
+
719
+ def compute_kPOD(self, M_DIST=[1, 10], k_m=0.1, cent=True,
720
+ n_Modes=10, alpha=1e-6, metric='rbf', K_out=False):
721
+ """
722
+ 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
723
+
724
+ The computation of the kernel function is carried out as in https://arxiv.org/pdf/2208.07746.pdf.
725
+
726
+
727
+ :param M_DIST: array,
728
+ position of the two snapshots that will be considered to
729
+ estimate the minimal k. They should be the most different ones.
730
+ :param k_m: float,
731
+ minimum value for the kernelized correlation
732
+ :param alpha: float
733
+ regularization for K_zeta
734
+ :param cent: bool,
735
+ if True, the matrix K is centered. Else it is not
736
+ :param n_Modes: float,
737
+ number of modes to be computed
738
+ :param metric: string,
739
+ This identifies the metric for the kernel matrix. It is a wrapper to 'pairwise_kernels' from sklearn.metrics.pairwise
740
+ Note that different metrics would need different set of parameters. For the moment, only rbf was tested; use any other option at your peril !
741
+ :param K_out: bool,
742
+ If true, the matrix K is also exported as a fourth output.
743
+ :return Psi_xi: np.array
744
+ kPOD's Psis
745
+ :return Sigma_xi: np.array
746
+ kPOD's Sigmas.
747
+ :return Phi_xi: np.array
748
+ kPOD's Phis
749
+ :return K_zeta: np.array
750
+ Kernel Function from which the decomposition is computed.
751
+ (exported only if K_out=True)
752
+
753
+
754
+ """
755
+ if self.D is None:
756
+ D = np.load(self.FOLDER_OUT + '/MODULO_tmp/data_matrix/database.npz')['D']
757
+ else:
758
+ D = self.D
759
+
760
+ # Compute Eucledean distances
761
+ i, j = M_DIST;
762
+ n_s, n_t = np.shape(D)
763
+ M_ij = np.linalg.norm(D[:, i] - D[:, j]) ** 2
764
+
765
+ gamma = -np.log(k_m) / M_ij
766
+
767
+ K_zeta = pairwise_kernels(D.T, metric='rbf', gamma=gamma)
768
+ print('Kernel K ready')
769
+
770
+ # Compute the Kernel Matrix
771
+ n_t = np.shape(D)[1]
772
+ # Center the Kernel Matrix (if cent is True):
773
+ if cent:
774
+ H = np.eye(n_t) - 1 / n_t * np.ones_like(K_zeta)
775
+ K_zeta = H @ K_zeta @ H.T
776
+ print('K_zeta centered')
777
+ # Diagonalize and Sort
778
+ lambdas, Psi_xi = linalg.eigh(K_zeta + alpha * np.eye(n_t), subset_by_index=[n_t - n_Modes, n_t - 1])
779
+ lambdas, Psi_xi = lambdas[::-1], Psi_xi[:, ::-1];
780
+ Sigma_xi = np.sqrt(lambdas);
781
+ print('K_zeta diagonalized')
782
+ # Encode
783
+ # Z_xi=np.diag(Sigma_xi)@Psi_xi.T
784
+ # We compute the spatial structures as projections of the data
785
+ # onto the Psi_xi!
786
+ R = Psi_xi.shape[1]
787
+ PHI_xi_SIGMA_xi = np.dot(D, (Psi_xi))
788
+ # Initialize the output
789
+ PHI_xi = np.zeros((n_s, R))
790
+ SIGMA_xi = np.zeros((R))
791
+
792
+ for i in tqdm(range(0, R)):
793
+ # Assign the norm as amplitude
794
+ SIGMA_xi[i] = np.linalg.norm(PHI_xi_SIGMA_xi[:, i])
795
+ # Normalize the columns of C to get spatial modes
796
+ PHI_xi[:, i] = PHI_xi_SIGMA_xi[:, i] / SIGMA_xi[i]
797
+
798
+ Indices = np.flipud(np.argsort(SIGMA_xi)) # find indices for sorting in decreasing order
799
+ Sorted_Sigmas = SIGMA_xi[Indices] # Sort all the sigmas
800
+ Phi_xi = PHI_xi[:, Indices] # Sorted Spatial Structures Matrix
801
+ Psi_xi = Psi_xi[:, Indices] # Sorted Temporal Structures Matrix
802
+ Sigma_xi = Sorted_Sigmas # Sorted Amplitude Matrix
803
+ print('Phi_xi computed')
804
+
805
+ if K_out:
806
+ return Phi_xi, Psi_xi, Sigma_xi, K_zeta
807
+ else:
808
+ return Phi_xi, Psi_xi, Sigma_xi
809
+
810
+
811
+ # --- updated functions down here --- #
812
+
813
+ def mPOD(self, Nf, Ex, F_V, Keep, SAT, boundaries, MODE, dt, SAVE=False, K_in=None):
814
+ """
815
+ Multi-Scale Proper Orthogonal Decomposition (mPOD) of a signal.
816
+
817
+ Parameters
818
+ ----------
819
+ Nf : np.array
820
+ Orders of the FIR filters used to isolate each scale.
821
+
822
+ Ex : int
823
+ Extension length at the boundaries to impose boundary conditions (must be at least as large as Nf).
824
+
825
+ F_V : np.array
826
+ Frequency splitting vector, containing the cutoff frequencies for each scale. Units depend on the temporal step `dt`.
827
+
828
+ Keep : np.array
829
+ Boolean array indicating scales to retain.
830
+
831
+ SAT : int
832
+ Maximum number of modes per scale.
833
+
834
+ boundaries : {'nearest', 'reflect', 'wrap', 'extrap'}
835
+ Boundary conditions for filtering to avoid edge effects. Refer to:
836
+ https://docs.scipy.org/doc/scipy/reference/tutorial/ndimage.html
837
+
838
+ MODE : {'reduced', 'complete', 'r', 'raw'}
839
+ Mode option for QR factorization, used to enforce orthonormality of the mPOD basis to account for non-ideal filter responses.
840
+
841
+ dt : float
842
+ Temporal step size between snapshots.
843
+
844
+ SAVE : bool, default=False
845
+ Whether to save intermediate results to disk.
846
+
847
+ load_existing_K : bool, default=True
848
+ If True and MEMORY_SAVING is active, attempts to load an existing correlation matrix K from disk,
849
+ skipping recomputation if possible.
850
+
851
+ Returns
852
+ -------
853
+ Phi_M : np.array
854
+ Spatial mPOD modes (spatial structures matrix).
855
+
856
+ Psi_M : np.array
857
+ Temporal mPOD modes (temporal structures matrix).
858
+
859
+ Sigma_M : np.array
860
+ Modal amplitudes.
861
+ """
862
+
863
+ if K_in is None:
864
+ print('Computing correlation matrix D matrix...')
865
+ self.K = CorrelationMatrix(self.N_T, self.N_PARTITIONS,
866
+ self.MEMORY_SAVING,
867
+ self.FOLDER_OUT, self.SAVE_K, D=self.Dstar)
868
+
869
+ if self.MEMORY_SAVING:
870
+ self.K = np.load(self.FOLDER_OUT + '/correlation_matrix/k_matrix.npz')['K']
871
+ else:
872
+ print('Using K matrix provided by the user...')
873
+ self.K = K_in
874
+
875
+
876
+ print("Computing Temporal Basis...")
877
+ PSI_M = temporal_basis_mPOD(
878
+ K=self.K, Nf=Nf, Ex=Ex, F_V=F_V, Keep=Keep, boundaries=boundaries,
879
+ MODE=MODE, dt=dt, FOLDER_OUT=self.FOLDER_OUT,
880
+ n_Modes=self.n_Modes, MEMORY_SAVING=self.MEMORY_SAVING, SAT=SAT,
881
+ eig_solver=self.eig_solver
882
+ )
883
+ print("Temporal Basis computed.")
884
+
885
+ if hasattr(self, 'D'):
886
+ print('Computing spatial modes Phi from D...')
887
+ Phi_M, Psi_M, Sigma_M = spatial_basis_mPOD(
888
+ self.D, PSI_M, N_T=self.N_T, N_PARTITIONS=self.N_PARTITIONS,
889
+ N_S=self.N_S, MEMORY_SAVING=self.MEMORY_SAVING,
890
+ FOLDER_OUT=self.FOLDER_OUT, SAVE=SAVE
891
+ )
892
+ else:
893
+ print('Computing spatial modes Phi from partitions...')
894
+ Phi_M, Psi_M, Sigma_M = spatial_basis_mPOD(
895
+ np.array([1]), PSI_M, N_T=self.N_T,
896
+ N_PARTITIONS=self.N_PARTITIONS, N_S=self.N_S,
897
+ MEMORY_SAVING=self.MEMORY_SAVING,
898
+ FOLDER_OUT=self.FOLDER_OUT, SAVE=SAVE
899
+ )
900
+
901
+ print("Spatial modes computed.")
902
+
903
+ return Phi_M, Psi_M, Sigma_M
904
+
905
+ def DFT(self, F_S, SAVE_DFT=False):
906
+ """
907
+ Computes the Discrete Fourier Transform (DFT) of the dataset.
908
+
909
+ For detailed guidance, see the tutorial video:
910
+ https://www.youtube.com/watch?v=8fhupzhAR_M&list=PLEJZLD0-4PeKW6Ze984q08bNz28GTntkR&index=2
911
+
912
+ Parameters
913
+ ----------
914
+ F_S : float
915
+ Sampling frequency in Hz.
916
+
917
+ SAVE_DFT : bool, default=False
918
+ If True, saves the computed DFT outputs to disk under:
919
+ `self.FOLDER_OUT/MODULO_tmp`.
920
+
921
+ Returns
922
+ -------
923
+ Phi_F : np.ndarray
924
+ Spatial DFT modes (spatial structures matrix).
925
+
926
+ Psi_F : np.ndarray
927
+ Temporal DFT modes (temporal structures matrix).
928
+
929
+ Sigma_F : np.ndarray
930
+ Modal amplitudes.
931
+ """
932
+ if self.D is None:
933
+ D = np.load(self.FOLDER_OUT + '/MODULO_tmp/data_matrix/database.npz')['D']
934
+ SAVE_DFT = True
935
+ Phi_F, Psi_F, Sigma_F = dft(self.N_T, F_S, D, self.FOLDER_OUT, SAVE_DFT=SAVE_DFT)
936
+
937
+ else:
938
+ Phi_F, Psi_F, Sigma_F = dft(self.N_T, F_S, self.D, self.FOLDER_OUT, SAVE_DFT=SAVE_DFT)
939
+
940
+ return Phi_F, Psi_F, Sigma_F
941
+
942
+
943
+ def POD(self, SAVE_T_POD: bool = False, mode: str = 'K'):
944
+ """
945
+ Compute the Proper Orthogonal Decomposition (POD) of a dataset.
946
+
947
+ The POD is computed using the snapshot approach, working on the
948
+ temporal correlation matrix. The eigenvalue solver for this
949
+ matrix is defined in the `eig_solver` attribute of the class.
950
+
951
+ Parameters
952
+ ----------
953
+ SAVE_T_POD : bool, optional
954
+ Flag to save time-dependent POD data. Default is False.
955
+ mode : str, optional
956
+ The mode of POD computation. Must be either 'K' or 'svd'.
957
+ 'K' (default) uses the snapshot method on the temporal
958
+ correlation matrix.
959
+ 'svd' uses the SVD decomposition (full dataset must fit in memory).
960
+
961
+ Returns
962
+ -------
963
+ Psi_P : numpy.ndarray
964
+ POD spatial modes.
965
+ Sigma_P : numpy.ndarray
966
+ POD singular values (eigenvalues are Sigma_P**2).
967
+ Phi_P : numpy.ndarray
968
+ POD temporal modes.
969
+
970
+ Raises
971
+ ------
972
+ ValueError
973
+ If `mode` is not 'k' or 'svd'.
974
+
975
+ Notes
976
+ -----
977
+ A brief recall of the theoretical background of the POD is
978
+ available at https://youtu.be/8fhupzhAR_M
979
+ """
980
+
981
+ mode = mode.lower()
982
+ assert mode in ('k', 'svd'), "POD mode must be either 'K', temporal correlation matrix, or 'svd'."
983
+
984
+ if mode == 'k':
985
+
986
+ print('Computing correlation matrix...')
987
+ self.K = CorrelationMatrix(self.N_T, self.N_PARTITIONS,
988
+ self.MEMORY_SAVING,
989
+ self.FOLDER_OUT, self.SAVE_K, D=self.Dstar, weights=self.weights)
990
+
991
+ if self.MEMORY_SAVING:
992
+ self.K = np.load(self.FOLDER_OUT + '/correlation_matrix/k_matrix.npz')['K']
993
+
994
+ print("Computing Temporal Basis...")
995
+ Psi_P, Sigma_P = Temporal_basis_POD(self.K, SAVE_T_POD,
996
+ self.FOLDER_OUT, self.n_Modes, eig_solver=self.eig_solver)
997
+ print("Done.")
998
+ print("Computing Spatial Basis...")
999
+
1000
+ if self.MEMORY_SAVING: # if self.D is available:
1001
+ print('Computing Phi from partitions...')
1002
+ Phi_P = Spatial_basis_POD(np.array([1]), N_T=self.N_T,
1003
+ PSI_P=Psi_P,
1004
+ Sigma_P=Sigma_P,
1005
+ MEMORY_SAVING=self.MEMORY_SAVING,
1006
+ FOLDER_OUT=self.FOLDER_OUT,
1007
+ N_PARTITIONS=self.N_PARTITIONS)
1008
+
1009
+ else: # if not, the memory saving is on and D will not be used. We pass a dummy D
1010
+ print('Computing Phi from D...')
1011
+ Phi_P = Spatial_basis_POD(self.D, N_T=self.N_T,
1012
+ PSI_P=Psi_P,
1013
+ Sigma_P=Sigma_P,
1014
+ MEMORY_SAVING=self.MEMORY_SAVING,
1015
+ FOLDER_OUT=self.FOLDER_OUT,
1016
+ N_PARTITIONS=self.N_PARTITIONS)
1017
+ print("Done.")
1018
+
1019
+ else:
1020
+ if self.MEMORY_SAVING:
1021
+
1022
+ if self.N_T % self.N_PARTITIONS != 0:
1023
+ tot_blocks_col = self.N_PARTITIONS + 1
1024
+ else:
1025
+ tot_blocks_col = self.N_PARTITIONS
1026
+
1027
+ # Prepare the D matrix again
1028
+ D = np.zeros((self.N_S, self.N_T))
1029
+ R1 = 0
1030
+
1031
+ # print(' \n Reloading D from tmp...')
1032
+ for k in tqdm(range(tot_blocks_col)):
1033
+ di = np.load(self.FOLDER_OUT + f"/data_partitions/di_{k + 1}.npz")['di']
1034
+ R2 = R1 + np.shape(di)[1]
1035
+ D[:, R1:R2] = di
1036
+ R1 = R2
1037
+
1038
+ # Now that we have D back, we can proceed with the SVD approach
1039
+ Phi_P, Psi_P, Sigma_P = switch_svds(D, self.n_Modes, self.svd_solver)
1040
+
1041
+ else: # self.MEMORY_SAVING:
1042
+ Phi_P, Psi_P, Sigma_P = switch_svds(self.D, self.n_Modes, self.svd_solver)
1043
+
1044
+ return Phi_P, Psi_P, Sigma_P
1045
+
1046
+
1047
+ def DMD(self, SAVE_T_DMD: bool = True, F_S: float = 1.0):
1048
+ """
1049
+ Compute the Dynamic Mode Decomposition (DMD) of the dataset.
1050
+
1051
+ This implementation follows the algorithm in Tu et al. (2014) [1]_, which is
1052
+ essentially the same as Penland (1996) [2]_. For
1053
+ additional low-level details see v1 of Mendez et al. (2020) [3]_.
1054
+
1055
+ Parameters
1056
+ ----------
1057
+ SAVE_T_DMD : bool, optional
1058
+ If True, save time-dependent DMD results to disk. Default is True.
1059
+ F_S : float, optional
1060
+ Sampling frequency in Hz. Default is 1.0.
1061
+
1062
+ Returns
1063
+ -------
1064
+ Phi_D : numpy.ndarray
1065
+ Complex DMD modes.
1066
+ Lambda_D : numpy.ndarray
1067
+ Complex eigenvalues of the reduced-order propagator.
1068
+ freqs : numpy.ndarray
1069
+ Frequencies (Hz) associated with each DMD mode.
1070
+ a0s : numpy.ndarray
1071
+ Initial amplitudes (coefficients) of the DMD modes.
1072
+
1073
+ References
1074
+ ----------
1075
+ .. [1] https://arxiv.org/abs/1312.0041
1076
+ .. [2] https://www.sciencedirect.com/science/article/pii/0167278996001248
1077
+ .. [3] https://arxiv.org/abs/2001.01971
1078
+ """
1079
+
1080
+ # If Memory saving is active, we must load back the data
1081
+ if self.MEMORY_SAVING:
1082
+ if self.N_T % self.N_PARTITIONS != 0:
1083
+ tot_blocks_col = self.N_PARTITIONS + 1
1084
+ else:
1085
+ tot_blocks_col = self.N_PARTITIONS
1086
+
1087
+ # Prepare the D matrix again
1088
+ D = np.zeros((self.N_S, self.N_T))
1089
+ R1 = 0
1090
+
1091
+ # print(' \n Reloading D from tmp...')
1092
+ for k in tqdm(range(tot_blocks_col)):
1093
+ di = np.load(self.FOLDER_OUT + f"/data_partitions/di_{k + 1}.npz")['di']
1094
+ R2 = R1 + np.shape(di)[1]
1095
+ D[:, R1:R2] = di
1096
+ R1 = R2
1097
+
1098
+ # Compute the DMD
1099
+ Phi_D, Lambda, freqs, a0s = dmd_s(D[:, 0:self.N_T - 1],
1100
+ D[:, 1:self.N_T], self.n_Modes, F_S, svd_solver=self.svd_solver)
1101
+
1102
+ else:
1103
+ Phi_D, Lambda, freqs, a0s = dmd_s(self.D[:, 0:self.N_T - 1],
1104
+ self.D[:, 1:self.N_T], self.n_Modes, F_S, SAVE_T_DMD=SAVE_T_DMD,
1105
+ svd_solver=self.svd_solver, FOLDER_OUT=self.FOLDER_OUT)
1106
+
1107
+ return Phi_D, Lambda, freqs, a0s
1108
+
1109
+ def SPOD(
1110
+ self,
1111
+ mode: str,
1112
+ F_S: float,
1113
+ n_Modes: int = 10,
1114
+ SAVE_SPOD: bool = True,
1115
+ **kwargs
1116
+ ):
1117
+ """
1118
+ Unified Spectral POD interface.
1119
+
1120
+ Parameters
1121
+ ----------
1122
+ mode : {'sieber', 'towne'}
1123
+ Which SPOD algorithm to run.
1124
+ F_S : float
1125
+ Sampling frequency [Hz].
1126
+ n_Modes : int, optional
1127
+ Number of modes to compute, by default 10.
1128
+ SAVE_SPOD : bool, optional
1129
+ Whether to save outputs, by default True.
1130
+ **kwargs
1131
+ For mode='sieber', accepts:
1132
+ - N_O (int): semi-order of the diagonal filter
1133
+ - f_c (float): cutoff frequency
1134
+ For mode='towne', accepts:
1135
+ - L_B (int): block length
1136
+ - O_B (int): block overlap
1137
+ - n_processes (int): number of parallel workers
1138
+
1139
+ Returns
1140
+ -------
1141
+ Phi : ndarray
1142
+ Spatial modes.
1143
+ Sigma : ndarray
1144
+ Modal amplitudes.
1145
+ Aux : tuple
1146
+ Additional outputs.
1147
+ """
1148
+ mode = mode.lower()
1149
+ if mode == 'sieber':
1150
+ N_O = kwargs.pop('N_O', 100)
1151
+ f_c = kwargs.pop('f_c', 0.3)
1152
+
1153
+ return self.compute_SPOD_s(
1154
+ F_S=F_S,
1155
+ N_O=N_O,
1156
+ f_c=f_c,
1157
+ n_Modes=n_Modes,
1158
+ SAVE_SPOD=SAVE_SPOD
1159
+ )
1160
+
1161
+ elif mode == 'towne':
1162
+ L_B = kwargs.pop('L_B', 500)
1163
+ O_B = kwargs.pop('O_B', 250)
1164
+ n_processes = kwargs.pop('n_processes', 1)
1165
+
1166
+ # Load or reuse data matrix
1167
+
1168
+ if self.D is None:
1169
+ D = np.load(f"{self.FOLDER_OUT}/MODULO_tmp/data_matrix/database.npz")['D']
1170
+ else:
1171
+ D = self.D
1172
+
1173
+ # Segment and FFT
1174
+ D_hat, freqs_pos = segment_and_fft(
1175
+ D=D,
1176
+ F_S=F_S,
1177
+ L_B=L_B,
1178
+ O_B=O_B,
1179
+ n_processes=n_processes
1180
+ )
1181
+
1182
+ return self.compute_SPOD_t(D_hat=D_hat,
1183
+ freq_pos=freqs_pos,
1184
+ n_modes=n_Modes,
1185
+ SAVE_SPOD=SAVE_SPOD,
1186
+ svd_solver=self.svd_solver,
1187
+ n_processes=n_processes)
1188
+
1189
+ else:
1190
+ raise ValueError("mode must be 'sieber' or 'towne'")
1191
+
1192
+
1193
+ def compute_SPOD_t(self, D_hat, freq_pos, n_Modes=10, SAVE_SPOD=True, svd_solver=None,
1194
+ n_processes=1):
1195
+ """
1196
+ Compute the CSD-based Spectral POD (Towne et al.) from a precomputed FFT tensor.
1197
+
1198
+ Parameters
1199
+ ----------
1200
+ D_hat : ndarray, shape (n_s, n_freqs, n_blocks)
1201
+ FFT of each block, only nonnegative frequencies retained.
1202
+ freq_pos : ndarray, shape (n_freqs,)
1203
+ Positive frequency values (Hz) corresponding to D_hat’s second axis.
1204
+ n_Modes : int, optional
1205
+ Number of SPOD modes per frequency bin. Default is 10.
1206
+ SAVE_SPOD : bool, optional
1207
+ If True, save outputs under `self.FOLDER_OUT/MODULO_tmp`. Default is True.
1208
+ svd_solver : str or None, optional
1209
+ Which SVD solver to use (passed to `switch_svds`), by default None.
1210
+ n_processes : int, optional
1211
+ Number of parallel workers for the POD step. Default is 1 (serial).
1212
+
1213
+ Returns
1214
+ -------
1215
+ Phi_SP : ndarray, shape (n_s, n_Modes, n_freqs)
1216
+ Spatial SPOD modes at each positive frequency.
1217
+ Sigma_SP : ndarray, shape (n_Modes, n_freqs)
1218
+ Modal energies per frequency bin.
1219
+ freq_pos : ndarray, shape (n_freqs,)
1220
+ The positive frequency vector (Hz), returned unchanged.
1221
+ """
1222
+ # Perform the POD (parallel if requested)
1223
+ # received D_hat_f, this is now just a POD on the transversal direction of the tensor,
1224
+ # e.g. the frequency domain.
1225
+ n_freqs = len(freq_pos)
1226
+
1227
+ # also here we can parallelize
1228
+ Phi_SP, Sigma_SP = pod_from_dhat(D_hat=D_hat, n_modes=n_Modes, n_freqs=n_freqs,
1229
+ svd_solver=self.svd_solver, n_processes=n_processes)
1230
+
1231
+ # Optionally save the results
1232
+ if SAVE_SPOD:
1233
+ np.savez(
1234
+ f"{self.FOLDER_OUT}/MODULO_tmp/spod_towne.npz",
1235
+ Phi=Phi_SP,
1236
+ Sigma=Sigma_SP,
1237
+ freqs=freq_pos
1238
+ )
1239
+
1240
+ return Phi_SP, Sigma_SP, freq_pos
1241
+
1242
+
1243
+
1244
+ def compute_SPOD_s(self, N_O=100, f_c=0.3, n_Modes=10, SAVE_SPOD=True):
1245
+ """
1246
+ Compute the filtered‐covariance Spectral POD (Sieber _et al._) of your data.
1247
+
1248
+ This implementation follows Sieber et al. (2016), which applies a zero‐phase
1249
+ diagonal filter to the time‐lag covariance and then performs a single POD
1250
+ on the filtered covariance matrix.
1251
+
1252
+ Parameters
1253
+ ----------
1254
+ N_O : int, optional
1255
+ Semi‐order of the diagonal FIR filter. The true filter length is
1256
+ 2*N_O+1, by default 100.
1257
+ f_c : float, optional
1258
+ Normalized cutoff frequency of the diagonal filter (0 < f_c < 0.5),
1259
+ by default 0.3.
1260
+ n_Modes : int, optional
1261
+ Number of SPOD modes to compute, by default 10.
1262
+ SAVE_SPOD : bool, optional
1263
+ If True, save output under `self.FOLDER_OUT/MODULO_tmp`, by default True.
1264
+
1265
+ Returns
1266
+ -------
1267
+ Phi_sP : numpy.ndarray, shape (n_S, n_Modes)
1268
+ Spatial SPOD modes.
1269
+ Psi_sP : numpy.ndarray, shape (n_t, n_Modes)
1270
+ Temporal SPOD modes (filtered).
1271
+ Sigma_sP : numpy.ndarray, shape (n_Modes,)
1272
+ Modal energies (eigenvalues of the filtered covariance).
1273
+ """
1274
+ if self.D is None:
1275
+
1276
+ D = np.load(self.FOLDER_OUT + '/MODULO_tmp/data_matrix/database.npz')['D']
1277
+
1278
+ self.K = CorrelationMatrix(self.N_T, self.N_PARTITIONS, self.MEMORY_SAVING, self.FOLDER_OUT, self.SAVE_K, D=D)
1279
+
1280
+ # additional step: diagonal spectral filter of K
1281
+ K_F = spectral_filter(self.K, N_o=N_O, f_c=f_c)
1282
+
1283
+ # and then proceed with normal POD procedure
1284
+ Psi_P, Sigma_P = Temporal_basis_POD(K_F, SAVE_SPOD, self.FOLDER_OUT, n_Modes)
1285
+
1286
+ # but with a normalization aspect to handle the non-orthogonality of the SPOD modes
1287
+ Phi_P = Spatial_basis_POD(D, N_T=self.K.shape[0],
1288
+ PSI_P=Psi_P, Sigma_P=Sigma_P,
1289
+ MEMORY_SAVING=self.MEMORY_SAVING,
1290
+ FOLDER_OUT=self.FOLDER_OUT,
1291
+ N_PARTITIONS=self.N_PARTITIONS,rescale=True)
1292
+
1293
+
1294
+ return Phi_P, Psi_P, Sigma_P
1295
+
1296
+
1297
+ def kPOD(self, M_DIST=[1, 10],
1298
+ k_m=0.1, cent=True,
1299
+ n_Modes=10,
1300
+ alpha=1e-6,
1301
+ metric='rbf',
1302
+ K_out=False, SAVE_KPOD=False):
1303
+ """
1304
+ 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
1305
+
1306
+ The computation of the kernel function is carried out as in https://arxiv.org/pdf/2208.07746.pdf.
1307
+
1308
+
1309
+ :param M_DIST: array,
1310
+ position of the two snapshots that will be considered to
1311
+ estimate the minimal k. They should be the most different ones.
1312
+ :param k_m: float,
1313
+ minimum value for the kernelized correlation
1314
+ :param alpha: float
1315
+ regularization for K_zeta
1316
+ :param cent: bool,
1317
+ if True, the matrix K is centered. Else it is not
1318
+ :param n_Modes: float,
1319
+ number of modes to be computed
1320
+ :param metric: string,
1321
+ This identifies the metric for the kernel matrix. It is a wrapper to 'pairwise_kernels' from sklearn.metrics.pairwise
1322
+ Note that different metrics would need different set of parameters. For the moment, only rbf was tested; use any other option at your peril !
1323
+ :param K_out: bool,
1324
+ If true, the matrix K is also exported as a fourth output.
1325
+ :return Psi_xi: np.array
1326
+ kPOD's Psis
1327
+ :return Sigma_xi: np.array
1328
+ kPOD's Sigmas.
1329
+ :return Phi_xi: np.array
1330
+ kPOD's Phis
1331
+ :return K_zeta: np.array
1332
+ Kernel Function from which the decomposition is computed.
1333
+ (exported only if K_out=True)
1334
+
1335
+
1336
+ """
1337
+ if self.D is None:
1338
+ D = np.load(self.FOLDER_OUT + '/MODULO_tmp/data_matrix/database.npz')['D']
1339
+ else:
1340
+ D = self.D
1341
+
1342
+ # Compute Eucledean distances
1343
+ i, j = M_DIST
1344
+
1345
+ M_ij = np.linalg.norm(D[:, i] - D[:, j]) ** 2
1346
+
1347
+ K_r = kernelized_K(D=D, M_ij=M_ij, k_m=k_m, metric=metric, cent=cent, alpha=alpha)
1348
+
1349
+ Psi_xi, Sigma_xi = Temporal_basis_POD(K=K_r, n_Modes=n_Modes, eig_solver='eigh')
1350
+
1351
+ PHI_xi_SIGMA_xi = D @ Psi_xi
1352
+
1353
+ Sigma_xi = np.linalg.norm(PHI_xi_SIGMA_xi, axis=0) # (R,)
1354
+ Phi_xi = PHI_xi_SIGMA_xi / Sigma_xi[None, :] # (n_s, R)
1355
+
1356
+ sorted_idx = np.argsort(-Sigma_xi)
1357
+
1358
+ Phi_xi = Phi_xi[:, sorted_idx] # Sorted Spatial Structures Matrix
1359
+ Psi_xi = Psi_xi[:, sorted_idx] # Sorted Temporal Structures Matrix
1360
+ Sigma_xi = Sigma_xi[sorted_idx]
1361
+
1362
+ if K_out:
1363
+ return Phi_xi, Psi_xi, Sigma_xi, K_r
1364
+ else:
1365
+ return Phi_xi, Psi_xi, Sigma_xi
1366
+
1367
+
1368
+