foamlib 0.8.8__py3-none-any.whl → 0.8.9__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.
- foamlib/__init__.py +1 -1
- foamlib/_cases/_async.py +93 -12
- foamlib/_cases/_base.py +21 -5
- foamlib/_cases/_run.py +6 -0
- foamlib/_cases/_slurm.py +10 -1
- foamlib/_cases/_sync.py +94 -12
- foamlib/_files/_files.py +110 -16
- foamlib/_files/_serialization.py +20 -8
- foamlib/_files/_types.py +43 -17
- {foamlib-0.8.8.dist-info → foamlib-0.8.9.dist-info}/METADATA +175 -9
- foamlib-0.8.9.dist-info/RECORD +20 -0
- foamlib-0.8.8.dist-info/RECORD +0 -20
- {foamlib-0.8.8.dist-info → foamlib-0.8.9.dist-info}/WHEEL +0 -0
- {foamlib-0.8.8.dist-info → foamlib-0.8.9.dist-info}/licenses/LICENSE.txt +0 -0
foamlib/__init__.py
CHANGED
foamlib/_cases/_async.py
CHANGED
@@ -45,8 +45,20 @@ class AsyncFoamCase(FoamCaseRunBase):
|
|
45
45
|
Provides methods for running and cleaning cases, as well as accessing files.
|
46
46
|
|
47
47
|
Access the time directories of the case as a sequence, e.g. `case[0]` or `case[-1]`.
|
48
|
+
These will return `AsyncFoamCase.TimeDirectory` objects.
|
48
49
|
|
49
|
-
:param path: The path to the case directory.
|
50
|
+
:param path: The path to the case directory. Defaults to the current working
|
51
|
+
directory.
|
52
|
+
|
53
|
+
Example usage: ::
|
54
|
+
from foamlib import AsyncFoamCase
|
55
|
+
|
56
|
+
case = AsyncFoamCase("path/to/case") # Load an OpenFOAM case
|
57
|
+
case[0]["U"].internal_field = [0, 0, 0] # Set the initial velocity field to zero
|
58
|
+
await case.run() # Run the case
|
59
|
+
for time in case: # Iterate over the time directories
|
60
|
+
print(time.time) # Print the time
|
61
|
+
print(time["U"].internal_field) # Print the velocity field
|
50
62
|
"""
|
51
63
|
|
52
64
|
class TimeDirectory(FoamCaseRunBase.TimeDirectory):
|
@@ -55,7 +67,12 @@ class AsyncFoamCase(FoamCaseRunBase):
|
|
55
67
|
return AsyncFoamCase(self.path.parent)
|
56
68
|
|
57
69
|
async def cell_centers(self) -> FoamFieldFile:
|
58
|
-
"""
|
70
|
+
"""
|
71
|
+
Write and return the cell centers.
|
72
|
+
|
73
|
+
Currently only works for reconstructed cases (decomposed cases will need to
|
74
|
+
be reconstructed first).
|
75
|
+
"""
|
59
76
|
calls = ValuedGenerator(self._cell_centers_calls())
|
60
77
|
|
61
78
|
for coro in calls:
|
@@ -126,7 +143,20 @@ class AsyncFoamCase(FoamCaseRunBase):
|
|
126
143
|
"""
|
127
144
|
Clean this case.
|
128
145
|
|
129
|
-
|
146
|
+
If a `clean` or `Allclean` script is present in the case directory, it will be invoked.
|
147
|
+
Otherwise, the case directory will be cleaned using these rules:
|
148
|
+
|
149
|
+
- All time directories except `0` will be deleted.
|
150
|
+
- The `0` time directory will be deleted if `0.orig` exists.
|
151
|
+
- `processor*` directories will be deleted if a `system/decomposeParDict` file is present.
|
152
|
+
- `constant/polyMesh` will be deleted if a `system/blockMeshDict` file is present.
|
153
|
+
- All `log.*` files will be deleted.
|
154
|
+
|
155
|
+
If this behavior is not appropriate for a case, it is recommended to write a custom
|
156
|
+
`clean` script.
|
157
|
+
|
158
|
+
:param check: If True, raise a `CalledProcessError` if the clean script returns a
|
159
|
+
non-zero exit code.
|
130
160
|
"""
|
131
161
|
for coro in self._clean_calls(check=check):
|
132
162
|
await coro
|
@@ -161,11 +191,35 @@ class AsyncFoamCase(FoamCaseRunBase):
|
|
161
191
|
"""
|
162
192
|
Run this case, or a specified command in the context of this case.
|
163
193
|
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
:
|
168
|
-
|
194
|
+
If `cmd` is given, this method will run the given command in the context of the case.
|
195
|
+
|
196
|
+
If `cmd` is `None`, a series of heuristic rules will be used to run the case. This works as
|
197
|
+
follows:
|
198
|
+
|
199
|
+
- If a `run`, `Allrun` or `Allrun-parallel` script is present in the case directory,
|
200
|
+
it will be invoked. If both `run` and `Allrun` are present, `Allrun` will be used. If
|
201
|
+
both `Allrun` and `Allrun-parallel` are present and `parallel` is `None`, an error will
|
202
|
+
be raised.
|
203
|
+
- If no run script is present but an `Allrun.pre` script exists, it will be invoked.
|
204
|
+
- Otherwise, if a `system/blockMeshDict` file is present, the method will call
|
205
|
+
`self.block_mesh()`.
|
206
|
+
- Then, if a `0.orig` directory is present, it will call `self.restore_0_dir()`.
|
207
|
+
- Then, if the case is to be run in parallel (see the `parallel` option) and no
|
208
|
+
`processor*` directories exist but a`system/decomposeParDict` file is present, it will
|
209
|
+
call `self.decompose_par()`.
|
210
|
+
- Then, it will run the case using the application specified in the `controlDict` file.
|
211
|
+
|
212
|
+
If this behavior is not appropriate for a case, it is recommended to write a custom
|
213
|
+
`run`, `Allrun`, `Allrun-parallel` or `Allrun.pre` script.
|
214
|
+
|
215
|
+
:param cmd: The command to run. If `None`, run the case. If a sequence, the first element
|
216
|
+
is the command and the rest are arguments. If a string, `cmd` is executed in a shell.
|
217
|
+
:param parallel: If `True`, run in parallel using MPI. If None, autodetect whether to run
|
218
|
+
in parallel.
|
219
|
+
:param cpus: The number of CPUs to use. If `None`, autodetect from to the case.
|
220
|
+
:param check: If `True`, raise a `CalledProcessError` if any command returns a non-zero
|
221
|
+
exit code.
|
222
|
+
:param log: If `True`, log the command output to `log.*` files in the case directory.
|
169
223
|
"""
|
170
224
|
for coro in self._run_calls(
|
171
225
|
cmd=cmd, parallel=parallel, cpus=cpus, check=check, log=log
|
@@ -200,9 +254,21 @@ class AsyncFoamCase(FoamCaseRunBase):
|
|
200
254
|
"""
|
201
255
|
Make a copy of this case.
|
202
256
|
|
203
|
-
|
257
|
+
If used as an asynchronous context manager (i.e., within an `async with` block) the copy
|
258
|
+
will be deleted automatically when exiting the block.
|
259
|
+
|
260
|
+
:param dst: The destination path. If `None`, clone to `$FOAM_RUN/foamlib`.
|
261
|
+
|
262
|
+
:return: The copy of the case.
|
204
263
|
|
205
|
-
|
264
|
+
Example usage: ::
|
265
|
+
import os
|
266
|
+
from pathlib import Path
|
267
|
+
from foamlib import AsyncFoamCase
|
268
|
+
|
269
|
+
pitz_tutorial = AsyncFoamCase(Path(os.environ["FOAM_TUTORIALS"]) / "incompressible/simpleFoam/pitzDaily")
|
270
|
+
|
271
|
+
my_pitz = await pitz_tutorial.copy("myPitz")
|
206
272
|
"""
|
207
273
|
calls = ValuedGenerator(self._copy_calls(dst))
|
208
274
|
|
@@ -221,9 +287,24 @@ class AsyncFoamCase(FoamCaseRunBase):
|
|
221
287
|
"""
|
222
288
|
Clone this case (make a clean copy).
|
223
289
|
|
224
|
-
|
290
|
+
This is equivalent to running `(await self.copy()).clean()`, but it can be more efficient
|
291
|
+
in cases that do not contain custom clean scripts.
|
292
|
+
|
293
|
+
If used as an asynchronous context manager (i.e., within an `async with` block) the cloned
|
294
|
+
copy will be deleted automatically when exiting the block.
|
295
|
+
|
296
|
+
:param dst: The destination path. If `None`, clone to `$FOAM_RUN/foamlib`.
|
297
|
+
|
298
|
+
:return: The clone of the case.
|
299
|
+
|
300
|
+
Example usage: ::
|
301
|
+
import os
|
302
|
+
from pathlib import Path
|
303
|
+
from foamlib import AsyncFoamCase
|
304
|
+
|
305
|
+
pitz_tutorial = AsyncFoamCase(Path(os.environ["FOAM_TUTORIALS"]) / "incompressible/simpleFoam/pitzDaily")
|
225
306
|
|
226
|
-
|
307
|
+
my_pitz = await pitz_tutorial.clone("myPitz")
|
227
308
|
"""
|
228
309
|
calls = ValuedGenerator(self._clone_calls(dst))
|
229
310
|
|
foamlib/_cases/_base.py
CHANGED
@@ -18,16 +18,32 @@ if TYPE_CHECKING:
|
|
18
18
|
|
19
19
|
|
20
20
|
class FoamCaseBase(Sequence["FoamCaseBase.TimeDirectory"]):
|
21
|
+
"""
|
22
|
+
Base class for OpenFOAM cases.
|
23
|
+
|
24
|
+
Provides methods for accessing files and time directories in the case, but does not
|
25
|
+
provide methods for running the case or any commands. Users are encouraged to use
|
26
|
+
`FoamCase` or `AsyncFoamCase` instead of this class.
|
27
|
+
|
28
|
+
Access the time directories of the case as a sequence, e.g. `case[0]` or `case[-1]`.
|
29
|
+
These will return `FoamCaseBase.TimeDirectory` objects.
|
30
|
+
|
31
|
+
:param path: The path to the case directory. Defaults to the current working
|
32
|
+
directory.
|
33
|
+
"""
|
34
|
+
|
21
35
|
def __init__(self, path: os.PathLike[str] | str = Path()) -> None:
|
22
36
|
self.path = Path(path).absolute()
|
23
37
|
|
24
38
|
class TimeDirectory(AbstractSet[FoamFieldFile]):
|
25
39
|
"""
|
26
|
-
|
40
|
+
A time directory in an OpenFOAM case.
|
27
41
|
|
28
|
-
Use to access field files in the directory
|
42
|
+
Use to access field files in the directory (e.g. `time["U"]`). These will be
|
43
|
+
returned as `FoamFieldFile` objects.
|
29
44
|
|
30
|
-
|
45
|
+
It also behaves as a set of `FoamFieldFile` objects (e.g. it can be
|
46
|
+
iterated over with `for field in time: ...`).
|
31
47
|
"""
|
32
48
|
|
33
49
|
def __init__(self, path: os.PathLike[str] | str) -> None:
|
@@ -39,12 +55,12 @@ class FoamCaseBase(Sequence["FoamCaseBase.TimeDirectory"]):
|
|
39
55
|
|
40
56
|
@property
|
41
57
|
def time(self) -> float:
|
42
|
-
"""The time that corresponds to this directory."""
|
58
|
+
"""The time that corresponds to this directory, as a float."""
|
43
59
|
return float(self.path.name)
|
44
60
|
|
45
61
|
@property
|
46
62
|
def name(self) -> str:
|
47
|
-
"""The name of this time directory."""
|
63
|
+
"""The name of this time directory (the time as a string)."""
|
48
64
|
return self.path.name
|
49
65
|
|
50
66
|
def __getitem__(self, key: str) -> FoamFieldFile:
|
foamlib/_cases/_run.py
CHANGED
@@ -46,6 +46,12 @@ if TYPE_CHECKING:
|
|
46
46
|
|
47
47
|
|
48
48
|
class FoamCaseRunBase(FoamCaseBase):
|
49
|
+
"""
|
50
|
+
Abstract base class of `FoamCase` and `AsyncFoamCase`.
|
51
|
+
|
52
|
+
Do not use this class directly: use `FoamCase` or `AsyncFoamCase` instead.
|
53
|
+
"""
|
54
|
+
|
49
55
|
class TimeDirectory(FoamCaseBase.TimeDirectory):
|
50
56
|
@abstractmethod
|
51
57
|
def cell_centers(
|
foamlib/_cases/_slurm.py
CHANGED
@@ -17,7 +17,16 @@ if TYPE_CHECKING:
|
|
17
17
|
|
18
18
|
|
19
19
|
class AsyncSlurmFoamCase(AsyncFoamCase):
|
20
|
-
"""
|
20
|
+
"""
|
21
|
+
An asynchronous OpenFOAM case that launches jobs on a Slurm cluster.
|
22
|
+
|
23
|
+
`AsyncSlurmFoamCase` is a subclass of `AsyncFoamCase`. It provides the same interface,
|
24
|
+
as the latter, except that it will launch jobs on a Slurm cluster (using `salloc` and
|
25
|
+
`srun`) on the user's behalf when running a case or command.
|
26
|
+
|
27
|
+
:param path: The path to the case directory. Defaults to the current working
|
28
|
+
directory.
|
29
|
+
"""
|
21
30
|
|
22
31
|
@staticmethod
|
23
32
|
async def _run(
|
foamlib/_cases/_sync.py
CHANGED
@@ -33,8 +33,21 @@ class FoamCase(FoamCaseRunBase):
|
|
33
33
|
Provides methods for running and cleaning cases, as well as accessing files.
|
34
34
|
|
35
35
|
Access the time directories of the case as a sequence, e.g. `case[0]` or `case[-1]`.
|
36
|
+
These will return `FoamCase.TimeDirectory` objects.
|
36
37
|
|
37
|
-
:param path: The path to the case directory.
|
38
|
+
:param path: The path to the case directory. Defaults to the current working
|
39
|
+
directory.
|
40
|
+
|
41
|
+
Example usage: ::
|
42
|
+
|
43
|
+
from foamlib import FoamCase
|
44
|
+
|
45
|
+
case = FoamCase("path/to/case") # Load an OpenFOAM case
|
46
|
+
case[0]["U"].internal_field = [0, 0, 0] # Set the initial velocity field to zero
|
47
|
+
case.run() # Run the case
|
48
|
+
for time in case: # Iterate over the time directories
|
49
|
+
print(time.time) # Print the time
|
50
|
+
print(time["U"].internal_field) # Print the velocity field
|
38
51
|
"""
|
39
52
|
|
40
53
|
class TimeDirectory(FoamCaseRunBase.TimeDirectory):
|
@@ -43,7 +56,12 @@ class FoamCase(FoamCaseRunBase):
|
|
43
56
|
return FoamCase(self.path.parent)
|
44
57
|
|
45
58
|
def cell_centers(self) -> FoamFieldFile:
|
46
|
-
"""
|
59
|
+
"""
|
60
|
+
Write and return the cell centers.
|
61
|
+
|
62
|
+
Currently only works for reconstructed cases (decomposed cases will need to
|
63
|
+
be reconstructed first).
|
64
|
+
"""
|
47
65
|
calls = ValuedGenerator(self._cell_centers_calls())
|
48
66
|
|
49
67
|
for _ in calls:
|
@@ -107,7 +125,20 @@ class FoamCase(FoamCaseRunBase):
|
|
107
125
|
"""
|
108
126
|
Clean this case.
|
109
127
|
|
110
|
-
|
128
|
+
If a `clean` or `Allclean` script is present in the case directory, it will be invoked.
|
129
|
+
Otherwise, the case directory will be cleaned using these rules:
|
130
|
+
|
131
|
+
- All time directories except `0` will be deleted.
|
132
|
+
- The `0` time directory will be deleted if `0.orig` exists.
|
133
|
+
- `processor*` directories will be deleted if a `system/decomposeParDict` file is present.
|
134
|
+
- `constant/polyMesh` will be deleted if a `system/blockMeshDict` file is present.
|
135
|
+
- All `log.*` files will be deleted.
|
136
|
+
|
137
|
+
If this behavior is not appropriate for a case, it is recommended to write a custom
|
138
|
+
`clean` script.
|
139
|
+
|
140
|
+
:param check: If True, raise a `CalledProcessError` if the clean script returns a
|
141
|
+
non-zero exit code.
|
111
142
|
"""
|
112
143
|
for _ in self._clean_calls(check=check):
|
113
144
|
pass
|
@@ -128,11 +159,35 @@ class FoamCase(FoamCaseRunBase):
|
|
128
159
|
"""
|
129
160
|
Run this case, or a specified command in the context of this case.
|
130
161
|
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
:
|
135
|
-
|
162
|
+
If `cmd` is given, this method will run the given command in the context of the case.
|
163
|
+
|
164
|
+
If `cmd` is `None`, a series of heuristic rules will be used to run the case. This works as
|
165
|
+
follows:
|
166
|
+
|
167
|
+
- If a `run`, `Allrun` or `Allrun-parallel` script is present in the case directory,
|
168
|
+
it will be invoked. If both `run` and `Allrun` are present, `Allrun` will be used. If
|
169
|
+
both `Allrun` and `Allrun-parallel` are present and `parallel` is `None`, an error will
|
170
|
+
be raised.
|
171
|
+
- If no run script is present but an `Allrun.pre` script exists, it will be invoked.
|
172
|
+
- Otherwise, if a `system/blockMeshDict` file is present, the method will call
|
173
|
+
`self.block_mesh()`.
|
174
|
+
- Then, if a `0.orig` directory is present, it will call `self.restore_0_dir()`.
|
175
|
+
- Then, if the case is to be run in parallel (see the `parallel` option) and no
|
176
|
+
`processor*` directories exist but a`system/decomposeParDict` file is present, it will
|
177
|
+
call `self.decompose_par()`.
|
178
|
+
- Then, it will run the case using the application specified in the `controlDict` file.
|
179
|
+
|
180
|
+
If this behavior is not appropriate for a case, it is recommended to write a custom
|
181
|
+
`run`, `Allrun`, `Allrun-parallel` or `Allrun.pre` script.
|
182
|
+
|
183
|
+
:param cmd: The command to run. If `None`, run the case. If a sequence, the first element
|
184
|
+
is the command and the rest are arguments. If a string, `cmd` is executed in a shell.
|
185
|
+
:param parallel: If `True`, run in parallel using MPI. If None, autodetect whether to run
|
186
|
+
in parallel.
|
187
|
+
:param cpus: The number of CPUs to use. If `None`, autodetect from to the case.
|
188
|
+
:param check: If `True`, raise a `CalledProcessError` if any command returns a non-zero
|
189
|
+
exit code.
|
190
|
+
:param log: If `True`, log the command output to `log.*` files in the case directory.
|
136
191
|
"""
|
137
192
|
for _ in self._run_calls(
|
138
193
|
cmd=cmd, parallel=parallel, cpus=cpus, check=check, log=log
|
@@ -163,9 +218,21 @@ class FoamCase(FoamCaseRunBase):
|
|
163
218
|
"""
|
164
219
|
Make a copy of this case.
|
165
220
|
|
166
|
-
|
221
|
+
If used as a context manager (i.e., within a `with` block) the copy will be deleted
|
222
|
+
automatically when exiting the block.
|
223
|
+
|
224
|
+
:param dst: The destination path. If `None`, clone to `$FOAM_RUN/foamlib`.
|
167
225
|
|
168
|
-
:
|
226
|
+
:return: The copy of the case.
|
227
|
+
|
228
|
+
Example usage: ::
|
229
|
+
import os
|
230
|
+
from pathlib import Path
|
231
|
+
from foamlib import FoamCase
|
232
|
+
|
233
|
+
pitz_tutorial = FoamCase(Path(os.environ["FOAM_TUTORIALS"]) / "incompressible/simpleFoam/pitzDaily")
|
234
|
+
|
235
|
+
my_pitz = pitz_tutorial.copy("myPitz")
|
169
236
|
"""
|
170
237
|
calls = ValuedGenerator(self._copy_calls(dst))
|
171
238
|
|
@@ -178,9 +245,24 @@ class FoamCase(FoamCaseRunBase):
|
|
178
245
|
"""
|
179
246
|
Clone this case (make a clean copy).
|
180
247
|
|
181
|
-
|
248
|
+
This is equivalent to running `self.copy().clean()`, but it can be more efficient in cases
|
249
|
+
that do not contain custom clean scripts.
|
250
|
+
|
251
|
+
If used as a context manager (i.e., within a `with` block) the cloned copy will be deleted
|
252
|
+
automatically when exiting the block.
|
253
|
+
|
254
|
+
:param dst: The destination path. If `None`, clone to `$FOAM_RUN/foamlib`.
|
255
|
+
|
256
|
+
:return: The clone of the case.
|
257
|
+
|
258
|
+
Example usage: ::
|
259
|
+
import os
|
260
|
+
from pathlib import Path
|
261
|
+
from foamlib import FoamCase
|
262
|
+
|
263
|
+
pitz_tutorial = FoamCase(Path(os.environ["FOAM_TUTORIALS"]) / "incompressible/simpleFoam/pitzDaily")
|
182
264
|
|
183
|
-
|
265
|
+
my_pitz = pitz_tutorial.clone("myPitz")
|
184
266
|
"""
|
185
267
|
calls = ValuedGenerator(self._clone_calls(dst))
|
186
268
|
|
foamlib/_files/_files.py
CHANGED
@@ -23,8 +23,9 @@ from ._types import (
|
|
23
23
|
Dict_,
|
24
24
|
Dimensioned,
|
25
25
|
DimensionSet,
|
26
|
-
|
26
|
+
EntryLike,
|
27
27
|
Field,
|
28
|
+
FieldLike,
|
28
29
|
File,
|
29
30
|
MutableEntry,
|
30
31
|
)
|
@@ -40,9 +41,46 @@ class FoamFile(
|
|
40
41
|
"""
|
41
42
|
An OpenFOAM data file.
|
42
43
|
|
43
|
-
|
44
|
+
`FoamFile` supports most OpenFOAM data and configuration files (i.e., files with a
|
45
|
+
"FoamFile" header), including those with regular expressions and #-based directives.
|
46
|
+
Notable exceptions are FoamFiles with #codeStreams and those multiple #-directives
|
47
|
+
with the same name, which are currently not supported. Non-FoamFile output files are
|
48
|
+
also not suppored by this class. Regular expressions and #-based directives can be
|
49
|
+
accessed and modified, but they are not evaluated or expanded by this library.
|
44
50
|
|
45
|
-
Use as a
|
51
|
+
Use `FoamFile` as a mutable mapping (i.e., like a `dict`) to access and modify
|
52
|
+
entries. When accessing a sub-dictionary, the returned value will be a
|
53
|
+
`FoamFile.SubDict` object, that allows for further access and modification of nested
|
54
|
+
dictionaries within the `FoamFile` in a single operation.
|
55
|
+
|
56
|
+
If the `FoamFile` does not store a dictionary, the main stored value can be accessed
|
57
|
+
and modified by passing `None` as the key (e.g., `file[None]`).
|
58
|
+
|
59
|
+
You can also use the `FoamFile` as a context manager (i.e., within a `with` block)
|
60
|
+
to make multiple changes to the file while saving any and all changes only once at
|
61
|
+
the end.
|
62
|
+
|
63
|
+
:param path: The path to the file. If the file does not exist, it will be created
|
64
|
+
when the first change is made. However, if an attempt is made to access entries
|
65
|
+
in a non-existent file, a `FileNotFoundError` will be raised.
|
66
|
+
|
67
|
+
Example usage: ::
|
68
|
+
from foamlib import FoamFile
|
69
|
+
|
70
|
+
file = FoamFile("path/to/case/system/controlDict") # Load a controlDict file
|
71
|
+
print(file["endTime"]) # Print the end time
|
72
|
+
file["writeInterval"] = 100 # Set the write interval to 100
|
73
|
+
file["writeFormat"] = "binary" # Set the write format to binary
|
74
|
+
|
75
|
+
or (better): ::
|
76
|
+
from foamlib import FoamCase
|
77
|
+
|
78
|
+
case = FoamCase("path/to/case")
|
79
|
+
|
80
|
+
with case.control_dict as file: # Load the controlDict file
|
81
|
+
print(file["endTime"]) # Print the end time
|
82
|
+
file["writeInterval"] = 100
|
83
|
+
file["writeFormat"] = "binary"
|
46
84
|
"""
|
47
85
|
|
48
86
|
Dimensioned = Dimensioned
|
@@ -51,7 +89,32 @@ class FoamFile(
|
|
51
89
|
class SubDict(
|
52
90
|
MutableMapping[str, MutableEntry],
|
53
91
|
):
|
54
|
-
"""
|
92
|
+
"""
|
93
|
+
An OpenFOAM sub-dictionary within a file.
|
94
|
+
|
95
|
+
`FoamFile.SubDict` is a mutable mapping that allows for accessing and modifying
|
96
|
+
nested dictionaries within a `FoamFile` in a single operation. It behaves like a
|
97
|
+
`dict` and can be used to access and modify entries in the sub-dictionary.
|
98
|
+
|
99
|
+
To obtain a `FoamFile.SubDict` object, access a sub-dictionary in a `FoamFile`
|
100
|
+
object (e.g., `file["subDict"]`).
|
101
|
+
|
102
|
+
Example usage: ::
|
103
|
+
from foamlib import FoamFile
|
104
|
+
|
105
|
+
file = FoamFile("path/to/case/system/fvSchemes") # Load an fvSchemes file
|
106
|
+
print(file["ddtSchemes"]["default"]) # Print the default ddt scheme
|
107
|
+
file["ddtSchemes"]["default"] = "Euler" # Set the default ddt scheme
|
108
|
+
|
109
|
+
or (better): ::
|
110
|
+
from foamlib import FoamCase
|
111
|
+
|
112
|
+
case = FoamCase("path/to/case")
|
113
|
+
|
114
|
+
with case.fv_schemes as file: # Load the fvSchemes file
|
115
|
+
print(file["ddtSchemes"]["default"])
|
116
|
+
file["ddtSchemes"]["default"] = "Euler"
|
117
|
+
"""
|
55
118
|
|
56
119
|
def __init__(self, _file: FoamFile, _keywords: tuple[str, ...]) -> None:
|
57
120
|
self._file = _file
|
@@ -63,7 +126,7 @@ class FoamFile(
|
|
63
126
|
def __setitem__(
|
64
127
|
self,
|
65
128
|
keyword: str,
|
66
|
-
data:
|
129
|
+
data: EntryLike,
|
67
130
|
) -> None:
|
68
131
|
self._file[(*self._keywords, keyword)] = data
|
69
132
|
|
@@ -93,7 +156,7 @@ class FoamFile(
|
|
93
156
|
return f"{type(self).__qualname__}('{self._file}', {self._keywords})"
|
94
157
|
|
95
158
|
def as_dict(self) -> Dict_:
|
96
|
-
"""Return a nested dict representation of the dictionary."""
|
159
|
+
"""Return a nested dict representation of the sub-dictionary."""
|
97
160
|
ret = self._file.as_dict(include_header=True)
|
98
161
|
|
99
162
|
for k in self._keywords:
|
@@ -190,13 +253,15 @@ class FoamFile(
|
|
190
253
|
return FoamFile.SubDict(self, keywords)
|
191
254
|
return deepcopy(value)
|
192
255
|
|
193
|
-
def __setitem__(
|
256
|
+
def __setitem__(
|
257
|
+
self, keywords: str | tuple[str, ...] | None, data: EntryLike
|
258
|
+
) -> None:
|
194
259
|
if not keywords:
|
195
260
|
keywords = ()
|
196
261
|
elif not isinstance(keywords, tuple):
|
197
262
|
keywords = (keywords,)
|
198
263
|
|
199
|
-
if keywords and not isinstance(normalize(keywords[-1]), str):
|
264
|
+
if keywords and not isinstance(normalize(keywords[-1], kind=Kind.KEYWORD), str):
|
200
265
|
msg = f"Invalid keyword: {keywords[-1]}"
|
201
266
|
raise ValueError(msg)
|
202
267
|
|
@@ -379,15 +444,44 @@ class FoamFile(
|
|
379
444
|
|
380
445
|
|
381
446
|
class FoamFieldFile(FoamFile):
|
382
|
-
"""
|
447
|
+
"""
|
448
|
+
Subclass of `FoamFile` for representing OpenFOAM field files specifically.
|
449
|
+
|
450
|
+
The difference between `FoamFieldFile` and `FoamFile` is that `FoamFieldFile` has
|
451
|
+
the additional properties `dimensions`, `internal_field`, and `boundary_field` that
|
452
|
+
are commonly found in OpenFOAM field files. Note that these are only a shorthand for
|
453
|
+
accessing the corresponding entries in the file.
|
454
|
+
|
455
|
+
See `FoamFile` for more information on how to read and edit OpenFOAM files.
|
456
|
+
|
457
|
+
:param path: The path to the file. If the file does not exist, it will be created
|
458
|
+
when the first change is made. However, if an attempt is made to access entries
|
459
|
+
in a non-existent file, a `FileNotFoundError` will be raised.
|
460
|
+
|
461
|
+
Example usage: ::
|
462
|
+
from foamlib import FoamFieldFile
|
463
|
+
|
464
|
+
field = FoamFieldFile("path/to/case/0/U") # Load a field
|
465
|
+
print(field.dimensions) # Print the dimensions
|
466
|
+
print(field.boundary_field) # Print the boundary field
|
467
|
+
field.internal_field = [0, 0, 0] # Set the internal field
|
468
|
+
|
469
|
+
or (better): ::
|
470
|
+
from foamlib import FoamCase
|
471
|
+
|
472
|
+
case = FoamCase("path/to/case")
|
473
|
+
|
474
|
+
with case[0]["U"] as field: # Load a field
|
475
|
+
print(field.dimensions)
|
476
|
+
print(field.boundary_field)
|
477
|
+
field.internal_field = [0, 0, 0]
|
478
|
+
"""
|
383
479
|
|
384
480
|
class BoundariesSubDict(FoamFile.SubDict):
|
385
|
-
def __getitem__(self, keyword: str) -> FoamFieldFile.BoundarySubDict:
|
481
|
+
def __getitem__(self, keyword: str) -> FoamFieldFile.BoundarySubDict | Data:
|
386
482
|
value = super().__getitem__(keyword)
|
387
|
-
if
|
388
|
-
assert
|
389
|
-
msg = f"boundary {keyword} is not a dictionary"
|
390
|
-
raise TypeError(msg)
|
483
|
+
if isinstance(value, FoamFieldFile.SubDict):
|
484
|
+
assert isinstance(value, FoamFieldFile.BoundarySubDict)
|
391
485
|
return value
|
392
486
|
|
393
487
|
class BoundarySubDict(FoamFile.SubDict):
|
@@ -419,7 +513,7 @@ class FoamFieldFile(FoamFile):
|
|
419
513
|
@value.setter
|
420
514
|
def value(
|
421
515
|
self,
|
422
|
-
value:
|
516
|
+
value: FieldLike,
|
423
517
|
) -> None:
|
424
518
|
self["value"] = value
|
425
519
|
|
@@ -466,7 +560,7 @@ class FoamFieldFile(FoamFile):
|
|
466
560
|
@internal_field.setter
|
467
561
|
def internal_field(
|
468
562
|
self,
|
469
|
-
value:
|
563
|
+
value: FieldLike,
|
470
564
|
) -> None:
|
471
565
|
self["internalField"] = value
|
472
566
|
|
foamlib/_files/_serialization.py
CHANGED
@@ -12,7 +12,15 @@ else:
|
|
12
12
|
import numpy as np
|
13
13
|
|
14
14
|
from ._parsing import parse_data
|
15
|
-
from ._types import
|
15
|
+
from ._types import (
|
16
|
+
Data,
|
17
|
+
DataLike,
|
18
|
+
Dimensioned,
|
19
|
+
DimensionSet,
|
20
|
+
Entry,
|
21
|
+
EntryLike,
|
22
|
+
is_sequence,
|
23
|
+
)
|
16
24
|
|
17
25
|
|
18
26
|
class Kind(Enum):
|
@@ -23,17 +31,18 @@ class Kind(Enum):
|
|
23
31
|
BINARY_FIELD = auto()
|
24
32
|
SCALAR_BINARY_FIELD = auto()
|
25
33
|
DIMENSIONS = auto()
|
34
|
+
KEYWORD = auto()
|
26
35
|
|
27
36
|
|
28
37
|
@overload
|
29
|
-
def normalize(data:
|
38
|
+
def normalize(data: DataLike, *, kind: Kind = Kind.DEFAULT) -> Data: ...
|
30
39
|
|
31
40
|
|
32
41
|
@overload
|
33
|
-
def normalize(data:
|
42
|
+
def normalize(data: EntryLike, *, kind: Kind = Kind.DEFAULT) -> Entry: ...
|
34
43
|
|
35
44
|
|
36
|
-
def normalize(data:
|
45
|
+
def normalize(data: EntryLike, *, kind: Kind = Kind.DEFAULT) -> Entry:
|
37
46
|
if kind in (
|
38
47
|
Kind.ASCII_FIELD,
|
39
48
|
Kind.SCALAR_ASCII_FIELD,
|
@@ -52,12 +61,12 @@ def normalize(data: Entry, *, kind: Kind = Kind.DEFAULT) -> Entry:
|
|
52
61
|
if arr.ndim == 1 or (arr.ndim == 2 and arr.shape[1] in (3, 6, 9)):
|
53
62
|
return arr # type: ignore [return-value]
|
54
63
|
|
55
|
-
return data
|
64
|
+
return [normalize(d, kind=Kind.SINGLE_ENTRY) for d in data]
|
56
65
|
|
57
66
|
if isinstance(data, int):
|
58
67
|
return float(data)
|
59
68
|
|
60
|
-
return data
|
69
|
+
return normalize(data)
|
61
70
|
|
62
71
|
if isinstance(data, np.ndarray):
|
63
72
|
ret = data.tolist()
|
@@ -83,7 +92,10 @@ def normalize(data: Entry, *, kind: Kind = Kind.DEFAULT) -> Entry:
|
|
83
92
|
return [normalize(d, kind=Kind.SINGLE_ENTRY) for d in data]
|
84
93
|
|
85
94
|
if isinstance(data, str):
|
86
|
-
|
95
|
+
parsed_data = parse_data(data)
|
96
|
+
if kind == Kind.KEYWORD and isinstance(parsed_data, bool):
|
97
|
+
return data
|
98
|
+
return parsed_data
|
87
99
|
|
88
100
|
if isinstance(
|
89
101
|
data,
|
@@ -96,7 +108,7 @@ def normalize(data: Entry, *, kind: Kind = Kind.DEFAULT) -> Entry:
|
|
96
108
|
|
97
109
|
|
98
110
|
def dumps(
|
99
|
-
data:
|
111
|
+
data: EntryLike,
|
100
112
|
*,
|
101
113
|
kind: Kind = Kind.DEFAULT,
|
102
114
|
) -> bytes:
|
foamlib/_files/_types.py
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
import sys
|
4
|
-
from dataclasses import dataclass
|
5
4
|
from enum import Enum
|
6
5
|
from typing import Dict, NamedTuple, Optional, Union
|
7
6
|
|
@@ -33,8 +32,13 @@ class DimensionSet(NamedTuple):
|
|
33
32
|
|
34
33
|
Tensor = Union[
|
35
34
|
float,
|
35
|
+
"np.ndarray[tuple[int], np.dtype[np.float64]]",
|
36
|
+
]
|
37
|
+
|
38
|
+
TensorLike = Union[
|
36
39
|
Sequence[float],
|
37
|
-
"np.ndarray[tuple[()]
|
40
|
+
"np.ndarray[tuple[()], np.dtype[np.float64]]",
|
41
|
+
Tensor,
|
38
42
|
]
|
39
43
|
|
40
44
|
|
@@ -71,21 +75,25 @@ class TensorKind(Enum):
|
|
71
75
|
raise ValueError(msg)
|
72
76
|
|
73
77
|
|
74
|
-
@dataclass
|
75
78
|
class Dimensioned:
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
+
def __init__(
|
80
|
+
self,
|
81
|
+
value: TensorLike,
|
82
|
+
dimensions: DimensionSet | Sequence[float],
|
83
|
+
name: str | None = None,
|
84
|
+
) -> None:
|
85
|
+
if is_sequence(value):
|
86
|
+
self.value: Tensor = np.array(value, dtype=float) # type: ignore [assignment]
|
87
|
+
else:
|
88
|
+
assert isinstance(value, (int, float, np.ndarray))
|
89
|
+
self.value = float(value)
|
79
90
|
|
80
|
-
|
81
|
-
|
82
|
-
self.value = np.asarray(self.value, dtype=float) # type: ignore [assignment]
|
91
|
+
if not isinstance(dimensions, DimensionSet):
|
92
|
+
self.dimensions = DimensionSet(*dimensions)
|
83
93
|
else:
|
84
|
-
|
85
|
-
self.value = float(self.value)
|
94
|
+
self.dimensions = dimensions
|
86
95
|
|
87
|
-
|
88
|
-
self.dimensions = DimensionSet(*self.dimensions)
|
96
|
+
self.name = name
|
89
97
|
|
90
98
|
def __eq__(self, other: object) -> bool:
|
91
99
|
if not isinstance(other, Dimensioned):
|
@@ -99,11 +107,18 @@ class Dimensioned:
|
|
99
107
|
|
100
108
|
|
101
109
|
Field = Union[
|
102
|
-
|
103
|
-
Sequence[Tensor],
|
110
|
+
float,
|
104
111
|
"np.ndarray[tuple[int] | tuple[int, int], np.dtype[np.float64 | np.float32]]",
|
105
112
|
]
|
106
113
|
|
114
|
+
FieldLike = Union[
|
115
|
+
TensorLike,
|
116
|
+
Sequence[TensorLike],
|
117
|
+
Sequence[Sequence[TensorLike]],
|
118
|
+
Field,
|
119
|
+
]
|
120
|
+
|
121
|
+
|
107
122
|
Data = Union[
|
108
123
|
str,
|
109
124
|
int,
|
@@ -123,11 +138,22 @@ Entry = Union[
|
|
123
138
|
A value that can be stored in an OpenFOAM file.
|
124
139
|
"""
|
125
140
|
|
141
|
+
DataLike = Union[
|
142
|
+
FieldLike,
|
143
|
+
Sequence["EntryLike"],
|
144
|
+
Data,
|
145
|
+
]
|
146
|
+
|
147
|
+
EntryLike = Union[
|
148
|
+
DataLike,
|
149
|
+
Mapping[str, "EntryLike"],
|
150
|
+
]
|
151
|
+
|
126
152
|
|
127
153
|
def is_sequence(
|
128
|
-
value:
|
154
|
+
value: EntryLike,
|
129
155
|
) -> TypeGuard[
|
130
|
-
Sequence[
|
156
|
+
Sequence[EntryLike]
|
131
157
|
| np.ndarray[tuple[int] | tuple[int, int], np.dtype[np.float64 | np.float32]]
|
132
158
|
]:
|
133
159
|
return (isinstance(value, Sequence) and not isinstance(value, str)) or (
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: foamlib
|
3
|
-
Version: 0.8.
|
3
|
+
Version: 0.8.9
|
4
4
|
Summary: A Python interface for interacting with OpenFOAM
|
5
5
|
Project-URL: Homepage, https://github.com/gerlero/foamlib
|
6
6
|
Project-URL: Repository, https://github.com/gerlero/foamlib
|
@@ -27,7 +27,6 @@ Classifier: Typing :: Typed
|
|
27
27
|
Requires-Python: >=3.7
|
28
28
|
Requires-Dist: aioshutil<2,>=1
|
29
29
|
Requires-Dist: numpy<3,>=1
|
30
|
-
Requires-Dist: numpy<3,>=1.25.0; python_version >= '3.10'
|
31
30
|
Requires-Dist: pyparsing<4,>=3.1.2
|
32
31
|
Requires-Dist: rich<15,>=13
|
33
32
|
Requires-Dist: typing-extensions<5,>=4; python_version < '3.11'
|
@@ -37,22 +36,29 @@ Requires-Dist: pytest-asyncio<0.27,>=0.21; extra == 'dev'
|
|
37
36
|
Requires-Dist: pytest-cov; extra == 'dev'
|
38
37
|
Requires-Dist: pytest<9,>=7; extra == 'dev'
|
39
38
|
Requires-Dist: ruff; extra == 'dev'
|
39
|
+
Requires-Dist: scipy-stubs; (python_version >= '3.10') and extra == 'dev'
|
40
|
+
Requires-Dist: scipy<2,>=1; extra == 'dev'
|
40
41
|
Requires-Dist: sphinx-rtd-theme; extra == 'dev'
|
41
42
|
Requires-Dist: sphinx<9,>=5; extra == 'dev'
|
42
43
|
Provides-Extra: docs
|
44
|
+
Requires-Dist: ruff; extra == 'docs'
|
43
45
|
Requires-Dist: sphinx-rtd-theme; extra == 'docs'
|
44
46
|
Requires-Dist: sphinx<9,>=5; extra == 'docs'
|
45
47
|
Provides-Extra: lint
|
46
48
|
Requires-Dist: ruff; extra == 'lint'
|
47
49
|
Provides-Extra: test
|
50
|
+
Requires-Dist: mypy<2,>=1; extra == 'test'
|
48
51
|
Requires-Dist: pytest-asyncio<0.27,>=0.21; extra == 'test'
|
49
52
|
Requires-Dist: pytest-cov; extra == 'test'
|
50
53
|
Requires-Dist: pytest<9,>=7; extra == 'test'
|
54
|
+
Requires-Dist: scipy<2,>=1; extra == 'test'
|
51
55
|
Provides-Extra: typing
|
52
56
|
Requires-Dist: mypy<2,>=1; extra == 'typing'
|
53
57
|
Requires-Dist: pytest-asyncio<0.27,>=0.21; extra == 'typing'
|
54
58
|
Requires-Dist: pytest-cov; extra == 'typing'
|
55
59
|
Requires-Dist: pytest<9,>=7; extra == 'typing'
|
60
|
+
Requires-Dist: scipy-stubs; (python_version >= '3.10') and extra == 'typing'
|
61
|
+
Requires-Dist: scipy<2,>=1; extra == 'typing'
|
56
62
|
Description-Content-Type: text/markdown
|
57
63
|
|
58
64
|
[<img alt="foamlib" src="https://github.com/gerlero/foamlib/raw/main/logo.png" height="65">](https://github.com/gerlero/foamlib)
|
@@ -73,11 +79,18 @@ Description-Content-Type: text/markdown
|
|
73
79
|
|
74
80
|
**foamlib** provides a simple, modern, ergonomic and fast Python interface for interacting with [OpenFOAM](https://www.openfoam.com).
|
75
81
|
|
76
|
-
<
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
</
|
82
|
+
<div align="center">
|
83
|
+
<img alt="benchmark" src="https://github.com/gerlero/foamlib/raw/main/benchmark/benchmark.png" height="250">
|
84
|
+
|
85
|
+
Parsing a volVectorField with 200k cells.<sup>[1](#benchmark)</sup>
|
86
|
+
</div>
|
87
|
+
|
88
|
+
|
89
|
+
## 🚀 Introduction
|
90
|
+
|
91
|
+
**foamlib** is a Python package designed to simplify the manipulation of OpenFOAM cases and files. Its standalone parser makes it easy to work with OpenFOAM’s input/output files from Python, while its case-handling capabilities facilitate various execution workflows—reducing boilerplate code and enabling efficient Python-based pre- and post-processing, as well as simulation management.
|
92
|
+
|
93
|
+
Compared to [PyFoam](https://openfoamwiki.net/index.php/Contrib/PyFoam) and other similar tools like [fluidfoam](https://github.com/fluiddyn/fluidfoam), [fluidsimfoam](https://foss.heptapod.net/fluiddyn/fluidsimfoam), and [Ofpp](https://github.com/xu-xianghua/ofpp), **foamlib** offers advantages such as modern Python compatibility, support for binary-formatted fields, a fully type-hinted API, and asynchronous operations; making OpenFOAM workflows more accessible and streamlined.
|
81
94
|
|
82
95
|
## 👋 Basics
|
83
96
|
|
@@ -208,6 +221,159 @@ case = FoamCase(Path(__file__).parent)
|
|
208
221
|
case.run()
|
209
222
|
```
|
210
223
|
|
211
|
-
##
|
224
|
+
## ▶️ A complete example
|
225
|
+
|
226
|
+
The following is a fully self-contained example that demonstrates how to create an OpenFOAM case from scratch, run it, and analyze the results.
|
227
|
+
|
228
|
+
<details>
|
229
|
+
|
230
|
+
<summary>Example</summary>
|
231
|
+
|
232
|
+
```python
|
233
|
+
#!/usr/bin/env python3
|
234
|
+
"""Check the diffusion of a scalar field in a scalarTransportFoam case."""
|
235
|
+
|
236
|
+
import shutil
|
237
|
+
from pathlib import Path
|
238
|
+
|
239
|
+
import numpy as np
|
240
|
+
from scipy.special import erfc
|
241
|
+
from foamlib import FoamCase
|
242
|
+
|
243
|
+
path = Path(__file__).parent / "diffusionCheck"
|
244
|
+
shutil.rmtree(path, ignore_errors=True)
|
245
|
+
path.mkdir(parents=True)
|
246
|
+
(path / "system").mkdir()
|
247
|
+
(path / "constant").mkdir()
|
248
|
+
(path / "0").mkdir()
|
249
|
+
|
250
|
+
case = FoamCase(path)
|
251
|
+
|
252
|
+
with case.control_dict as f:
|
253
|
+
f["application"] = "scalarTransportFoam"
|
254
|
+
f["startFrom"] = "latestTime"
|
255
|
+
f["stopAt"] = "endTime"
|
256
|
+
f["endTime"] = 5
|
257
|
+
f["deltaT"] = 1e-3
|
258
|
+
f["writeControl"] = "adjustableRunTime"
|
259
|
+
f["writeInterval"] = 1
|
260
|
+
f["purgeWrite"] = 0
|
261
|
+
f["writeFormat"] = "ascii"
|
262
|
+
f["writePrecision"] = 6
|
263
|
+
f["writeCompression"] = False
|
264
|
+
f["timeFormat"] = "general"
|
265
|
+
f["timePrecision"] = 6
|
266
|
+
f["adjustTimeStep"] = False
|
267
|
+
f["runTimeModifiable"] = False
|
268
|
+
|
269
|
+
with case.fv_schemes as f:
|
270
|
+
f["ddtSchemes"] = {"default": "Euler"}
|
271
|
+
f["gradSchemes"] = {"default": "Gauss linear"}
|
272
|
+
f["divSchemes"] = {"default": "none", "div(phi,U)": "Gauss linear", "div(phi,T)": "Gauss linear"}
|
273
|
+
f["laplacianSchemes"] = {"default": "Gauss linear corrected"}
|
274
|
+
|
275
|
+
with case.fv_solution as f:
|
276
|
+
f["solvers"] = {"T": {"solver": "PBiCG", "preconditioner": "DILU", "tolerance": 1e-6, "relTol": 0}}
|
277
|
+
|
278
|
+
with case.block_mesh_dict as f:
|
279
|
+
f["scale"] = 1
|
280
|
+
f["vertices"] = [
|
281
|
+
[0, 0, 0],
|
282
|
+
[1, 0, 0],
|
283
|
+
[1, 0.5, 0],
|
284
|
+
[1, 1, 0],
|
285
|
+
[0, 1, 0],
|
286
|
+
[0, 0.5, 0],
|
287
|
+
[0, 0, 0.1],
|
288
|
+
[1, 0, 0.1],
|
289
|
+
[1, 0.5, 0.1],
|
290
|
+
[1, 1, 0.1],
|
291
|
+
[0, 1, 0.1],
|
292
|
+
[0, 0.5, 0.1],
|
293
|
+
]
|
294
|
+
f["blocks"] = [
|
295
|
+
"hex", [0, 1, 2, 5, 6, 7, 8, 11], [400, 20, 1], "simpleGrading", [1, 1, 1],
|
296
|
+
"hex", [5, 2, 3, 4, 11, 8, 9, 10], [400, 20, 1], "simpleGrading", [1, 1, 1],
|
297
|
+
]
|
298
|
+
f["edges"] = []
|
299
|
+
f["boundary"] = [
|
300
|
+
("inletUp", {"type": "patch", "faces": [[5, 4, 10, 11]]}),
|
301
|
+
("inletDown", {"type": "patch", "faces": [[0, 5, 11, 6]]}),
|
302
|
+
("outletUp", {"type": "patch", "faces": [[2, 3, 9, 8]]}),
|
303
|
+
("outletDown", {"type": "patch", "faces": [[1, 2, 8, 7]]}),
|
304
|
+
("walls", {"type": "wall", "faces": [[4, 3, 9, 10], [0, 1, 7, 6]]}),
|
305
|
+
("frontAndBack", {"type": "empty", "faces": [[0, 1, 2, 5], [5, 2, 3, 4], [6, 7, 8, 11], [11, 8, 9, 10]]}),
|
306
|
+
]
|
307
|
+
f["mergePatchPairs"] = []
|
308
|
+
|
309
|
+
with case.transport_properties as f:
|
310
|
+
f["DT"] = f.Dimensioned(1e-3, f.DimensionSet(length=2, time=-1), "DT")
|
311
|
+
|
312
|
+
with case[0]["U"] as f:
|
313
|
+
f.dimensions = f.DimensionSet(length=1, time=-1)
|
314
|
+
f.internal_field = [1, 0, 0]
|
315
|
+
f.boundary_field = {
|
316
|
+
"inletUp": {"type": "fixedValue", "value": [1, 0, 0]},
|
317
|
+
"inletDown": {"type": "fixedValue", "value": [1, 0, 0]},
|
318
|
+
"outletUp": {"type": "zeroGradient"},
|
319
|
+
"outletDown": {"type": "zeroGradient"},
|
320
|
+
"walls": {"type": "zeroGradient"},
|
321
|
+
"frontAndBack": {"type": "empty"},
|
322
|
+
}
|
323
|
+
|
324
|
+
with case[0]["T"] as f:
|
325
|
+
f.dimensions = f.DimensionSet(temperature=1)
|
326
|
+
f.internal_field = 0
|
327
|
+
f.boundary_field = {
|
328
|
+
"inletUp": {"type": "fixedValue", "value": 0},
|
329
|
+
"inletDown": {"type": "fixedValue", "value": 1},
|
330
|
+
"outletUp": {"type": "zeroGradient"},
|
331
|
+
"outletDown": {"type": "zeroGradient"},
|
332
|
+
"walls": {"type": "zeroGradient"},
|
333
|
+
"frontAndBack": {"type": "empty"},
|
334
|
+
}
|
335
|
+
|
336
|
+
case.run()
|
337
|
+
|
338
|
+
x, y, z = case[0].cell_centers().internal_field.T
|
339
|
+
|
340
|
+
end = x == x.max()
|
341
|
+
x = x[end]
|
342
|
+
y = y[end]
|
343
|
+
z = z[end]
|
344
|
+
|
345
|
+
DT = case.transport_properties["DT"].value
|
346
|
+
U = case[0]["U"].internal_field[0]
|
347
|
+
|
348
|
+
for time in case[1:]:
|
349
|
+
if U*time.time < 2*x.max():
|
350
|
+
continue
|
351
|
+
|
352
|
+
T = time["T"].internal_field[end]
|
353
|
+
analytical = 0.5 * erfc((y - 0.5) / np.sqrt(4 * DT * x/U))
|
354
|
+
if np.allclose(T, analytical, atol=0.1):
|
355
|
+
print(f"Time {time.time}: OK")
|
356
|
+
else:
|
357
|
+
raise RuntimeError(f"Time {time.time}: {T} != {analytical}")
|
358
|
+
```
|
359
|
+
|
360
|
+
</details>
|
361
|
+
|
362
|
+
|
363
|
+
## 📘 API documentation
|
364
|
+
|
365
|
+
For more information on how to use **foamlibs**'s classes and methods, check out the [documentation](https://foamlib.readthedocs.io/).
|
366
|
+
|
367
|
+
## 🙋 Support
|
368
|
+
|
369
|
+
If you have any questions or need help, feel free to open a [discussion](https://github.com/gerlero/foamlib/discussions).
|
370
|
+
|
371
|
+
If you believe you have found a bug in **foamlib**, please open an [issue](https://github.com/gerlero/foamlib/issues).
|
372
|
+
|
373
|
+
## 🧑💻 Contributing
|
374
|
+
|
375
|
+
You're welcome to contribute to **foamlib**! Check out the [contributing guidelines](CONTRIBUTING.md) for more information.
|
376
|
+
|
377
|
+
## Footnotes
|
212
378
|
|
213
|
-
|
379
|
+
<a id="benchmark">[1]</a> foamlib 0.8.1 vs PyFoam 2023.7 on a MacBook Air (2020, M1) with 8 GB of RAM. [Benchmark script](benchmark/benchmark.py).
|
@@ -0,0 +1,20 @@
|
|
1
|
+
foamlib/__init__.py,sha256=UPZX4CKrZGbzDVviSntbJjI7Bcv7ufYZWlGZbVVlHyQ,452
|
2
|
+
foamlib/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
3
|
+
foamlib/_cases/__init__.py,sha256=_A1TTHuQfS9FH2_33lSEyLtOJZGFHZBco1tWJCVOHks,358
|
4
|
+
foamlib/_cases/_async.py,sha256=e4lGTcQBbFGwfG6SmJks5aa5LWd_0dy01kgKZWAgTGQ,11655
|
5
|
+
foamlib/_cases/_base.py,sha256=CNfutRnqniTNS1xQZ2EUeK0n2VXTgRoFHadepWBndKs,7434
|
6
|
+
foamlib/_cases/_run.py,sha256=aD9JNv7YAs5GJGWXlYQAhr_-QT-46CUxecCPyrCmFA0,15631
|
7
|
+
foamlib/_cases/_slurm.py,sha256=2nimUWxHSkZdtmRROzcvnLW5urgmkNqxoTkCUmxALVE,2689
|
8
|
+
foamlib/_cases/_subprocess.py,sha256=VHV2SuOLqa711an6kCuvN6UlIkeh4qqFfdrpNoKzQps,5630
|
9
|
+
foamlib/_cases/_sync.py,sha256=yhrkwStKri7u41YImYCGBH4REcKn8Ar-32VW_WPa40c,9641
|
10
|
+
foamlib/_cases/_util.py,sha256=QCizfbuJdOCeF9ogU2R-y-iWX5kfaOA4U2W68t6QlOM,2544
|
11
|
+
foamlib/_files/__init__.py,sha256=q1vkjXnjnSZvo45jPAICpWeF2LZv5V6xfzAR6S8fS5A,96
|
12
|
+
foamlib/_files/_files.py,sha256=gSJQjvB1f7N2yJtCTx9kpivKqSSNjDj37qNMpned5CM,19505
|
13
|
+
foamlib/_files/_io.py,sha256=BGbbm6HKxL2ka0YMCmHqZQZ1R4PPQlkvWWb4FHMAS8k,2217
|
14
|
+
foamlib/_files/_parsing.py,sha256=IB6c9mc0AM_z_Gh7K82Z1ANseGaNwmzUo8AYv3pxEBw,14111
|
15
|
+
foamlib/_files/_serialization.py,sha256=QJ-F6BKizVe0gpjnpIfPxNGTqWwalY4PQtCKdDY9D70,5502
|
16
|
+
foamlib/_files/_types.py,sha256=PDhFW5hUzcoQsLx7M0Va1oaYV6km02jFgrvKJof0JKQ,3750
|
17
|
+
foamlib-0.8.9.dist-info/METADATA,sha256=zkGORMaEmCITWARJJOI-HxsAZGeXknnzKOMqA6ixa-c,14014
|
18
|
+
foamlib-0.8.9.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
19
|
+
foamlib-0.8.9.dist-info/licenses/LICENSE.txt,sha256=5Dte9TUnLZzPRs4NQzl-Jc2-Ljd-t_v0ZR5Ng5r0UsY,35131
|
20
|
+
foamlib-0.8.9.dist-info/RECORD,,
|
foamlib-0.8.8.dist-info/RECORD
DELETED
@@ -1,20 +0,0 @@
|
|
1
|
-
foamlib/__init__.py,sha256=ef8IIQjKj7fPW-7t7Oeo-bUD9Wy3bJY5Q63lobifP7o,452
|
2
|
-
foamlib/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
3
|
-
foamlib/_cases/__init__.py,sha256=_A1TTHuQfS9FH2_33lSEyLtOJZGFHZBco1tWJCVOHks,358
|
4
|
-
foamlib/_cases/_async.py,sha256=vXpq5T2gDomuawARsLaUEMjUBhMsZK58q7iVwQpXsYE,7903
|
5
|
-
foamlib/_cases/_base.py,sha256=37oBbM3NM-hpG7dKewZvyJNtqSAogMurcbmX-wLIgMU,6727
|
6
|
-
foamlib/_cases/_run.py,sha256=L2q9wab_pIbDpqC_N59ZpatgvjuDzQAWllQMyOHb1Hk,15475
|
7
|
-
foamlib/_cases/_slurm.py,sha256=Hzpf5Ugahwq7sUsCfhZ6JgXDOdkoYGGsHGiO7NV8uFs,2331
|
8
|
-
foamlib/_cases/_subprocess.py,sha256=VHV2SuOLqa711an6kCuvN6UlIkeh4qqFfdrpNoKzQps,5630
|
9
|
-
foamlib/_cases/_sync.py,sha256=e06aGGZ9n6WTWK9eWZhrezsT4yebGhKPE0sn0nwQH8A,5977
|
10
|
-
foamlib/_cases/_util.py,sha256=QCizfbuJdOCeF9ogU2R-y-iWX5kfaOA4U2W68t6QlOM,2544
|
11
|
-
foamlib/_files/__init__.py,sha256=q1vkjXnjnSZvo45jPAICpWeF2LZv5V6xfzAR6S8fS5A,96
|
12
|
-
foamlib/_files/_files.py,sha256=YAKw3RHEw8eCja4JAxobp6BC-2Kyy7AvG1O7yOqyMic,15414
|
13
|
-
foamlib/_files/_io.py,sha256=BGbbm6HKxL2ka0YMCmHqZQZ1R4PPQlkvWWb4FHMAS8k,2217
|
14
|
-
foamlib/_files/_parsing.py,sha256=IB6c9mc0AM_z_Gh7K82Z1ANseGaNwmzUo8AYv3pxEBw,14111
|
15
|
-
foamlib/_files/_serialization.py,sha256=PvMzNyTja6OKT_GUfExTulx9nMLBjfu0-9yThCKbvxs,5227
|
16
|
-
foamlib/_files/_types.py,sha256=m-fFjJnS4sFSavDsijlXpAfEhnbh10RBumSHAT0GOgQ,3408
|
17
|
-
foamlib-0.8.8.dist-info/METADATA,sha256=DdEh3U5bZ9VZmxA-3-0GlMgl_XNlPhwRDdE_K41bvT4,7996
|
18
|
-
foamlib-0.8.8.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
19
|
-
foamlib-0.8.8.dist-info/licenses/LICENSE.txt,sha256=5Dte9TUnLZzPRs4NQzl-Jc2-Ljd-t_v0ZR5Ng5r0UsY,35131
|
20
|
-
foamlib-0.8.8.dist-info/RECORD,,
|
File without changes
|
File without changes
|