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 CHANGED
@@ -1,6 +1,6 @@
1
1
  """A Python interface for interacting with OpenFOAM."""
2
2
 
3
- __version__ = "0.9.5"
3
+ __version__ = "0.9.7"
4
4
 
5
5
  from ._cases import (
6
6
  AsyncFoamCase,
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. `case[0]` or `case[-1]`.
48
- These will return `AsyncFoamCase.TimeDirectory` objects.
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 `AsyncFoamCase` concurrently.
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 `clean` or `Allclean` script is present in the case directory, it will be invoked.
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 `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.
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
- `clean` script.
161
+ ``clean`` script.
157
162
 
158
- :param check: If True, raise a `CalledProcessError` if the clean script returns 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 `cmd` is given, this method will run the given command in the context of the case.
199
+ If ``cmd`` is given, this method will run the given command in the context of the case.
195
200
 
196
- If `cmd` is `None`, a series of heuristic rules will be used to run the case. This works as
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 `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()`.
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
- `run`, `Allrun`, `Allrun-parallel` or `Allrun.pre` script.
223
+ ``run``, ``Allrun``, ``Allrun-parallel`` or ``Allrun.pre`` script.
214
224
 
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
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 `None`, autodetect from to the case.
220
- :param check: If `True`, raise a `CalledProcessError` if any command returns a non-zero
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 `True`, log the command output to `log.*` files in the case directory.
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
- `FoamCase` or `AsyncFoamCase` instead of this class.
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. `case[0]` or `case[-1]`.
29
- These will return `FoamCaseBase.TimeDirectory` objects.
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. `time["U"]`). These will be
43
- returned as `FoamFieldFile` objects.
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 `FoamFieldFile` objects (e.g. it can be
46
- iterated over with `for field in time: ...`).
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 `FoamCase` and `AsyncFoamCase`.
50
+ Abstract base class of :class:`FoamCase` and :class:`AsyncFoamCase`.
51
51
 
52
- Do not use this class directly: use `FoamCase` or `AsyncFoamCase` instead.
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
- `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.
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, 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 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.
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. `case[0]` or `case[-1]`.
36
- These will return `FoamCase.TimeDirectory` objects.
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 `clean` or `Allclean` script is present in the case directory, it will be invoked.
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 `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.
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
- `clean` script.
142
+ ``clean`` script.
139
143
 
140
- :param check: If True, raise a `CalledProcessError` if the clean script returns 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 `cmd` is given, this method will run the given command in the context of the case.
166
+ If ``cmd`` is given, this method will run the given command in the context of the case.
163
167
 
164
- If `cmd` is `None`, a series of heuristic rules will be used to run the case. This works as
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 `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()`.
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
- `run`, `Allrun`, `Allrun-parallel` or `Allrun.pre` script.
190
+ ``run``, ``Allrun``, ``Allrun-parallel`` or ``Allrun.pre`` script.
182
191
 
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
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 `None`, autodetect from to the case.
188
- :param check: If `True`, raise a `CalledProcessError` if any command returns a non-zero
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 `True`, log the command output to `log.*` files in the case directory.
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 `with` block) the copy will be deleted
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 `None`, clone to `$FOAM_RUN/foamlib`.
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 `self.copy().clean()`, but it can be more efficient in cases
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 `with` block) the cloned copy will be deleted
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 `None`, clone to `$FOAM_RUN/foamlib`.
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
- `FoamFile` supports most OpenFOAM data and configuration files (i.e., files with a
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 `FoamFile` as a mutable mapping (i.e., like a `dict`) to access and modify
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
- `FoamFile.SubDict` object, that allows for further access and modification of nested
85
- dictionaries within the `FoamFile` in a single operation.
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 `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]`).
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 `FoamFile` as a context manager (i.e., within a `with` block)
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 `FileNotFoundError` will be raised.
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
- `FoamFile.SubDict` is a mutable mapping that allows for accessing and modifying
127
- nested dictionaries within a `FoamFile` in a single operation. It behaves like a
128
- `dict` and can be used to access and modify entries in the sub-dictionary.
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 `FoamFile.SubDict` object, access a sub-dictionary in a `FoamFile`
131
- object (e.g., `file["subDict"]`).
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 `self["FoamFile", "version"]`."""
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 `self["FoamFile", "format"]`."""
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 `self["FoamFile", "class"]`."""
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 `self["FoamFile", "location"]`."""
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 `self["FoamFile", "object"]`."""
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 not keywords:
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, keywords: str | tuple[str, ...] | None, data: DataLike | SubDictLike
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 not keywords:
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 not keywords:
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 not keywords:
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 `True`, a header will be included if it is not already present in the
535
+ If ``True``, a header will be included if it is not already present in the
504
536
  input object.
505
537
  """
506
- header: SubDict | None
538
+ header: SubDictLike | None
507
539
  if isinstance(file, Mapping):
508
- header = file.get("FoamFile", None) # type: ignore [assignment]
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((k, v), keywords=(), header=header, tuple_is_entry=True) # type: ignore [arg-type]
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 `FoamFile` for representing OpenFOAM field files specifically.
590
+ Subclass of :class:`FoamFile` for representing OpenFOAM field files specifically.
551
591
 
552
- The difference between `FoamFieldFile` and `FoamFile` is that `FoamFieldFile` has
553
- the additional properties `dimensions`, `internal_field`, and `boundary_field` that
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 `FoamFile` for more information on how to read and edit OpenFOAM files.
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 `FileNotFoundError` will be raised.
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 `self["type"]`."""
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 `self["value"]`."""
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 not keywords:
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 `self["dimensions"]`."""
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 `self["internalField"]`."""
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 `self["boundaryField"]`."""
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)
@@ -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
- | _binary_numeric_list(dtype=np.int64)
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
- def loads(s: bytes | str) -> File | Data:
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
- file = _FILE.parse_string(s, parse_all=True).as_dict()
487
+ if keywords == ():
488
+ data = _FILE.parse_string(s, parse_all=True).as_dict()
415
489
 
416
- if len(file) == 1 and None in file:
417
- return file[None] # type: ignore[no-any-return]
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 file
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[tuple[str, ...], tuple[int, Data | EllipsisType, int]]:
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__(self, keywords: tuple[str, ...]) -> Data | EllipsisType:
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)
@@ -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, misc]
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, misc]
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, misc]
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
- tuple_is_entry: bool = False,
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
- tuple_is_entry=True,
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 tuple_is_entry:
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 b"(" + b" ".join(dumps(v, tuple_is_entry=True) for v in data) + b")" # type: ignore [arg-type]
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", Tuple["DataEntry", Union["DataEntry", "SubDict"]]]],
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
- Tuple["DataEntryLike", Union["DataEntryLike", "SubDictLike"]],
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
- "np.ndarray[tuple[int], np.dtype[np.float64 | np.float32]]",
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
- File = Dict[Optional[str], Union[StandaloneData, Data, "SubDict"]]
263
- FileLike = Mapping[Optional[str], Union[StandaloneDataLike, DataLike, "FileLike"]]
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.5
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
- ## ▶️ A complete example
199
+ ## 📘 Documentation
200
200
 
201
- The following is a fully self-contained example that demonstrates how to create an OpenFOAM case from scratch, run it, and analyze the results.
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,,
@@ -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,,