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.
Files changed (43) hide show
  1. flowtorch/__init__.py +2 -0
  2. flowtorch/analysis/__init__.py +42 -0
  3. flowtorch/analysis/dft.py +274 -0
  4. flowtorch/analysis/dmd.py +524 -0
  5. flowtorch/analysis/hodmd.py +226 -0
  6. flowtorch/analysis/hooptdmd.py +113 -0
  7. flowtorch/analysis/linear_control.py +249 -0
  8. flowtorch/analysis/linear_model.py +364 -0
  9. flowtorch/analysis/mssa.py +161 -0
  10. flowtorch/analysis/optdmd.py +345 -0
  11. flowtorch/analysis/periodogram.py +346 -0
  12. flowtorch/analysis/psp_explorer.py +246 -0
  13. flowtorch/analysis/spod.py +811 -0
  14. flowtorch/analysis/svd.py +290 -0
  15. flowtorch/analysis/utils.py +150 -0
  16. flowtorch/constants.py +40 -0
  17. flowtorch/data/__init__.py +51 -0
  18. flowtorch/data/csv_dataloader.py +369 -0
  19. flowtorch/data/dataloader.py +79 -0
  20. flowtorch/data/foam_dataloader.py +1079 -0
  21. flowtorch/data/hdf5_file.py +735 -0
  22. flowtorch/data/image_dataloader.py +128 -0
  23. flowtorch/data/outlier_tools.py +59 -0
  24. flowtorch/data/psp_dataloader.py +284 -0
  25. flowtorch/data/s_cube_dataloader.py +305 -0
  26. flowtorch/data/selection_tools.py +74 -0
  27. flowtorch/data/sequence_dataset.py +71 -0
  28. flowtorch/data/tau_dataloader.py +665 -0
  29. flowtorch/data/tecplot_dataloader.py +306 -0
  30. flowtorch/data/utils.py +233 -0
  31. flowtorch/data/vtk_dataloader.py +241 -0
  32. flowtorch/rom/__init__.py +2 -0
  33. flowtorch/rom/base.py +239 -0
  34. flowtorch/rom/cnm.py +405 -0
  35. flowtorch/rom/svd_encoder.py +124 -0
  36. flowtorch/rom/utils.py +78 -0
  37. flowtorch/utils.py +14 -0
  38. flowtorch/version.py +3 -0
  39. flowtorch_fluid-1.3.0.dist-info/METADATA +348 -0
  40. flowtorch_fluid-1.3.0.dist-info/RECORD +43 -0
  41. flowtorch_fluid-1.3.0.dist-info/WHEEL +5 -0
  42. flowtorch_fluid-1.3.0.dist-info/licenses/LICENSE +674 -0
  43. flowtorch_fluid-1.3.0.dist-info/top_level.txt +1 -0
flowtorch/__init__.py ADDED
@@ -0,0 +1,2 @@
1
+ from .version import __version__
2
+ from .constants import DEFAULT_DTYPE, FLOAT_TOLERANCE, DATASET_PATH, DATASETS
@@ -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)