flowtorch-fluid 1.3.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.
- flowtorch/__init__.py +2 -0
- flowtorch/analysis/__init__.py +42 -0
- flowtorch/analysis/dft.py +274 -0
- flowtorch/analysis/dmd.py +524 -0
- flowtorch/analysis/hodmd.py +226 -0
- flowtorch/analysis/hooptdmd.py +113 -0
- flowtorch/analysis/linear_control.py +249 -0
- flowtorch/analysis/linear_model.py +364 -0
- flowtorch/analysis/mssa.py +161 -0
- flowtorch/analysis/optdmd.py +345 -0
- flowtorch/analysis/periodogram.py +346 -0
- flowtorch/analysis/psp_explorer.py +246 -0
- flowtorch/analysis/spod.py +811 -0
- flowtorch/analysis/svd.py +290 -0
- flowtorch/analysis/utils.py +150 -0
- flowtorch/constants.py +40 -0
- flowtorch/data/__init__.py +51 -0
- flowtorch/data/csv_dataloader.py +369 -0
- flowtorch/data/dataloader.py +79 -0
- flowtorch/data/foam_dataloader.py +1079 -0
- flowtorch/data/hdf5_file.py +735 -0
- flowtorch/data/image_dataloader.py +128 -0
- flowtorch/data/outlier_tools.py +59 -0
- flowtorch/data/psp_dataloader.py +284 -0
- flowtorch/data/s_cube_dataloader.py +305 -0
- flowtorch/data/selection_tools.py +74 -0
- flowtorch/data/sequence_dataset.py +71 -0
- flowtorch/data/tau_dataloader.py +665 -0
- flowtorch/data/tecplot_dataloader.py +306 -0
- flowtorch/data/utils.py +233 -0
- flowtorch/data/vtk_dataloader.py +241 -0
- flowtorch/rom/__init__.py +2 -0
- flowtorch/rom/base.py +239 -0
- flowtorch/rom/cnm.py +405 -0
- flowtorch/rom/svd_encoder.py +124 -0
- flowtorch/rom/utils.py +78 -0
- flowtorch/utils.py +14 -0
- flowtorch/version.py +3 -0
- flowtorch_fluid-1.3.0.dist-info/METADATA +348 -0
- flowtorch_fluid-1.3.0.dist-info/RECORD +43 -0
- flowtorch_fluid-1.3.0.dist-info/WHEEL +5 -0
- flowtorch_fluid-1.3.0.dist-info/licenses/LICENSE +674 -0
- flowtorch_fluid-1.3.0.dist-info/top_level.txt +1 -0
flowtorch/__init__.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from .dmd import DMD
|
|
2
|
+
from .hodmd import HODMD
|
|
3
|
+
|
|
4
|
+
# temporary bug fix until completion of BagDMD
|
|
5
|
+
# from .bagdmd import BagDMD, HOBagDMD
|
|
6
|
+
from .optdmd import OptDMD
|
|
7
|
+
from .hooptdmd import HOOptDMD
|
|
8
|
+
from .mssa import MSSA, PMSSA
|
|
9
|
+
from .svd import SVD
|
|
10
|
+
|
|
11
|
+
# from .linear_control import LinearControlModel
|
|
12
|
+
from .linear_model import LinearModel
|
|
13
|
+
from .dft import DFT, PDFT
|
|
14
|
+
from .spod import AMSPOD, PAMSPOD
|
|
15
|
+
from .periodogram import AMPS
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"AMPS",
|
|
19
|
+
"AMSPOD",
|
|
20
|
+
"DFT",
|
|
21
|
+
"DMD",
|
|
22
|
+
"HODMD",
|
|
23
|
+
"HOOptDMD",
|
|
24
|
+
"LinearModel",
|
|
25
|
+
"MSSA",
|
|
26
|
+
"OptDMD",
|
|
27
|
+
"PAMSPOD",
|
|
28
|
+
"PDFT",
|
|
29
|
+
"PMSSA",
|
|
30
|
+
"PSPExplorer",
|
|
31
|
+
"SVD",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def __getattr__(name: str):
|
|
36
|
+
"""Load data-dependent analysis tools only when requested."""
|
|
37
|
+
if name == "PSPExplorer":
|
|
38
|
+
from .psp_explorer import PSPExplorer
|
|
39
|
+
|
|
40
|
+
globals()[name] = PSPExplorer
|
|
41
|
+
return PSPExplorer
|
|
42
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
"""Discrete Fourier transformations of snapshot data."""
|
|
2
|
+
|
|
3
|
+
# standard library packages
|
|
4
|
+
from typing import Set
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
# third party packages
|
|
8
|
+
import torch as pt
|
|
9
|
+
|
|
10
|
+
# flowtorch packages
|
|
11
|
+
from ..constants import FLOAT_TOLERANCE
|
|
12
|
+
from .svd import SVD
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
logging.basicConfig(level=logging.INFO)
|
|
16
|
+
|
|
17
|
+
WINDOWS = {
|
|
18
|
+
"boxcar": pt.ones,
|
|
19
|
+
"hann": pt.hann_window,
|
|
20
|
+
"hamming": pt.hamming_window,
|
|
21
|
+
"blackman": pt.blackman_window,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class DFT(object):
|
|
26
|
+
"""Compute the discrete Fourier transform (DFT) of a data matrix."""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
data_matrix: pt.Tensor,
|
|
31
|
+
dt: float,
|
|
32
|
+
nfft: int | None = None,
|
|
33
|
+
window: str = "boxcar",
|
|
34
|
+
device: str = "cpu",
|
|
35
|
+
):
|
|
36
|
+
"""Compute the discrete Fourier transform (DFT) of a data matrix.
|
|
37
|
+
|
|
38
|
+
The DFT is computed along the time axis of the data matrix (for every row).
|
|
39
|
+
|
|
40
|
+
:param data_matrix: M x N data matrix, where M is the number of spatial points
|
|
41
|
+
and N is the number of time instances
|
|
42
|
+
:type data_matrix: pt.Tensor
|
|
43
|
+
:param dt: timestep between snapshots; must be constant
|
|
44
|
+
:type dt: float
|
|
45
|
+
:param nfft: number of FFT frequency bins; zero-padding is used if nfft > N;
|
|
46
|
+
truncation is not allowed, defaults to N
|
|
47
|
+
:type nfft: int | None, optional
|
|
48
|
+
:param window: weighting used for tapering the signal, defaults to "boxcar"
|
|
49
|
+
:type window: str, optional
|
|
50
|
+
:param device: device on which to perform the DFT, defaults to "cpu"
|
|
51
|
+
:type device: str, optional
|
|
52
|
+
"""
|
|
53
|
+
self._dm = data_matrix
|
|
54
|
+
self._mean = data_matrix.mean(dim=-1)
|
|
55
|
+
self._dt = dt
|
|
56
|
+
self._nfft = nfft if nfft is not None else self._dm.shape[-1]
|
|
57
|
+
self._nfft = max(self._nfft, self._dm.shape[-1])
|
|
58
|
+
self._wname = window
|
|
59
|
+
self._wfunc = WINDOWS.get(window)
|
|
60
|
+
if self._wfunc is None:
|
|
61
|
+
raise ValueError(
|
|
62
|
+
f"Unknown window option {window}. Available windows are:"
|
|
63
|
+
+ ", ".join(WINDOWS.keys())
|
|
64
|
+
)
|
|
65
|
+
self._window = self._wfunc(self._dm.shape[-1])
|
|
66
|
+
self._device = device
|
|
67
|
+
self._frequency = (
|
|
68
|
+
pt.fft.fftfreq(self._nfft, dt)
|
|
69
|
+
if pt.is_complex(self._dm)
|
|
70
|
+
else pt.fft.rfftfreq(self._nfft, dt)
|
|
71
|
+
)
|
|
72
|
+
fft = pt.fft.fft if pt.is_complex(self._dm) else pt.fft.rfft
|
|
73
|
+
wdm = (self._dm - self._mean.unsqueeze(-1)) * self._window.type(self._dm.dtype)
|
|
74
|
+
self._modes = fft(wdm.to(device), self._nfft, -1, "ortho").cpu()
|
|
75
|
+
self._amplitude = self._modes.norm(dim=0)
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def amplitude(self) -> pt.Tensor:
|
|
79
|
+
"""Vector norm of the spatial DFT modes.
|
|
80
|
+
|
|
81
|
+
:return: mode amplitudes (vector norm of the raw modes)
|
|
82
|
+
:rtype: pt.Tensor
|
|
83
|
+
"""
|
|
84
|
+
w = self._window.square().mean()
|
|
85
|
+
a = self._amplitude[1:] ** 2
|
|
86
|
+
if not pt.is_complex(self._dm):
|
|
87
|
+
if self._nfft % 2 == 0:
|
|
88
|
+
a[:-1] *= 2.0
|
|
89
|
+
else:
|
|
90
|
+
a *= 2.0
|
|
91
|
+
return a / self._dm.shape[0] / w
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def frequency(self) -> pt.Tensor:
|
|
95
|
+
"""Bin frequency values.
|
|
96
|
+
|
|
97
|
+
For real input data, the symmetric part of the spectrum is excluded.
|
|
98
|
+
The zero-frequency bin is always excluded since the data is mean-subtracted.
|
|
99
|
+
|
|
100
|
+
:return: frequency bin values
|
|
101
|
+
:rtype: pt.Tensor
|
|
102
|
+
"""
|
|
103
|
+
return self._frequency[1:]
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def modes(self) -> pt.Tensor:
|
|
107
|
+
"""Spatial DFT modes normalized to unit length.
|
|
108
|
+
|
|
109
|
+
The zero-frequency mode is excluded since the data
|
|
110
|
+
is mean-subtracted.
|
|
111
|
+
|
|
112
|
+
:return: complex, spatial DFT modes
|
|
113
|
+
:rtype: pt.Tensor
|
|
114
|
+
"""
|
|
115
|
+
return self._modes[:, 1:] / self._amplitude[1:]
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def spectral_density(self) -> pt.Tensor:
|
|
119
|
+
df = self.frequency[1] - self.frequency[0]
|
|
120
|
+
return self.amplitude / df
|
|
121
|
+
|
|
122
|
+
@property
|
|
123
|
+
def reconstruction(self) -> pt.Tensor:
|
|
124
|
+
"""Compute the inverse DFT using all modes.
|
|
125
|
+
|
|
126
|
+
:return: reconstructed input data matrix
|
|
127
|
+
:rtype: pt.Tensor
|
|
128
|
+
"""
|
|
129
|
+
return self.partial_reconstruction(set(range(len(self.amplitude))), True)
|
|
130
|
+
|
|
131
|
+
def partial_reconstruction(
|
|
132
|
+
self, mode_indices: Set[int] | int, include_mean: bool = False
|
|
133
|
+
) -> pt.Tensor:
|
|
134
|
+
"""Reconstruct the original data using selected modes.
|
|
135
|
+
|
|
136
|
+
:param mode_indices: mode indices to include in the reconstruction;
|
|
137
|
+
the zero-frequency bin is included by default but does not contribute
|
|
138
|
+
to the reconstruction since the data is mean-subtracted.
|
|
139
|
+
:type mode_indices: Set[int] | int
|
|
140
|
+
:param include_mean: add the mean to the reconstruction; can be useful for
|
|
141
|
+
animations, defaults to False
|
|
142
|
+
:type include_mean: bool, optional
|
|
143
|
+
:return: partial reconstruction of the input data matrix
|
|
144
|
+
:rtype: pt.Tensor
|
|
145
|
+
"""
|
|
146
|
+
if self._wname in ("hann", "blackman"):
|
|
147
|
+
logger.warning(
|
|
148
|
+
"The first and last snapshots of the reconstruction are unphysical when using the "
|
|
149
|
+
+ f"'{self._wname}' window. Consider using the 'hamming' window."
|
|
150
|
+
)
|
|
151
|
+
if isinstance(mode_indices, int):
|
|
152
|
+
mode_indices = {mode_indices}
|
|
153
|
+
indices = pt.tensor(list(mode_indices), dtype=pt.int64) + 1
|
|
154
|
+
mask = pt.zeros_like(self._amplitude).type(self._modes.dtype)
|
|
155
|
+
mask[0] = 1.0
|
|
156
|
+
mask[indices] = 1.0
|
|
157
|
+
options = ((self._modes * mask).to(self._device), self._nfft, -1, "ortho")
|
|
158
|
+
offset = self._mean.unsqueeze(-1) if include_mean else 0
|
|
159
|
+
w_inv = 1.0 / pt.clamp(self._window, FLOAT_TOLERANCE).type(self._dm.dtype)
|
|
160
|
+
N = self._dm.shape[-1]
|
|
161
|
+
if pt.is_complex(self._dm):
|
|
162
|
+
return pt.fft.ifft(*options).cpu()[:, :N] * w_inv + offset
|
|
163
|
+
else:
|
|
164
|
+
return pt.fft.irfft(*options).cpu().real[:, :N] * w_inv + offset
|
|
165
|
+
|
|
166
|
+
def top_modes(
|
|
167
|
+
self,
|
|
168
|
+
n: int = 1,
|
|
169
|
+
f_min: float = -float("inf"),
|
|
170
|
+
f_max: float = float("inf"),
|
|
171
|
+
) -> pt.Tensor:
|
|
172
|
+
"""Get the indices of the first n most important modes.
|
|
173
|
+
|
|
174
|
+
:param n: number of indices to return; defaults to 1
|
|
175
|
+
:type n: int
|
|
176
|
+
:param f_min: consider only modes with a frequency larger or equal
|
|
177
|
+
to f_min; defaults to -inf
|
|
178
|
+
:type f_min: float, optional
|
|
179
|
+
:param f_max: consider only modes with a frequency smaller than f_max;
|
|
180
|
+
defaults to -inf
|
|
181
|
+
:type f_max: float, optional
|
|
182
|
+
:return: indices of top n modes sorted by amplitude or integral
|
|
183
|
+
contribution
|
|
184
|
+
:rtype: pt.Tensor
|
|
185
|
+
"""
|
|
186
|
+
modes_in_range = pt.logical_and(self.frequency >= f_min, self.frequency < f_max)
|
|
187
|
+
mode_indices = pt.tensor(range(modes_in_range.shape[0]), dtype=pt.int64)[
|
|
188
|
+
modes_in_range
|
|
189
|
+
]
|
|
190
|
+
n = min(n, mode_indices.shape[0])
|
|
191
|
+
top_n = self.amplitude[mode_indices].abs().topk(n).indices
|
|
192
|
+
return mode_indices[top_n]
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class PDFT(DFT):
|
|
196
|
+
"""Compute the DFT of the POD time coefficients."""
|
|
197
|
+
|
|
198
|
+
def __init__(
|
|
199
|
+
self,
|
|
200
|
+
data_matrix: pt.Tensor,
|
|
201
|
+
dt: float,
|
|
202
|
+
rank: int | None = None,
|
|
203
|
+
nfft: int | None = None,
|
|
204
|
+
window: str = "boxcar",
|
|
205
|
+
device: str = "cpu",
|
|
206
|
+
):
|
|
207
|
+
"""Compute the DFT of the POD time coefficients.
|
|
208
|
+
|
|
209
|
+
First, the POD basis is computed by means of an SVD. The POD basis may
|
|
210
|
+
be truncated to reduce noise in the spectrum. Only the time coefficients
|
|
211
|
+
are Fourier-transformed. Without rank truncation, this yields the DFT
|
|
212
|
+
of the data matrix (the DFT modes need to be projected back onto the POD basis).
|
|
213
|
+
For large data matrices, the projection step reduces the computational
|
|
214
|
+
cost and storage requirements significantly.
|
|
215
|
+
|
|
216
|
+
:param data_matrix: M x N data matrix, where M is the number of spatial points
|
|
217
|
+
and N is the number of time instances
|
|
218
|
+
:type data_matrix: pt.Tensor
|
|
219
|
+
:param dt: timestep between snapshots; must be constant
|
|
220
|
+
:type dt: float
|
|
221
|
+
:param rank: truncation parameter for the POD basis, defaults to None
|
|
222
|
+
(automatic selection in the SVD class)
|
|
223
|
+
:type rank: int | None, optional
|
|
224
|
+
:param nfft: number of FFT frequency bins; zero-padding is used if nfft > N;
|
|
225
|
+
truncation is not allowed, defaults to N
|
|
226
|
+
:type nfft: int | None, optional
|
|
227
|
+
:param window: weighting used for tapering the signal, defaults to "boxcar"
|
|
228
|
+
:type window: str, optional
|
|
229
|
+
:param device: device on which to perform the DFT, defaults to "cpu"
|
|
230
|
+
:type device: str, optional
|
|
231
|
+
"""
|
|
232
|
+
self._dm_org = data_matrix
|
|
233
|
+
self._svd = SVD(self._dm_org, rank)
|
|
234
|
+
super(PDFT, self).__init__(
|
|
235
|
+
(self._svd.V * self._svd.s).T,
|
|
236
|
+
dt,
|
|
237
|
+
nfft,
|
|
238
|
+
window,
|
|
239
|
+
device,
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
@property
|
|
243
|
+
def svd(self) -> SVD:
|
|
244
|
+
"""SVD used to obtain the POD basis.
|
|
245
|
+
|
|
246
|
+
:return: SVD of the original data matrix
|
|
247
|
+
:rtype: SVD
|
|
248
|
+
"""
|
|
249
|
+
return self._svd
|
|
250
|
+
|
|
251
|
+
@property
|
|
252
|
+
def amplitude(self) -> pt.Tensor:
|
|
253
|
+
"""Rescaled amplitudes consistent with DFT.
|
|
254
|
+
|
|
255
|
+
:return: amplitudes considering the size of the full state
|
|
256
|
+
:rtype: pt.Tensor
|
|
257
|
+
"""
|
|
258
|
+
return super().amplitude * self._dm.shape[0] / self._dm_org.shape[0]
|
|
259
|
+
|
|
260
|
+
@property
|
|
261
|
+
def modes(self) -> pt.Tensor:
|
|
262
|
+
"""DFT modes in the original space.
|
|
263
|
+
|
|
264
|
+
:return: DFT modes projected onto the POD basis
|
|
265
|
+
:rtype: pt.Tensor
|
|
266
|
+
"""
|
|
267
|
+
m = super().modes
|
|
268
|
+
return self._svd.U.type(m.dtype) @ m
|
|
269
|
+
|
|
270
|
+
def partial_reconstruction(
|
|
271
|
+
self, mode_indices: Set[int] | int, include_mean: bool = False
|
|
272
|
+
) -> pt.Tensor:
|
|
273
|
+
"""Partial reconstruction projected onto the POD basis."""
|
|
274
|
+
return self._svd.U @ super().partial_reconstruction(mode_indices, include_mean)
|