foamlib 0.9.5__py3-none-any.whl → 0.9.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- foamlib/__init__.py +1 -1
- foamlib/_cases/_async.py +43 -31
- foamlib/_cases/_base.py +8 -8
- foamlib/_cases/_run.py +2 -2
- foamlib/_cases/_slurm.py +9 -9
- foamlib/_cases/_sync.py +46 -35
- foamlib/_files/_files.py +97 -44
- foamlib/_files/_parsing.py +97 -16
- foamlib/_files/_serialization.py +14 -11
- foamlib/_files/_types.py +17 -10
- {foamlib-0.9.5.dist-info → foamlib-0.9.7.dist-info}/METADATA +3 -142
- foamlib-0.9.7.dist-info/RECORD +20 -0
- foamlib-0.9.5.dist-info/RECORD +0 -20
- {foamlib-0.9.5.dist-info → foamlib-0.9.7.dist-info}/WHEEL +0 -0
- {foamlib-0.9.5.dist-info → foamlib-0.9.7.dist-info}/licenses/LICENSE.txt +0 -0
foamlib/__init__.py
CHANGED
foamlib/_cases/_async.py
CHANGED
@@ -44,13 +44,14 @@ class AsyncFoamCase(FoamCaseRunBase):
|
|
44
44
|
|
45
45
|
Provides methods for running and cleaning cases, as well as accessing files.
|
46
46
|
|
47
|
-
Access the time directories of the case as a sequence, e.g.
|
48
|
-
These will return
|
47
|
+
Access the time directories of the case as a sequence, e.g. ``case[0]`` or ``case[-1]``.
|
48
|
+
These will return :class:`AsyncFoamCase.TimeDirectory` objects.
|
49
49
|
|
50
50
|
:param path: The path to the case directory. Defaults to the current working
|
51
51
|
directory.
|
52
52
|
|
53
53
|
Example usage: ::
|
54
|
+
|
54
55
|
from foamlib import AsyncFoamCase
|
55
56
|
|
56
57
|
case = AsyncFoamCase("path/to/case") # Load an OpenFOAM case
|
@@ -82,7 +83,7 @@ class AsyncFoamCase(FoamCaseRunBase):
|
|
82
83
|
|
83
84
|
max_cpus = multiprocessing.cpu_count()
|
84
85
|
"""
|
85
|
-
Maximum number of CPUs to use for running instances of
|
86
|
+
Maximum number of CPUs to use for running instances of :class:`AsyncFoamCase` concurrently.
|
86
87
|
|
87
88
|
Defaults to the number of CPUs on the system.
|
88
89
|
"""
|
@@ -143,19 +144,23 @@ class AsyncFoamCase(FoamCaseRunBase):
|
|
143
144
|
"""
|
144
145
|
Clean this case.
|
145
146
|
|
146
|
-
If a
|
147
|
+
If a ``clean`` or ``Allclean`` script is present in the case directory, it will be invoked.
|
147
148
|
Otherwise, the case directory will be cleaned using these rules:
|
148
149
|
|
149
|
-
- All time directories except
|
150
|
-
|
151
|
-
-
|
152
|
-
|
153
|
-
-
|
150
|
+
- All time directories except ``0`` will be deleted.
|
151
|
+
|
152
|
+
- The ``0`` time directory will be deleted if ``0.orig`` exists.
|
153
|
+
|
154
|
+
- ``processor*`` directories will be deleted if a ``system/decomposeParDict`` file is present.
|
155
|
+
|
156
|
+
- ``constant/polyMesh`` will be deleted if a ``system/blockMeshDict`` file is present.
|
157
|
+
|
158
|
+
- All ``log.*`` files will be deleted.
|
154
159
|
|
155
160
|
If this behavior is not appropriate for a case, it is recommended to write a custom
|
156
|
-
|
161
|
+
``clean`` script.
|
157
162
|
|
158
|
-
:param check: If True, raise a
|
163
|
+
:param check: If True, raise a :class:`CalledProcessError` if the clean script returns a
|
159
164
|
non-zero exit code.
|
160
165
|
"""
|
161
166
|
for coro in self._clean_calls(check=check):
|
@@ -191,35 +196,40 @@ class AsyncFoamCase(FoamCaseRunBase):
|
|
191
196
|
"""
|
192
197
|
Run this case, or a specified command in the context of this case.
|
193
198
|
|
194
|
-
If
|
199
|
+
If ``cmd`` is given, this method will run the given command in the context of the case.
|
195
200
|
|
196
|
-
If
|
201
|
+
If ``cmd`` is ``None``, a series of heuristic rules will be used to run the case. This works as
|
197
202
|
follows:
|
198
203
|
|
199
|
-
- If a
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
-
|
205
|
-
|
206
|
-
-
|
207
|
-
|
208
|
-
|
209
|
-
call
|
204
|
+
- If a ``run``, ``Allrun`` or ``Allrun-parallel`` script is present in the case directory,
|
205
|
+
it will be invoked. If both ``run`` and ``Allrun`` are present, ``Allrun`` will be used. If
|
206
|
+
both ``Allrun`` and ``Allrun-parallel`` are present and ``parallel`` is ``None``, an error will
|
207
|
+
be raised.
|
208
|
+
|
209
|
+
- If no run script is present but an ``Allrun.pre`` script exists, it will be invoked.
|
210
|
+
|
211
|
+
- Otherwise, if a ``system/blockMeshDict`` file is present, the method will call
|
212
|
+
:meth:`block_mesh()`.
|
213
|
+
|
214
|
+
- Then, if a ``0.orig`` directory is present, it will call :meth:`restore_0_dir()`.
|
215
|
+
|
216
|
+
- Then, if the case is to be run in parallel (see the ``parallel`` option) and no
|
217
|
+
``processor*`` directories exist but a ``system/decomposeParDict`` file is present, it will
|
218
|
+
call :meth:`decompose_par()`.
|
219
|
+
|
210
220
|
- Then, it will run the case using the application specified in the `controlDict` file.
|
211
221
|
|
212
222
|
If this behavior is not appropriate for a case, it is recommended to write a custom
|
213
|
-
|
223
|
+
``run``, ``Allrun``, ``Allrun-parallel`` or ``Allrun.pre`` script.
|
214
224
|
|
215
|
-
:param cmd: The command to run. If
|
216
|
-
is the command and the rest are arguments. If a string,
|
217
|
-
:param parallel: If
|
225
|
+
:param cmd: The command to run. If ``None``, run the case. If a sequence, the first element
|
226
|
+
is the command and the rest are arguments. If a string, ``cmd`` is executed in a shell.
|
227
|
+
:param parallel: If ``True``, run in parallel using MPI. If None, autodetect whether to run
|
218
228
|
in parallel.
|
219
|
-
:param cpus: The number of CPUs to use. If
|
220
|
-
:param check: If
|
229
|
+
:param cpus: The number of CPUs to use. If ``None``, autodetect from to the case.
|
230
|
+
:param check: If ``True``, raise a :class:`CalledProcessError` if any command returns a non-zero
|
221
231
|
exit code.
|
222
|
-
:param log: If
|
232
|
+
:param log: If ``True``, log the command output to ``log.*`` files in the case directory.
|
223
233
|
"""
|
224
234
|
for coro in self._run_calls(
|
225
235
|
cmd=cmd, parallel=parallel, cpus=cpus, check=check, log=log
|
@@ -262,6 +272,7 @@ class AsyncFoamCase(FoamCaseRunBase):
|
|
262
272
|
:return: The copy of the case.
|
263
273
|
|
264
274
|
Example usage: ::
|
275
|
+
|
265
276
|
import os
|
266
277
|
from pathlib import Path
|
267
278
|
from foamlib import AsyncFoamCase
|
@@ -298,6 +309,7 @@ class AsyncFoamCase(FoamCaseRunBase):
|
|
298
309
|
:return: The clone of the case.
|
299
310
|
|
300
311
|
Example usage: ::
|
312
|
+
|
301
313
|
import os
|
302
314
|
from pathlib import Path
|
303
315
|
from foamlib import AsyncFoamCase
|
foamlib/_cases/_base.py
CHANGED
@@ -23,10 +23,10 @@ class FoamCaseBase(Sequence["FoamCaseBase.TimeDirectory"]):
|
|
23
23
|
|
24
24
|
Provides methods for accessing files and time directories in the case, but does not
|
25
25
|
provide methods for running the case or any commands. Users are encouraged to use
|
26
|
-
|
26
|
+
:class:`FoamCase` or :class:`AsyncFoamCase` instead of this class.
|
27
27
|
|
28
|
-
Access the time directories of the case as a sequence, e.g.
|
29
|
-
These will return
|
28
|
+
Access the time directories of the case as a sequence, e.g. ``case[0]`` or ``case[-1]``.
|
29
|
+
These will return class:`FoamCaseBase.TimeDirectory` objects.
|
30
30
|
|
31
31
|
:param path: The path to the case directory. Defaults to the current working
|
32
32
|
directory.
|
@@ -39,11 +39,11 @@ class FoamCaseBase(Sequence["FoamCaseBase.TimeDirectory"]):
|
|
39
39
|
"""
|
40
40
|
A time directory in an OpenFOAM case.
|
41
41
|
|
42
|
-
Use to access field files in the directory (e.g.
|
43
|
-
returned as
|
42
|
+
Use to access field files in the directory (e.g. ``time["U"]``). These will be
|
43
|
+
returned as :class:`FoamFieldFile` objects.
|
44
44
|
|
45
|
-
It also behaves as a set of
|
46
|
-
iterated over with
|
45
|
+
It also behaves as a set of :class:`FoamFieldFile` objects (e.g. it can be
|
46
|
+
iterated over with ``for field in time: ...``).
|
47
47
|
"""
|
48
48
|
|
49
49
|
def __init__(self, path: os.PathLike[str] | str) -> None:
|
@@ -154,7 +154,7 @@ class FoamCaseBase(Sequence["FoamCaseBase.TimeDirectory"]):
|
|
154
154
|
|
155
155
|
@property
|
156
156
|
def _nsubdomains(self) -> int | None:
|
157
|
-
"""Return the number of subdomains as set in the decomposeParDict, or None if no decomposeParDict is found."""
|
157
|
+
"""Return the number of subdomains as set in the decomposeParDict, or ``None`` if no decomposeParDict is found."""
|
158
158
|
try:
|
159
159
|
nsubdomains = self.decompose_par_dict["numberOfSubdomains"]
|
160
160
|
if not isinstance(nsubdomains, int):
|
foamlib/_cases/_run.py
CHANGED
@@ -47,9 +47,9 @@ if TYPE_CHECKING:
|
|
47
47
|
|
48
48
|
class FoamCaseRunBase(FoamCaseBase):
|
49
49
|
"""
|
50
|
-
Abstract base class of
|
50
|
+
Abstract base class of :class:`FoamCase` and :class:`AsyncFoamCase`.
|
51
51
|
|
52
|
-
Do not use this class directly: use
|
52
|
+
Do not use this class directly: use :class:`FoamCase` or :class:`AsyncFoamCase` instead.
|
53
53
|
"""
|
54
54
|
|
55
55
|
class TimeDirectory(FoamCaseBase.TimeDirectory):
|
foamlib/_cases/_slurm.py
CHANGED
@@ -20,9 +20,9 @@ class AsyncSlurmFoamCase(AsyncFoamCase):
|
|
20
20
|
"""
|
21
21
|
An asynchronous OpenFOAM case that launches jobs on a Slurm cluster.
|
22
22
|
|
23
|
-
|
24
|
-
as the latter, except that it will launch jobs on a Slurm cluster (using
|
25
|
-
|
23
|
+
:class:`AsyncSlurmFoamCase` is a subclass of :class:`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
26
|
|
27
27
|
:param path: The path to the case directory. Defaults to the current working
|
28
28
|
directory.
|
@@ -64,12 +64,12 @@ class AsyncSlurmFoamCase(AsyncFoamCase):
|
|
64
64
|
"""
|
65
65
|
Run this case, or a specified command in the context of this case.
|
66
66
|
|
67
|
-
:param cmd: The command to run. If None
|
68
|
-
:param parallel: If True
|
69
|
-
:param cpus: The number of CPUs to use. If None
|
70
|
-
:param check: If True
|
71
|
-
:param log: If True
|
72
|
-
:param fallback: If True
|
67
|
+
:param cmd: The command to run. If ``None``, run the case. If a sequence, the first element is the command and the rest are arguments. If a string, `cmd` is executed in a shell.
|
68
|
+
:param parallel: If ``True``, run in parallel using MPI. If ``None``, autodetect whether to run in parallel.
|
69
|
+
:param cpus: The number of CPUs to use. If ``None``, autodetect according to the case. If ``0``, run locally.
|
70
|
+
:param check: If ``True``, raise a :class:`CalledProcessError` if any command returns a non-zero exit code.
|
71
|
+
:param log: If ``True``, log the command output to a file.
|
72
|
+
:param fallback: If ``True``, fall back to running the command locally if Slurm is not available.
|
73
73
|
"""
|
74
74
|
for coro in self._run_calls(
|
75
75
|
cmd=cmd,
|
foamlib/_cases/_sync.py
CHANGED
@@ -32,8 +32,8 @@ class FoamCase(FoamCaseRunBase):
|
|
32
32
|
|
33
33
|
Provides methods for running and cleaning cases, as well as accessing files.
|
34
34
|
|
35
|
-
Access the time directories of the case as a sequence, e.g.
|
36
|
-
These will return
|
35
|
+
Access the time directories of the case as a sequence, e.g. ``case[0]`` or ``case[-1]``.
|
36
|
+
These will return :class:`FoamCase.TimeDirectory` objects.
|
37
37
|
|
38
38
|
:param path: The path to the case directory. Defaults to the current working
|
39
39
|
directory.
|
@@ -125,19 +125,23 @@ class FoamCase(FoamCaseRunBase):
|
|
125
125
|
"""
|
126
126
|
Clean this case.
|
127
127
|
|
128
|
-
If a
|
128
|
+
If a ``clean`` or ``Allclean`` script is present in the case directory, it will be invoked.
|
129
129
|
Otherwise, the case directory will be cleaned using these rules:
|
130
130
|
|
131
|
-
- All time directories except
|
132
|
-
|
133
|
-
-
|
134
|
-
|
135
|
-
-
|
131
|
+
- All time directories except ``0`` will be deleted.
|
132
|
+
|
133
|
+
- The ``0`` time directory will be deleted if ``0.orig`` exists.
|
134
|
+
|
135
|
+
- ``processor*`` directories will be deleted if a ``system/decomposeParDict`` file is present.
|
136
|
+
|
137
|
+
- ``constant/polyMesh`` will be deleted if a ``system/blockMeshDict`` file is present.
|
138
|
+
|
139
|
+
- All ``log.*`` files will be deleted.
|
136
140
|
|
137
141
|
If this behavior is not appropriate for a case, it is recommended to write a custom
|
138
|
-
|
142
|
+
``clean`` script.
|
139
143
|
|
140
|
-
:param check: If True, raise a
|
144
|
+
:param check: If True, raise a :class:`CalledProcessError` if the clean script returns a
|
141
145
|
non-zero exit code.
|
142
146
|
"""
|
143
147
|
for _ in self._clean_calls(check=check):
|
@@ -159,35 +163,40 @@ class FoamCase(FoamCaseRunBase):
|
|
159
163
|
"""
|
160
164
|
Run this case, or a specified command in the context of this case.
|
161
165
|
|
162
|
-
If
|
166
|
+
If ``cmd`` is given, this method will run the given command in the context of the case.
|
163
167
|
|
164
|
-
If
|
168
|
+
If ``cmd`` is ``None``, a series of heuristic rules will be used to run the case. This works as
|
165
169
|
follows:
|
166
170
|
|
167
|
-
- If a
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
-
|
173
|
-
|
174
|
-
-
|
175
|
-
|
176
|
-
|
177
|
-
call
|
171
|
+
- If a ``run``, ``Allrun`` or ``Allrun-parallel`` script is present in the case directory,
|
172
|
+
it will be invoked. If both ``run`` and ``Allrun`` are present, ``Allrun`` will be used. If
|
173
|
+
both ``Allrun`` and ``Allrun-parallel`` are present and :param:`parallel` is ``None``, an error will
|
174
|
+
be raised.
|
175
|
+
|
176
|
+
- If no run script is present but an ``Allrun.pre`` script exists, it will be invoked.
|
177
|
+
|
178
|
+
- Otherwise, if a ``system/blockMeshDict`` file is present, the method will call
|
179
|
+
:meth:`block_mesh()`.
|
180
|
+
|
181
|
+
- Then, if a ``0.orig`` directory is present, it will call :meth:`restore_0_dir()`.
|
182
|
+
|
183
|
+
- Then, if the case is to be run in parallel (see the :param:`parallel` option) and no
|
184
|
+
``processor*`` directories exist but a ``system/decomposeParDict`` file is present, it will
|
185
|
+
call :meth:`decompose_par()`.
|
186
|
+
|
178
187
|
- Then, it will run the case using the application specified in the `controlDict` file.
|
179
188
|
|
180
189
|
If this behavior is not appropriate for a case, it is recommended to write a custom
|
181
|
-
|
190
|
+
``run``, ``Allrun``, ``Allrun-parallel`` or ``Allrun.pre`` script.
|
182
191
|
|
183
|
-
:param cmd: The command to run. If
|
184
|
-
is the command and the rest are arguments. If a string,
|
185
|
-
:param parallel: If
|
192
|
+
:param cmd: The command to run. If ``None``, run the case. If a sequence, the first element
|
193
|
+
is the command and the rest are arguments. If a string, ``cmd`` is executed in a shell.
|
194
|
+
:param parallel: If ``True``, run in parallel using MPI. If None, autodetect whether to run
|
186
195
|
in parallel.
|
187
|
-
:param cpus: The number of CPUs to use. If
|
188
|
-
:param check: If
|
196
|
+
:param cpus: The number of CPUs to use. If ``None``, autodetect from to the case.
|
197
|
+
:param check: If ``True``, raise a :class:`CalledProcessError` if any command returns a non-zero
|
189
198
|
exit code.
|
190
|
-
:param log: If
|
199
|
+
:param log: If ``True``, log the command output to ``log.*`` files in the case directory.
|
191
200
|
"""
|
192
201
|
for _ in self._run_calls(
|
193
202
|
cmd=cmd, parallel=parallel, cpus=cpus, check=check, log=log
|
@@ -218,14 +227,15 @@ class FoamCase(FoamCaseRunBase):
|
|
218
227
|
"""
|
219
228
|
Make a copy of this case.
|
220
229
|
|
221
|
-
If used as a context manager (i.e., within a
|
230
|
+
If used as a context manager (i.e., within a ``with`` block) the copy will be deleted
|
222
231
|
automatically when exiting the block.
|
223
232
|
|
224
|
-
:param dst: The destination path. If
|
233
|
+
:param dst: The destination path. If ``None``, clone to ``$FOAM_RUN/foamlib``.
|
225
234
|
|
226
235
|
:return: The copy of the case.
|
227
236
|
|
228
237
|
Example usage: ::
|
238
|
+
|
229
239
|
import os
|
230
240
|
from pathlib import Path
|
231
241
|
from foamlib import FoamCase
|
@@ -245,17 +255,18 @@ class FoamCase(FoamCaseRunBase):
|
|
245
255
|
"""
|
246
256
|
Clone this case (make a clean copy).
|
247
257
|
|
248
|
-
This is equivalent to running
|
258
|
+
This is equivalent to running ``self.copy().clean()``, but it can be more efficient in cases
|
249
259
|
that do not contain custom clean scripts.
|
250
260
|
|
251
|
-
If used as a context manager (i.e., within a
|
261
|
+
If used as a context manager (i.e., within a ``with`` block) the cloned copy will be deleted
|
252
262
|
automatically when exiting the block.
|
253
263
|
|
254
|
-
:param dst: The destination path. If
|
264
|
+
:param dst: The destination path. If ``None``, clone to ``$FOAM_RUN/foamlib``.
|
255
265
|
|
256
266
|
:return: The clone of the case.
|
257
267
|
|
258
268
|
Example usage: ::
|
269
|
+
|
259
270
|
import os
|
260
271
|
from pathlib import Path
|
261
272
|
from foamlib import FoamCase
|
foamlib/_files/_files.py
CHANGED
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|
2
2
|
|
3
3
|
import sys
|
4
4
|
from copy import deepcopy
|
5
|
-
from typing import Any, Optional, Tuple, Union, cast
|
5
|
+
from typing import Any, Optional, Tuple, Union, cast, overload
|
6
6
|
|
7
7
|
if sys.version_info >= (3, 8):
|
8
8
|
from typing import Literal
|
@@ -65,37 +65,38 @@ def _tensor_kind_for_field(
|
|
65
65
|
class FoamFile(
|
66
66
|
MutableMapping[
|
67
67
|
Optional[Union[str, Tuple[str, ...]]],
|
68
|
-
Union[Data, MutableSubDict],
|
68
|
+
Union[Data, StandaloneData, MutableSubDict],
|
69
69
|
],
|
70
70
|
FoamFileIO,
|
71
71
|
):
|
72
72
|
"""
|
73
73
|
An OpenFOAM data file.
|
74
74
|
|
75
|
-
|
75
|
+
:class:`FoamFile` supports most OpenFOAM data and configuration files (i.e., files with a
|
76
76
|
"FoamFile" header), including those with regular expressions and #-based directives.
|
77
77
|
Notable exceptions are FoamFiles with #codeStreams and those multiple #-directives
|
78
78
|
with the same name, which are currently not supported. Non-FoamFile output files are
|
79
79
|
also not suppored by this class. Regular expressions and #-based directives can be
|
80
80
|
accessed and modified, but they are not evaluated or expanded by this library.
|
81
81
|
|
82
|
-
Use
|
82
|
+
Use :class:`FoamFile` as a mutable mapping (i.e., like a :class:`dict`) to access and modify
|
83
83
|
entries. When accessing a sub-dictionary, the returned value will be a
|
84
|
-
|
85
|
-
dictionaries within the
|
84
|
+
:class:`FoamFile.SubDict` object, that allows for further access and modification of nested
|
85
|
+
dictionaries within the :class:`FoamFile` in a single operation.
|
86
86
|
|
87
|
-
If the
|
88
|
-
and modified by passing
|
87
|
+
If the :class:`FoamFile` does not store a dictionary, the main stored value can be accessed
|
88
|
+
and modified by passing ``None`` as the key (e.g., ``file[None]``).
|
89
89
|
|
90
|
-
You can also use the
|
90
|
+
You can also use the :class:`FoamFile` as a context manager (i.e., within a ``with`` block)
|
91
91
|
to make multiple changes to the file while saving any and all changes only once at
|
92
92
|
the end.
|
93
93
|
|
94
94
|
:param path: The path to the file. If the file does not exist, it will be created
|
95
95
|
when the first change is made. However, if an attempt is made to access entries
|
96
|
-
in a non-existent file, a
|
96
|
+
in a non-existent file, a :class:`FileNotFoundError` will be raised.
|
97
97
|
|
98
98
|
Example usage: ::
|
99
|
+
|
99
100
|
from foamlib import FoamFile
|
100
101
|
|
101
102
|
file = FoamFile("path/to/case/system/controlDict") # Load a controlDict file
|
@@ -104,6 +105,7 @@ class FoamFile(
|
|
104
105
|
file["writeFormat"] = "binary" # Set the write format to binary
|
105
106
|
|
106
107
|
or (better): ::
|
108
|
+
|
107
109
|
from foamlib import FoamCase
|
108
110
|
|
109
111
|
case = FoamCase("path/to/case")
|
@@ -123,14 +125,15 @@ class FoamFile(
|
|
123
125
|
"""
|
124
126
|
An OpenFOAM sub-dictionary within a file.
|
125
127
|
|
126
|
-
|
127
|
-
nested dictionaries within a
|
128
|
-
|
128
|
+
:class:`FoamFile.SubDict` is a mutable mapping that allows for accessing and modifying
|
129
|
+
nested dictionaries within a :class:`FoamFile` in a single operation. It behaves like a
|
130
|
+
:class:`dict` and can be used to access and modify entries in the sub-dictionary.
|
129
131
|
|
130
|
-
To obtain a
|
131
|
-
object (e.g.,
|
132
|
+
To obtain a :class:`FoamFile.SubDict` object, access a sub-dictionary in a :class:`FoamFile`
|
133
|
+
object (e.g., ``file["subDict"]``).
|
132
134
|
|
133
135
|
Example usage: ::
|
136
|
+
|
134
137
|
from foamlib import FoamFile
|
135
138
|
|
136
139
|
file = FoamFile("path/to/case/system/fvSchemes") # Load an fvSchemes file
|
@@ -138,6 +141,7 @@ class FoamFile(
|
|
138
141
|
file["ddtSchemes"]["default"] = "Euler" # Set the default ddt scheme
|
139
142
|
|
140
143
|
or (better): ::
|
144
|
+
|
141
145
|
from foamlib import FoamCase
|
142
146
|
|
143
147
|
case = FoamCase("path/to/case")
|
@@ -152,7 +156,7 @@ class FoamFile(
|
|
152
156
|
self._keywords = _keywords
|
153
157
|
|
154
158
|
def __getitem__(self, keyword: str) -> Data | FoamFile.SubDict:
|
155
|
-
return self._file[(*self._keywords, keyword)]
|
159
|
+
return self._file[(*self._keywords, keyword)] # type: ignore [return-value]
|
156
160
|
|
157
161
|
def __setitem__(
|
158
162
|
self,
|
@@ -200,7 +204,7 @@ class FoamFile(
|
|
200
204
|
|
201
205
|
@property
|
202
206
|
def version(self) -> float:
|
203
|
-
"""Alias of
|
207
|
+
"""Alias of ``self["FoamFile"]["version"]``."""
|
204
208
|
ret = self["FoamFile", "version"]
|
205
209
|
if not isinstance(ret, (int, float)):
|
206
210
|
msg = "version is not a number"
|
@@ -213,7 +217,7 @@ class FoamFile(
|
|
213
217
|
|
214
218
|
@property
|
215
219
|
def format(self) -> Literal["ascii", "binary"]:
|
216
|
-
"""Alias of
|
220
|
+
"""Alias of ``self["FoamFile"]["format"]``."""
|
217
221
|
ret = self["FoamFile", "format"]
|
218
222
|
if not isinstance(ret, str):
|
219
223
|
msg = "format is not a string"
|
@@ -229,7 +233,7 @@ class FoamFile(
|
|
229
233
|
|
230
234
|
@property
|
231
235
|
def class_(self) -> str:
|
232
|
-
"""Alias of
|
236
|
+
"""Alias of ``self["FoamFile"]["class"]``."""
|
233
237
|
ret = self["FoamFile", "class"]
|
234
238
|
if not isinstance(ret, str):
|
235
239
|
msg = "class is not a string"
|
@@ -242,7 +246,7 @@ class FoamFile(
|
|
242
246
|
|
243
247
|
@property
|
244
248
|
def location(self) -> str:
|
245
|
-
"""Alias of
|
249
|
+
"""Alias of ``self["FoamFile"]["location"]``."""
|
246
250
|
ret = self["FoamFile", "location"]
|
247
251
|
if not isinstance(ret, str):
|
248
252
|
msg = "location is not a string"
|
@@ -255,7 +259,7 @@ class FoamFile(
|
|
255
259
|
|
256
260
|
@property
|
257
261
|
def object_(self) -> str:
|
258
|
-
"""Alias of
|
262
|
+
"""Alias of ``self["FoamFile"]["object"]``."""
|
259
263
|
ret = self["FoamFile", "object"]
|
260
264
|
if not isinstance(ret, str):
|
261
265
|
msg = "object is not a string"
|
@@ -266,10 +270,21 @@ class FoamFile(
|
|
266
270
|
def object_(self, value: str) -> None:
|
267
271
|
self["FoamFile", "object"] = value
|
268
272
|
|
273
|
+
@overload # type: ignore [override]
|
274
|
+
def __getitem__(self, keywords: None | tuple[()]) -> StandaloneData: ...
|
275
|
+
|
276
|
+
@overload
|
277
|
+
def __getitem__(self, keywords: str) -> Data | FoamFile.SubDict: ...
|
278
|
+
|
279
|
+
@overload
|
280
|
+
def __getitem__(
|
281
|
+
self, keywords: tuple[str, ...]
|
282
|
+
) -> Data | StandaloneData | FoamFile.SubDict: ...
|
283
|
+
|
269
284
|
def __getitem__(
|
270
285
|
self, keywords: str | tuple[str, ...] | None
|
271
|
-
) -> Data | FoamFile.SubDict:
|
272
|
-
if
|
286
|
+
) -> Data | StandaloneData | FoamFile.SubDict:
|
287
|
+
if keywords is None:
|
273
288
|
keywords = ()
|
274
289
|
elif not isinstance(keywords, tuple):
|
275
290
|
keywords = (keywords,)
|
@@ -284,10 +299,27 @@ class FoamFile(
|
|
284
299
|
return FoamFile.SubDict(self, keywords)
|
285
300
|
return deepcopy(value)
|
286
301
|
|
302
|
+
@overload # type: ignore [override]
|
303
|
+
def __setitem__(
|
304
|
+
self, keywords: None | tuple[()], data: StandaloneDataLike
|
305
|
+
) -> None: ...
|
306
|
+
|
307
|
+
@overload
|
308
|
+
def __setitem__(self, keywords: str, data: DataLike | SubDictLike) -> None: ...
|
309
|
+
|
310
|
+
@overload
|
287
311
|
def __setitem__(
|
288
|
-
self,
|
312
|
+
self,
|
313
|
+
keywords: tuple[str, ...],
|
314
|
+
data: DataLike | StandaloneDataLike | SubDictLike,
|
315
|
+
) -> None: ...
|
316
|
+
|
317
|
+
def __setitem__(
|
318
|
+
self,
|
319
|
+
keywords: str | tuple[str, ...] | None,
|
320
|
+
data: DataLike | StandaloneDataLike | SubDictLike,
|
289
321
|
) -> None:
|
290
|
-
if
|
322
|
+
if keywords is None:
|
291
323
|
keywords = ()
|
292
324
|
elif not isinstance(keywords, tuple):
|
293
325
|
keywords = (keywords,)
|
@@ -412,7 +444,7 @@ class FoamFile(
|
|
412
444
|
)
|
413
445
|
|
414
446
|
def __delitem__(self, keywords: str | tuple[str, ...] | None) -> None:
|
415
|
-
if
|
447
|
+
if keywords is None:
|
416
448
|
keywords = ()
|
417
449
|
elif not isinstance(keywords, tuple):
|
418
450
|
keywords = (keywords,)
|
@@ -429,7 +461,7 @@ class FoamFile(
|
|
429
461
|
yield from (k for k in self._iter() if k != "FoamFile")
|
430
462
|
|
431
463
|
def __contains__(self, keywords: object) -> bool:
|
432
|
-
if
|
464
|
+
if keywords is None:
|
433
465
|
keywords = ()
|
434
466
|
elif not isinstance(keywords, tuple):
|
435
467
|
keywords = (keywords,)
|
@@ -477,7 +509,7 @@ class FoamFile(
|
|
477
509
|
:param include_header: Whether to include the "FoamFile" header in the output.
|
478
510
|
If `True`, the header will be included if it is present in the input object.
|
479
511
|
"""
|
480
|
-
ret = loads(s)
|
512
|
+
ret = loads(s, keywords=())
|
481
513
|
|
482
514
|
if not include_header and isinstance(ret, Mapping) and "FoamFile" in ret:
|
483
515
|
del ret["FoamFile"]
|
@@ -500,18 +532,26 @@ class FoamFile(
|
|
500
532
|
:param file: The Python object to serialize. This can be a dictionary, list,
|
501
533
|
or any other object that can be serialized to the OpenFOAM format.
|
502
534
|
:param ensure_header: Whether to include the "FoamFile" header in the output.
|
503
|
-
If
|
535
|
+
If ``True``, a header will be included if it is not already present in the
|
504
536
|
input object.
|
505
537
|
"""
|
506
|
-
header:
|
538
|
+
header: SubDictLike | None
|
507
539
|
if isinstance(file, Mapping):
|
508
|
-
|
540
|
+
h = file.get("FoamFile", None)
|
541
|
+
assert h is None or isinstance(h, Mapping)
|
542
|
+
header = h
|
509
543
|
|
510
544
|
entries: list[bytes] = []
|
511
545
|
for k, v in file.items():
|
512
546
|
if k is not None:
|
547
|
+
v = cast("Union[Data, SubDict]", v)
|
513
548
|
entries.append(
|
514
|
-
dumps(
|
549
|
+
dumps(
|
550
|
+
(k, v),
|
551
|
+
keywords=(),
|
552
|
+
header=header,
|
553
|
+
tuple_is_keyword_entry=True,
|
554
|
+
)
|
515
555
|
)
|
516
556
|
else:
|
517
557
|
assert not isinstance(v, Mapping)
|
@@ -547,20 +587,21 @@ class FoamFile(
|
|
547
587
|
|
548
588
|
class FoamFieldFile(FoamFile):
|
549
589
|
"""
|
550
|
-
Subclass of
|
590
|
+
Subclass of :class:`FoamFile` for representing OpenFOAM field files specifically.
|
551
591
|
|
552
|
-
The difference between
|
553
|
-
the additional properties
|
592
|
+
The difference between :class:`FoamFieldFile` and :class:`FoamFile` is that :class:`FoamFieldFile` has
|
593
|
+
the additional properties :attr:`dimensions`, :attr:`internal_field`, and :attr:`boundary_field` that
|
554
594
|
are commonly found in OpenFOAM field files. Note that these are only a shorthand for
|
555
595
|
accessing the corresponding entries in the file.
|
556
596
|
|
557
|
-
See
|
597
|
+
See :class:`FoamFile` for more information on how to read and edit OpenFOAM files.
|
558
598
|
|
559
599
|
:param path: The path to the file. If the file does not exist, it will be created
|
560
600
|
when the first change is made. However, if an attempt is made to access entries
|
561
|
-
in a non-existent file, a
|
601
|
+
in a non-existent file, a :class:`FileNotFoundError` will be raised.
|
562
602
|
|
563
603
|
Example usage: ::
|
604
|
+
|
564
605
|
from foamlib import FoamFieldFile
|
565
606
|
|
566
607
|
field = FoamFieldFile("path/to/case/0/U") # Load a field
|
@@ -569,6 +610,7 @@ class FoamFieldFile(FoamFile):
|
|
569
610
|
field.internal_field = [0, 0, 0] # Set the internal field
|
570
611
|
|
571
612
|
or (better): ::
|
613
|
+
|
572
614
|
from foamlib import FoamCase
|
573
615
|
|
574
616
|
case = FoamCase("path/to/case")
|
@@ -591,7 +633,7 @@ class FoamFieldFile(FoamFile):
|
|
591
633
|
|
592
634
|
@property
|
593
635
|
def type(self) -> str:
|
594
|
-
"""Alias of
|
636
|
+
"""Alias of ``self["type"]``."""
|
595
637
|
ret = self["type"]
|
596
638
|
if not isinstance(ret, str):
|
597
639
|
msg = "type is not a string"
|
@@ -606,7 +648,7 @@ class FoamFieldFile(FoamFile):
|
|
606
648
|
def value(
|
607
649
|
self,
|
608
650
|
) -> Field:
|
609
|
-
"""Alias of
|
651
|
+
"""Alias of ``self["value"]``."""
|
610
652
|
return cast(
|
611
653
|
"Field",
|
612
654
|
self["value"],
|
@@ -623,10 +665,21 @@ class FoamFieldFile(FoamFile):
|
|
623
665
|
def value(self) -> None:
|
624
666
|
del self["value"]
|
625
667
|
|
668
|
+
@overload # type: ignore [override]
|
669
|
+
def __getitem__(self, keywords: None | tuple[()]) -> StandaloneData: ...
|
670
|
+
|
671
|
+
@overload
|
672
|
+
def __getitem__(self, keywords: str) -> Data | FoamFieldFile.SubDict: ...
|
673
|
+
|
674
|
+
@overload
|
675
|
+
def __getitem__(
|
676
|
+
self, keywords: tuple[str, ...]
|
677
|
+
) -> Data | StandaloneData | FoamFieldFile.SubDict: ...
|
678
|
+
|
626
679
|
def __getitem__(
|
627
680
|
self, keywords: str | tuple[str, ...] | None
|
628
|
-
) -> Data | FoamFile.SubDict:
|
629
|
-
if
|
681
|
+
) -> Data | StandaloneData | FoamFile.SubDict:
|
682
|
+
if keywords is None:
|
630
683
|
keywords = ()
|
631
684
|
elif not isinstance(keywords, tuple):
|
632
685
|
keywords = (keywords,)
|
@@ -641,7 +694,7 @@ class FoamFieldFile(FoamFile):
|
|
641
694
|
|
642
695
|
@property
|
643
696
|
def dimensions(self) -> DimensionSet | Sequence[float]:
|
644
|
-
"""Alias of
|
697
|
+
"""Alias of ``self["dimensions"]``."""
|
645
698
|
ret = self["dimensions"]
|
646
699
|
if not isinstance(ret, DimensionSet):
|
647
700
|
msg = "dimensions is not a DimensionSet"
|
@@ -656,7 +709,7 @@ class FoamFieldFile(FoamFile):
|
|
656
709
|
def internal_field(
|
657
710
|
self,
|
658
711
|
) -> Field:
|
659
|
-
"""Alias of
|
712
|
+
"""Alias of ``self["internalField"]``."""
|
660
713
|
return cast("Field", self["internalField"])
|
661
714
|
|
662
715
|
@internal_field.setter
|
@@ -668,7 +721,7 @@ class FoamFieldFile(FoamFile):
|
|
668
721
|
|
669
722
|
@property
|
670
723
|
def boundary_field(self) -> FoamFieldFile.BoundariesSubDict:
|
671
|
-
"""Alias of
|
724
|
+
"""Alias of ``self["boundaryField"]``."""
|
672
725
|
ret = self["boundaryField"]
|
673
726
|
if not isinstance(ret, FoamFieldFile.BoundariesSubDict):
|
674
727
|
assert not isinstance(ret, FoamFile.SubDict)
|
foamlib/_files/_parsing.py
CHANGED
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|
2
2
|
|
3
3
|
import re
|
4
4
|
import sys
|
5
|
-
from typing import TYPE_CHECKING, Tuple, Union, cast
|
5
|
+
from typing import TYPE_CHECKING, Tuple, Union, cast, overload
|
6
6
|
|
7
7
|
if sys.version_info >= (3, 9):
|
8
8
|
from collections.abc import Iterator, Mapping, MutableMapping, Sequence
|
@@ -38,7 +38,7 @@ from pyparsing import (
|
|
38
38
|
printables,
|
39
39
|
)
|
40
40
|
|
41
|
-
from ._types import Data, Dimensioned, DimensionSet, File
|
41
|
+
from ._types import Data, Dimensioned, DimensionSet, File, StandaloneData, SubDict
|
42
42
|
|
43
43
|
if TYPE_CHECKING:
|
44
44
|
from numpy.typing import DTypeLike
|
@@ -190,6 +190,60 @@ def _binary_numeric_list(
|
|
190
190
|
).add_parse_action(to_array)
|
191
191
|
|
192
192
|
|
193
|
+
def _ascii_face_list(*, ignore: Regex | None = None) -> ParserElement:
|
194
|
+
element_pattern = r"(?:-?\d+)"
|
195
|
+
spacing_pattern = (
|
196
|
+
rf"(?:(?:\s|{ignore.re.pattern})+)" if ignore is not None else r"(?:\s+)"
|
197
|
+
)
|
198
|
+
|
199
|
+
element_pattern = rf"(?:(?:3{spacing_pattern}?\((?:{element_pattern}{spacing_pattern}){{2}}{element_pattern}{spacing_pattern}?\))|(?:4{spacing_pattern}?\((?:{element_pattern}{spacing_pattern}){{3}}{element_pattern}{spacing_pattern}?\)))"
|
200
|
+
|
201
|
+
list_ = Forward()
|
202
|
+
|
203
|
+
def process_count(tks: ParseResults) -> None:
|
204
|
+
nonlocal list_
|
205
|
+
if not tks:
|
206
|
+
count = None
|
207
|
+
else:
|
208
|
+
(count,) = tks
|
209
|
+
assert isinstance(count, int)
|
210
|
+
|
211
|
+
if count is None:
|
212
|
+
list_pattern = rf"\({spacing_pattern}?(?:{element_pattern}{spacing_pattern})*{element_pattern}{spacing_pattern}?\)"
|
213
|
+
|
214
|
+
elif count == 0:
|
215
|
+
list_ <<= NoMatch()
|
216
|
+
return
|
217
|
+
|
218
|
+
else:
|
219
|
+
list_pattern = rf"\({spacing_pattern}?(?:{element_pattern}{spacing_pattern}){{{count - 1}}}{element_pattern}{spacing_pattern}?\)"
|
220
|
+
|
221
|
+
list_ <<= Regex(list_pattern).add_parse_action(to_face_list)
|
222
|
+
|
223
|
+
def to_face_list(
|
224
|
+
tks: ParseResults,
|
225
|
+
) -> list[list[np.ndarray[tuple[int], np.dtype[np.int64]]]]:
|
226
|
+
(s,) = tks
|
227
|
+
assert s.startswith("(")
|
228
|
+
assert s.endswith(")")
|
229
|
+
if ignore is not None:
|
230
|
+
s = re.sub(ignore.re, " ", s)
|
231
|
+
s = s.replace("(", " ").replace(")", " ")
|
232
|
+
|
233
|
+
raw = np.fromstring(s, sep=" ", dtype=int)
|
234
|
+
|
235
|
+
values: list[np.ndarray[tuple[int], np.dtype[np.int64]]] = []
|
236
|
+
i = 0
|
237
|
+
while i < raw.size:
|
238
|
+
assert raw[i] in (3, 4)
|
239
|
+
values.append(raw[i + 1 : i + raw[i] + 1]) # type: ignore[arg-type]
|
240
|
+
i += raw[i] + 1
|
241
|
+
|
242
|
+
return [values]
|
243
|
+
|
244
|
+
return Opt(common.integer).add_parse_action(process_count).suppress() + list_
|
245
|
+
|
246
|
+
|
193
247
|
def _list_of(entry: ParserElement) -> ParserElement:
|
194
248
|
return (
|
195
249
|
(
|
@@ -370,7 +424,7 @@ _KEYWORD_ENTRY = _keyword_entry_of(
|
|
370
424
|
directive=_DIRECTIVE,
|
371
425
|
data_entry=_DATA_ENTRY,
|
372
426
|
)
|
373
|
-
_DICT = _dict_of(_TOKEN, _DATA)
|
427
|
+
_DICT = _dict_of(_TOKEN, Opt(_DATA, default=""))
|
374
428
|
_LIST_ENTRY = _DICT | _KEYWORD_ENTRY | _DATA_ENTRY
|
375
429
|
_LIST = _list_of(_LIST_ENTRY)
|
376
430
|
_NUMBER = (
|
@@ -391,9 +445,14 @@ _DATA <<= _DATA_ENTRY[1, ...].set_parse_action(
|
|
391
445
|
|
392
446
|
_STANDALONE_DATA = (
|
393
447
|
_ascii_numeric_list(dtype=int, ignore=_COMMENT)
|
394
|
-
|
|
395
|
-
| _binary_numeric_list(dtype=np.int32)
|
448
|
+
| _ascii_face_list(ignore=_COMMENT)
|
396
449
|
| _ascii_numeric_list(dtype=float, nested=3, ignore=_COMMENT)
|
450
|
+
| (
|
451
|
+
_binary_numeric_list(dtype=np.int64) + Opt(_binary_numeric_list(dtype=np.int64))
|
452
|
+
).add_parse_action(lambda tks: tuple(tks) if len(tks) > 1 else tks[0])
|
453
|
+
| (
|
454
|
+
_binary_numeric_list(dtype=np.int32) + Opt(_binary_numeric_list(dtype=np.int32))
|
455
|
+
).add_parse_action(lambda tks: tuple(tks) if len(tks) > 1 else tks[0])
|
397
456
|
| _binary_numeric_list(dtype=np.float64, nested=3)
|
398
457
|
| _binary_numeric_list(dtype=np.float32, nested=3)
|
399
458
|
| _DATA
|
@@ -406,17 +465,35 @@ _FILE = (
|
|
406
465
|
.parse_with_tabs()
|
407
466
|
)
|
408
467
|
|
468
|
+
_DATA_OR_DICT = (_DATA | _DICT).ignore(_COMMENT).parse_with_tabs()
|
469
|
+
|
470
|
+
|
471
|
+
@overload
|
472
|
+
def loads(s: bytes | str, *, keywords: tuple[()]) -> File | StandaloneData: ...
|
409
473
|
|
410
|
-
|
474
|
+
|
475
|
+
@overload
|
476
|
+
def loads(
|
477
|
+
s: bytes | str, *, keywords: tuple[str, ...] | None = None
|
478
|
+
) -> File | StandaloneData | Data | SubDict: ...
|
479
|
+
|
480
|
+
|
481
|
+
def loads(
|
482
|
+
s: bytes | str, *, keywords: tuple[str, ...] | None = None
|
483
|
+
) -> File | StandaloneData | Data | SubDict:
|
411
484
|
if isinstance(s, bytes):
|
412
485
|
s = s.decode("latin-1")
|
413
486
|
|
414
|
-
|
487
|
+
if keywords == ():
|
488
|
+
data = _FILE.parse_string(s, parse_all=True).as_dict()
|
415
489
|
|
416
|
-
|
417
|
-
|
490
|
+
if len(data) == 1 and None in data:
|
491
|
+
data = data[None]
|
492
|
+
|
493
|
+
else:
|
494
|
+
data = _DATA_OR_DICT.parse_string(s, parse_all=True)[0]
|
418
495
|
|
419
|
-
return
|
496
|
+
return data
|
420
497
|
|
421
498
|
|
422
499
|
_LOCATED_KEYWORD_ENTRIES = Group(
|
@@ -441,11 +518,11 @@ _LOCATED_FILE = (
|
|
441
518
|
)
|
442
519
|
|
443
520
|
|
444
|
-
class Parsed(Mapping[Tuple[str, ...], Union[Data, EllipsisType]]):
|
521
|
+
class Parsed(Mapping[Tuple[str, ...], Union[Data, StandaloneData, EllipsisType]]):
|
445
522
|
def __init__(self, contents: bytes) -> None:
|
446
523
|
self._parsed: MutableMapping[
|
447
524
|
tuple[str, ...],
|
448
|
-
tuple[int, Data | EllipsisType, int],
|
525
|
+
tuple[int, Data | StandaloneData | EllipsisType, int],
|
449
526
|
] = {}
|
450
527
|
for parse_result in _LOCATED_FILE.parse_string(
|
451
528
|
contents.decode("latin-1"), parse_all=True
|
@@ -458,10 +535,12 @@ class Parsed(Mapping[Tuple[str, ...], Union[Data, EllipsisType]]):
|
|
458
535
|
@staticmethod
|
459
536
|
def _flatten_result(
|
460
537
|
parse_result: ParseResults, *, _keywords: tuple[str, ...] = ()
|
461
|
-
) -> Mapping[
|
538
|
+
) -> Mapping[
|
539
|
+
tuple[str, ...], tuple[int, Data | StandaloneData | EllipsisType, int]
|
540
|
+
]:
|
462
541
|
ret: MutableMapping[
|
463
542
|
tuple[str, ...],
|
464
|
-
tuple[int, Data | EllipsisType, int],
|
543
|
+
tuple[int, Data | StandaloneData | EllipsisType, int],
|
465
544
|
] = {}
|
466
545
|
start = parse_result.locn_start
|
467
546
|
assert isinstance(start, int)
|
@@ -487,14 +566,16 @@ class Parsed(Mapping[Tuple[str, ...], Union[Data, EllipsisType]]):
|
|
487
566
|
ret[(*_keywords, keyword)] = (start, d, end)
|
488
567
|
return ret
|
489
568
|
|
490
|
-
def __getitem__(
|
569
|
+
def __getitem__(
|
570
|
+
self, keywords: tuple[str, ...]
|
571
|
+
) -> Data | StandaloneData | EllipsisType:
|
491
572
|
_, data, _ = self._parsed[keywords]
|
492
573
|
return data
|
493
574
|
|
494
575
|
def put(
|
495
576
|
self,
|
496
577
|
keywords: tuple[str, ...],
|
497
|
-
data: Data | EllipsisType,
|
578
|
+
data: Data | StandaloneData | EllipsisType,
|
498
579
|
content: bytes,
|
499
580
|
) -> None:
|
500
581
|
start, end = self.entry_location(keywords, missing_ok=True)
|
foamlib/_files/_serialization.py
CHANGED
@@ -16,6 +16,7 @@ from ._types import (
|
|
16
16
|
DataLike,
|
17
17
|
Dimensioned,
|
18
18
|
DimensionSet,
|
19
|
+
KeywordEntryLike,
|
19
20
|
StandaloneData,
|
20
21
|
StandaloneDataLike,
|
21
22
|
SubDict,
|
@@ -87,7 +88,7 @@ def normalize_data(
|
|
87
88
|
if arr.ndim == 1 or (arr.ndim == 2 and arr.shape[1] in (3, 6, 9)):
|
88
89
|
return arr # type: ignore [return-value]
|
89
90
|
|
90
|
-
return [normalize_data(d) for d in data] # type: ignore [arg-type,
|
91
|
+
return [normalize_data(d) for d in data] # type: ignore [arg-type, return-value]
|
91
92
|
|
92
93
|
if isinstance(data, int):
|
93
94
|
return float(data)
|
@@ -114,7 +115,7 @@ def normalize_data(
|
|
114
115
|
assert not isinstance(k, Mapping)
|
115
116
|
return ( # type: ignore [return-value]
|
116
117
|
normalize_keyword(k), # type: ignore [arg-type]
|
117
|
-
normalize_data(v) if not isinstance(v, Mapping) else v, # type: ignore [arg-type
|
118
|
+
normalize_data(v) if not isinstance(v, Mapping) else v, # type: ignore [arg-type]
|
118
119
|
)
|
119
120
|
|
120
121
|
if (
|
@@ -122,13 +123,13 @@ def normalize_data(
|
|
122
123
|
and not isinstance(data, DimensionSet)
|
123
124
|
and not isinstance(data, tuple)
|
124
125
|
):
|
125
|
-
return [normalize_data(d) for d in data] # type: ignore [arg-type,
|
126
|
+
return [normalize_data(d) for d in data] # type: ignore [arg-type, return-value]
|
126
127
|
|
127
128
|
if isinstance(data, tuple) and not isinstance(data, DimensionSet):
|
128
|
-
return tuple(normalize_data(d) for d in data) # type: ignore [misc]
|
129
|
+
return tuple(normalize_data(d, keywords=keywords) for d in data) # type: ignore [misc]
|
129
130
|
|
130
131
|
if isinstance(data, str):
|
131
|
-
s = loads(data)
|
132
|
+
s = loads(data, keywords=keywords)
|
132
133
|
if isinstance(s, (str, tuple, bool)):
|
133
134
|
return s
|
134
135
|
|
@@ -152,11 +153,11 @@ def normalize_keyword(data: DataLike) -> Data:
|
|
152
153
|
|
153
154
|
|
154
155
|
def dumps(
|
155
|
-
data: DataLike | StandaloneDataLike | SubDictLike,
|
156
|
+
data: DataLike | StandaloneDataLike | KeywordEntryLike | SubDictLike,
|
156
157
|
*,
|
157
158
|
keywords: tuple[str, ...] | None = None,
|
158
159
|
header: SubDictLike | None = None,
|
159
|
-
|
160
|
+
tuple_is_keyword_entry: bool = False,
|
160
161
|
) -> bytes:
|
161
162
|
data = normalize_data(data, keywords=keywords) # type: ignore [arg-type, misc]
|
162
163
|
|
@@ -167,7 +168,7 @@ def dumps(
|
|
167
168
|
dumps(
|
168
169
|
(k, v),
|
169
170
|
keywords=keywords,
|
170
|
-
|
171
|
+
tuple_is_keyword_entry=True,
|
171
172
|
)
|
172
173
|
for k, v in data.items()
|
173
174
|
)
|
@@ -245,7 +246,7 @@ def dumps(
|
|
245
246
|
return dumps(data.dimensions) + b" " + dumps(data.value)
|
246
247
|
|
247
248
|
if isinstance(data, tuple):
|
248
|
-
if
|
249
|
+
if tuple_is_keyword_entry:
|
249
250
|
k, v = data
|
250
251
|
ret = b"\n" if isinstance(k, str) and k[0] == "#" else b""
|
251
252
|
ret += dumps(k)
|
@@ -263,10 +264,12 @@ def dumps(
|
|
263
264
|
ret += b";"
|
264
265
|
return ret
|
265
266
|
|
266
|
-
return b" ".join(dumps(v) for v in data)
|
267
|
+
return b" ".join(dumps(v, keywords=keywords, header=header) for v in data)
|
267
268
|
|
268
269
|
if is_sequence(data):
|
269
|
-
return
|
270
|
+
return (
|
271
|
+
b"(" + b" ".join(dumps(v, tuple_is_keyword_entry=True) for v in data) + b")"
|
272
|
+
)
|
270
273
|
|
271
274
|
if data is True:
|
272
275
|
return b"yes"
|
foamlib/_files/_types.py
CHANGED
@@ -198,6 +198,8 @@ FieldLike = Union[
|
|
198
198
|
Sequence[TensorLike],
|
199
199
|
]
|
200
200
|
|
201
|
+
KeywordEntry = Tuple["DataEntry", Union["DataEntry", "SubDict"]]
|
202
|
+
KeywordEntryLike = Tuple["DataEntryLike", Union["DataEntryLike", "SubDictLike"]]
|
201
203
|
|
202
204
|
DataEntry = Union[
|
203
205
|
str,
|
@@ -206,16 +208,15 @@ DataEntry = Union[
|
|
206
208
|
bool,
|
207
209
|
Dimensioned,
|
208
210
|
DimensionSet,
|
209
|
-
List[Union["DataEntry",
|
211
|
+
List[Union["DataEntry", KeywordEntry]],
|
210
212
|
Field,
|
211
213
|
]
|
212
|
-
|
213
214
|
DataEntryLike = Union[
|
214
215
|
DataEntry,
|
215
216
|
Sequence[
|
216
217
|
Union[
|
217
218
|
"DataEntryLike",
|
218
|
-
|
219
|
+
"KeywordEntryLike",
|
219
220
|
]
|
220
221
|
],
|
221
222
|
FieldLike,
|
@@ -225,7 +226,6 @@ Data = Union[
|
|
225
226
|
DataEntry,
|
226
227
|
Tuple[DataEntry, ...],
|
227
228
|
]
|
228
|
-
|
229
229
|
DataLike = Union[
|
230
230
|
DataEntryLike,
|
231
231
|
Tuple["DataEntryLike", ...],
|
@@ -234,13 +234,19 @@ DataLike = Union[
|
|
234
234
|
StandaloneData = Union[
|
235
235
|
Data,
|
236
236
|
"np.ndarray[tuple[int], np.dtype[np.int64 | np.int32]]",
|
237
|
-
"np.ndarray[tuple[int], np.dtype[np.float64 | np.float32]]",
|
237
|
+
"np.ndarray[tuple[int, int], np.dtype[np.float64 | np.float32]]",
|
238
|
+
List["np.ndarray[tuple[int], np.dtype[np.int64 | np.int32]]"],
|
239
|
+
Tuple[
|
240
|
+
"np.ndarray[tuple[int], np.dtype[np.int64 | np.int32]]",
|
241
|
+
"np.ndarray[tuple[int], np.dtype[np.int64 | np.int32]]",
|
242
|
+
],
|
238
243
|
]
|
239
|
-
|
240
244
|
StandaloneDataLike = Union[
|
245
|
+
StandaloneData,
|
241
246
|
DataLike,
|
242
|
-
"np.ndarray[tuple[int], np.dtype[np.int64 | np.int32]]",
|
243
|
-
|
247
|
+
Sequence["np.ndarray[tuple[int], np.dtype[np.int64 | np.int32]]"],
|
248
|
+
Sequence[Sequence[int]],
|
249
|
+
Tuple[Sequence[int], Sequence[int]],
|
244
250
|
]
|
245
251
|
|
246
252
|
|
@@ -259,5 +265,6 @@ SubDict = Dict[str, Union[Data, "SubDict"]]
|
|
259
265
|
SubDictLike = Mapping[str, Union[DataLike, "SubDictLike"]]
|
260
266
|
MutableSubDict = MutableMapping[str, Union[Data, "MutableSubDict"]]
|
261
267
|
|
262
|
-
|
263
|
-
|
268
|
+
|
269
|
+
File = Dict[Optional[str], Union[StandaloneData, Data, SubDict]]
|
270
|
+
FileLike = Mapping[Optional[str], Union[StandaloneDataLike, DataLike, SubDictLike]]
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: foamlib
|
3
|
-
Version: 0.9.
|
3
|
+
Version: 0.9.7
|
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
|
@@ -196,148 +196,9 @@ case = FoamCase(Path(__file__).parent)
|
|
196
196
|
case.run()
|
197
197
|
```
|
198
198
|
|
199
|
-
##
|
199
|
+
## 📘 Documentation
|
200
200
|
|
201
|
-
|
202
|
-
|
203
|
-
<details>
|
204
|
-
|
205
|
-
<summary>Example</summary>
|
206
|
-
|
207
|
-
```python
|
208
|
-
#!/usr/bin/env python3
|
209
|
-
"""Check the diffusion of a scalar field in a scalarTransportFoam case."""
|
210
|
-
|
211
|
-
import shutil
|
212
|
-
from pathlib import Path
|
213
|
-
|
214
|
-
import numpy as np
|
215
|
-
from scipy.special import erfc
|
216
|
-
from foamlib import FoamCase
|
217
|
-
|
218
|
-
path = Path(__file__).parent / "diffusionCheck"
|
219
|
-
shutil.rmtree(path, ignore_errors=True)
|
220
|
-
path.mkdir(parents=True)
|
221
|
-
(path / "system").mkdir()
|
222
|
-
(path / "constant").mkdir()
|
223
|
-
(path / "0").mkdir()
|
224
|
-
|
225
|
-
case = FoamCase(path)
|
226
|
-
|
227
|
-
with case.control_dict as f:
|
228
|
-
f["application"] = "scalarTransportFoam"
|
229
|
-
f["startFrom"] = "latestTime"
|
230
|
-
f["stopAt"] = "endTime"
|
231
|
-
f["endTime"] = 5
|
232
|
-
f["deltaT"] = 1e-3
|
233
|
-
f["writeControl"] = "adjustableRunTime"
|
234
|
-
f["writeInterval"] = 1
|
235
|
-
f["purgeWrite"] = 0
|
236
|
-
f["writeFormat"] = "ascii"
|
237
|
-
f["writePrecision"] = 6
|
238
|
-
f["writeCompression"] = False
|
239
|
-
f["timeFormat"] = "general"
|
240
|
-
f["timePrecision"] = 6
|
241
|
-
f["adjustTimeStep"] = False
|
242
|
-
f["runTimeModifiable"] = False
|
243
|
-
|
244
|
-
with case.fv_schemes as f:
|
245
|
-
f["ddtSchemes"] = {"default": "Euler"}
|
246
|
-
f["gradSchemes"] = {"default": "Gauss linear"}
|
247
|
-
f["divSchemes"] = {"default": "none", "div(phi,U)": "Gauss linear", "div(phi,T)": "Gauss linear"}
|
248
|
-
f["laplacianSchemes"] = {"default": "Gauss linear corrected"}
|
249
|
-
|
250
|
-
with case.fv_solution as f:
|
251
|
-
f["solvers"] = {"T": {"solver": "PBiCG", "preconditioner": "DILU", "tolerance": 1e-6, "relTol": 0}}
|
252
|
-
|
253
|
-
with case.block_mesh_dict as f:
|
254
|
-
f["scale"] = 1
|
255
|
-
f["vertices"] = [
|
256
|
-
[0, 0, 0],
|
257
|
-
[1, 0, 0],
|
258
|
-
[1, 0.5, 0],
|
259
|
-
[1, 1, 0],
|
260
|
-
[0, 1, 0],
|
261
|
-
[0, 0.5, 0],
|
262
|
-
[0, 0, 0.1],
|
263
|
-
[1, 0, 0.1],
|
264
|
-
[1, 0.5, 0.1],
|
265
|
-
[1, 1, 0.1],
|
266
|
-
[0, 1, 0.1],
|
267
|
-
[0, 0.5, 0.1],
|
268
|
-
]
|
269
|
-
f["blocks"] = [
|
270
|
-
"hex", [0, 1, 2, 5, 6, 7, 8, 11], [400, 20, 1], "simpleGrading", [1, 1, 1],
|
271
|
-
"hex", [5, 2, 3, 4, 11, 8, 9, 10], [400, 20, 1], "simpleGrading", [1, 1, 1],
|
272
|
-
]
|
273
|
-
f["edges"] = []
|
274
|
-
f["boundary"] = [
|
275
|
-
("inletUp", {"type": "patch", "faces": [[5, 4, 10, 11]]}),
|
276
|
-
("inletDown", {"type": "patch", "faces": [[0, 5, 11, 6]]}),
|
277
|
-
("outletUp", {"type": "patch", "faces": [[2, 3, 9, 8]]}),
|
278
|
-
("outletDown", {"type": "patch", "faces": [[1, 2, 8, 7]]}),
|
279
|
-
("walls", {"type": "wall", "faces": [[4, 3, 9, 10], [0, 1, 7, 6]]}),
|
280
|
-
("frontAndBack", {"type": "empty", "faces": [[0, 1, 2, 5], [5, 2, 3, 4], [6, 7, 8, 11], [11, 8, 9, 10]]}),
|
281
|
-
]
|
282
|
-
f["mergePatchPairs"] = []
|
283
|
-
|
284
|
-
with case.transport_properties as f:
|
285
|
-
f["DT"] = f.Dimensioned(1e-3, f.DimensionSet(length=2, time=-1), "DT")
|
286
|
-
|
287
|
-
with case[0]["U"] as f:
|
288
|
-
f.dimensions = f.DimensionSet(length=1, time=-1)
|
289
|
-
f.internal_field = [1, 0, 0]
|
290
|
-
f.boundary_field = {
|
291
|
-
"inletUp": {"type": "fixedValue", "value": [1, 0, 0]},
|
292
|
-
"inletDown": {"type": "fixedValue", "value": [1, 0, 0]},
|
293
|
-
"outletUp": {"type": "zeroGradient"},
|
294
|
-
"outletDown": {"type": "zeroGradient"},
|
295
|
-
"walls": {"type": "zeroGradient"},
|
296
|
-
"frontAndBack": {"type": "empty"},
|
297
|
-
}
|
298
|
-
|
299
|
-
with case[0]["T"] as f:
|
300
|
-
f.dimensions = f.DimensionSet(temperature=1)
|
301
|
-
f.internal_field = 0
|
302
|
-
f.boundary_field = {
|
303
|
-
"inletUp": {"type": "fixedValue", "value": 0},
|
304
|
-
"inletDown": {"type": "fixedValue", "value": 1},
|
305
|
-
"outletUp": {"type": "zeroGradient"},
|
306
|
-
"outletDown": {"type": "zeroGradient"},
|
307
|
-
"walls": {"type": "zeroGradient"},
|
308
|
-
"frontAndBack": {"type": "empty"},
|
309
|
-
}
|
310
|
-
|
311
|
-
case.run()
|
312
|
-
|
313
|
-
x, y, z = case[0].cell_centers().internal_field.T
|
314
|
-
|
315
|
-
end = x == x.max()
|
316
|
-
x = x[end]
|
317
|
-
y = y[end]
|
318
|
-
z = z[end]
|
319
|
-
|
320
|
-
DT = case.transport_properties["DT"].value
|
321
|
-
U = case[0]["U"].internal_field[0]
|
322
|
-
|
323
|
-
for time in case[1:]:
|
324
|
-
if U*time.time < 2*x.max():
|
325
|
-
continue
|
326
|
-
|
327
|
-
T = time["T"].internal_field[end]
|
328
|
-
analytical = 0.5 * erfc((y - 0.5) / np.sqrt(4 * DT * x/U))
|
329
|
-
if np.allclose(T, analytical, atol=0.1):
|
330
|
-
print(f"Time {time.time}: OK")
|
331
|
-
else:
|
332
|
-
raise RuntimeError(f"Time {time.time}: {T} != {analytical}")
|
333
|
-
```
|
334
|
-
|
335
|
-
</details>
|
336
|
-
|
337
|
-
|
338
|
-
## 📘 API documentation
|
339
|
-
|
340
|
-
For more information on how to use **foamlibs**'s classes and methods, check out the [documentation](https://foamlib.readthedocs.io/).
|
201
|
+
For details on how to use **foamlib**, check out the [documentation](https://foamlib.readthedocs.io/).
|
341
202
|
|
342
203
|
## 🙋 Support
|
343
204
|
|
@@ -0,0 +1,20 @@
|
|
1
|
+
foamlib/__init__.py,sha256=TlX6bgqC9lLrRtBY4Bany589hu5CarS_mYUD6XfnJHw,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=1NuBaKa7NC-320SFNYW7JWZ5rAi344br_SoEdl64dmo,11797
|
5
|
+
foamlib/_cases/_base.py,sha256=0Bb45FWxxMRnx6njtnJ3Tqh2_NcphrPtVSFjmfYbTjw,7480
|
6
|
+
foamlib/_cases/_run.py,sha256=C5sf-PWE73cqyPVmmWDiZU3V9QYVsrhSXpgil7aIp10,15659
|
7
|
+
foamlib/_cases/_slurm.py,sha256=X8eSL_tDnip3bPHb2Fot-n1yD0FfiVP5sCxHxjKt1f0,2748
|
8
|
+
foamlib/_cases/_subprocess.py,sha256=VHV2SuOLqa711an6kCuvN6UlIkeh4qqFfdrpNoKzQps,5630
|
9
|
+
foamlib/_cases/_sync.py,sha256=lsgJV2dMAAmmsiJMtzqy1bhW3yAZQOUMXh3h8jNqyes,9799
|
10
|
+
foamlib/_cases/_util.py,sha256=QCizfbuJdOCeF9ogU2R-y-iWX5kfaOA4U2W68t6QlOM,2544
|
11
|
+
foamlib/_files/__init__.py,sha256=q1vkjXnjnSZvo45jPAICpWeF2LZv5V6xfzAR6S8fS5A,96
|
12
|
+
foamlib/_files/_files.py,sha256=uMCn4kNdVJBbcEl7sTSDn9bpc6JUZtNUBbyio7oMqSg,24346
|
13
|
+
foamlib/_files/_io.py,sha256=BGbbm6HKxL2ka0YMCmHqZQZ1R4PPQlkvWWb4FHMAS8k,2217
|
14
|
+
foamlib/_files/_parsing.py,sha256=zLRXwv9PEil-vlIr1QiIEw8bhanRQ_vbVIEdTHv4bdI,20534
|
15
|
+
foamlib/_files/_serialization.py,sha256=kQfPfuTXtc9jryQdieCbAX0-8_Oz__vY_kr7uH9f_rU,8172
|
16
|
+
foamlib/_files/_types.py,sha256=7reA_TjRjCFV3waQVaGaYWURFoN8u92ao-NH9rESiAk,8202
|
17
|
+
foamlib-0.9.7.dist-info/METADATA,sha256=tI5zFm2gLiXI1mKn92fMk5kitpUdNWnoO2iv9cjNhFw,8701
|
18
|
+
foamlib-0.9.7.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
19
|
+
foamlib-0.9.7.dist-info/licenses/LICENSE.txt,sha256=5Dte9TUnLZzPRs4NQzl-Jc2-Ljd-t_v0ZR5Ng5r0UsY,35131
|
20
|
+
foamlib-0.9.7.dist-info/RECORD,,
|
foamlib-0.9.5.dist-info/RECORD
DELETED
@@ -1,20 +0,0 @@
|
|
1
|
-
foamlib/__init__.py,sha256=T6_0si2KRlok9sXXMi_yP97TelpSM0w0aivlsmziRxI,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=nbf9SPGH4v4-H4mxiMtiD8YuBJJdGC8RFlV_KLFM6bA,22676
|
13
|
-
foamlib/_files/_io.py,sha256=BGbbm6HKxL2ka0YMCmHqZQZ1R4PPQlkvWWb4FHMAS8k,2217
|
14
|
-
foamlib/_files/_parsing.py,sha256=r9F7QVfGhAFOXu1q3Otzo0gfdwZ4FxRNELuauJsw5-I,17718
|
15
|
-
foamlib/_files/_serialization.py,sha256=P7u2EUx0OwvfVYinp46CpzdjGYGXJVN0-xXXuOYogfA,8020
|
16
|
-
foamlib/_files/_types.py,sha256=eNVxuK_NDRqh0mrTcseuAD3lqn4VEBGVUYhXn-T1zEU,7884
|
17
|
-
foamlib-0.9.5.dist-info/METADATA,sha256=cauprjQ7VzXsUqi5HWl7B_qsVufNM0uB0Iw3h0QQgpA,12906
|
18
|
-
foamlib-0.9.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
19
|
-
foamlib-0.9.5.dist-info/licenses/LICENSE.txt,sha256=5Dte9TUnLZzPRs4NQzl-Jc2-Ljd-t_v0ZR5Ng5r0UsY,35131
|
20
|
-
foamlib-0.9.5.dist-info/RECORD,,
|
File without changes
|
File without changes
|