foamlib 0.3.11__tar.gz → 0.3.13__tar.gz
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-0.3.11 → foamlib-0.3.13}/PKG-INFO +2 -2
- {foamlib-0.3.11 → foamlib-0.3.13}/foamlib/__init__.py +1 -1
- foamlib-0.3.13/foamlib/_cases/__init__.py +9 -0
- foamlib-0.3.13/foamlib/_cases/_async.py +157 -0
- foamlib-0.3.13/foamlib/_cases/_base.py +354 -0
- foamlib-0.3.13/foamlib/_cases/_sync.py +121 -0
- {foamlib-0.3.11 → foamlib-0.3.13}/foamlib/_files/_parsing.py +1 -1
- {foamlib-0.3.11 → foamlib-0.3.13}/foamlib.egg-info/PKG-INFO +2 -2
- {foamlib-0.3.11 → foamlib-0.3.13}/foamlib.egg-info/SOURCES.txt +4 -1
- {foamlib-0.3.11 → foamlib-0.3.13}/foamlib.egg-info/requires.txt +1 -1
- {foamlib-0.3.11 → foamlib-0.3.13}/pyproject.toml +1 -1
- foamlib-0.3.11/foamlib/_cases.py +0 -630
- {foamlib-0.3.11 → foamlib-0.3.13}/LICENSE.txt +0 -0
- {foamlib-0.3.11 → foamlib-0.3.13}/README.md +0 -0
- {foamlib-0.3.11 → foamlib-0.3.13}/foamlib/_files/__init__.py +0 -0
- {foamlib-0.3.11 → foamlib-0.3.13}/foamlib/_files/_base.py +0 -0
- {foamlib-0.3.11 → foamlib-0.3.13}/foamlib/_files/_files.py +0 -0
- {foamlib-0.3.11 → foamlib-0.3.13}/foamlib/_files/_io.py +0 -0
- {foamlib-0.3.11 → foamlib-0.3.13}/foamlib/_files/_serialization.py +0 -0
- {foamlib-0.3.11 → foamlib-0.3.13}/foamlib/_util.py +0 -0
- {foamlib-0.3.11 → foamlib-0.3.13}/foamlib/py.typed +0 -0
- {foamlib-0.3.11 → foamlib-0.3.13}/foamlib.egg-info/dependency_links.txt +0 -0
- {foamlib-0.3.11 → foamlib-0.3.13}/foamlib.egg-info/top_level.txt +0 -0
- {foamlib-0.3.11 → foamlib-0.3.13}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: foamlib
|
3
|
-
Version: 0.3.
|
3
|
+
Version: 0.3.13
|
4
4
|
Summary: A Python interface for interacting with OpenFOAM
|
5
5
|
Author-email: "Gabriel S. Gerlero" <ggerlero@cimec.unl.edu.ar>
|
6
6
|
Project-URL: Homepage, https://github.com/gerlero/foamlib
|
@@ -42,7 +42,7 @@ Requires-Dist: foamlib[test]; extra == "typing"
|
|
42
42
|
Requires-Dist: mypy<2,>=1; extra == "typing"
|
43
43
|
Provides-Extra: docs
|
44
44
|
Requires-Dist: foamlib[numpy]; extra == "docs"
|
45
|
-
Requires-Dist: sphinx<
|
45
|
+
Requires-Dist: sphinx<9,>=7; extra == "docs"
|
46
46
|
Requires-Dist: sphinx_rtd_theme; extra == "docs"
|
47
47
|
Provides-Extra: dev
|
48
48
|
Requires-Dist: foamlib[numpy]; extra == "dev"
|
@@ -0,0 +1,157 @@
|
|
1
|
+
import asyncio
|
2
|
+
import multiprocessing
|
3
|
+
import sys
|
4
|
+
from contextlib import asynccontextmanager
|
5
|
+
from pathlib import Path
|
6
|
+
from typing import (
|
7
|
+
Optional,
|
8
|
+
Union,
|
9
|
+
)
|
10
|
+
|
11
|
+
if sys.version_info >= (3, 9):
|
12
|
+
from collections.abc import AsyncGenerator, Sequence
|
13
|
+
else:
|
14
|
+
from typing import AsyncGenerator, Sequence
|
15
|
+
|
16
|
+
import aioshutil
|
17
|
+
|
18
|
+
from .._util import run_process_async
|
19
|
+
from ._base import FoamCaseBase
|
20
|
+
|
21
|
+
|
22
|
+
class AsyncFoamCase(FoamCaseBase):
|
23
|
+
"""
|
24
|
+
An OpenFOAM case with asynchronous support.
|
25
|
+
|
26
|
+
Provides methods for running and cleaning cases, as well as accessing files.
|
27
|
+
|
28
|
+
Access the time directories of the case as a sequence, e.g. `case[0]` or `case[-1]`.
|
29
|
+
|
30
|
+
:param path: The path to the case directory.
|
31
|
+
"""
|
32
|
+
|
33
|
+
max_cpus = multiprocessing.cpu_count()
|
34
|
+
"""
|
35
|
+
Maximum number of CPUs to use for running `AsyncFoamCase`s concurrently. Defaults to the number of CPUs on the system.
|
36
|
+
"""
|
37
|
+
|
38
|
+
_reserved_cpus = 0
|
39
|
+
_cpus_cond = None # Cannot be initialized here yet
|
40
|
+
|
41
|
+
@staticmethod
|
42
|
+
@asynccontextmanager
|
43
|
+
async def _cpus(cpus: int) -> AsyncGenerator[None, None]:
|
44
|
+
if AsyncFoamCase._cpus_cond is None:
|
45
|
+
AsyncFoamCase._cpus_cond = asyncio.Condition()
|
46
|
+
|
47
|
+
cpus = min(cpus, AsyncFoamCase.max_cpus)
|
48
|
+
if cpus > 0:
|
49
|
+
async with AsyncFoamCase._cpus_cond:
|
50
|
+
await AsyncFoamCase._cpus_cond.wait_for(
|
51
|
+
lambda: AsyncFoamCase.max_cpus - AsyncFoamCase._reserved_cpus
|
52
|
+
>= cpus
|
53
|
+
)
|
54
|
+
AsyncFoamCase._reserved_cpus += cpus
|
55
|
+
try:
|
56
|
+
yield
|
57
|
+
finally:
|
58
|
+
if cpus > 0:
|
59
|
+
async with AsyncFoamCase._cpus_cond:
|
60
|
+
AsyncFoamCase._reserved_cpus -= cpus
|
61
|
+
AsyncFoamCase._cpus_cond.notify(cpus)
|
62
|
+
|
63
|
+
async def clean(
|
64
|
+
self,
|
65
|
+
*,
|
66
|
+
script: bool = True,
|
67
|
+
check: bool = False,
|
68
|
+
) -> None:
|
69
|
+
"""
|
70
|
+
Clean this case.
|
71
|
+
|
72
|
+
:param script: If True, use an (All)clean script if it exists. If False, ignore any clean scripts.
|
73
|
+
:param check: If True, raise a CalledProcessError if the clean script returns a non-zero exit code.
|
74
|
+
"""
|
75
|
+
script_path = self._clean_script() if script else None
|
76
|
+
|
77
|
+
if script_path is not None:
|
78
|
+
await self.run([script_path], check=check)
|
79
|
+
else:
|
80
|
+
for p in self._clean_paths():
|
81
|
+
if p.is_dir():
|
82
|
+
await aioshutil.rmtree(p) # type: ignore [call-arg]
|
83
|
+
else:
|
84
|
+
p.unlink()
|
85
|
+
|
86
|
+
async def _run(
|
87
|
+
self,
|
88
|
+
cmd: Union[Sequence[Union[str, Path]], str, Path],
|
89
|
+
*,
|
90
|
+
check: bool = True,
|
91
|
+
) -> None:
|
92
|
+
await run_process_async(cmd, cwd=self.path, check=check)
|
93
|
+
|
94
|
+
async def run(
|
95
|
+
self,
|
96
|
+
cmd: Optional[Union[Sequence[Union[str, Path]], str, Path]] = None,
|
97
|
+
*,
|
98
|
+
script: bool = True,
|
99
|
+
parallel: Optional[bool] = None,
|
100
|
+
cpus: Optional[int] = None,
|
101
|
+
check: bool = True,
|
102
|
+
) -> None:
|
103
|
+
for name, args, kwargs in self._run_cmds(
|
104
|
+
cmd=cmd, script=script, parallel=parallel, check=check
|
105
|
+
):
|
106
|
+
if cpus is None:
|
107
|
+
cpus = 1
|
108
|
+
|
109
|
+
if name == "run" and kwargs.get("parallel", False):
|
110
|
+
cpus = min(self._nprocessors, cpus)
|
111
|
+
|
112
|
+
async with self._cpus(cpus):
|
113
|
+
await getattr(self, name)(*args, **kwargs)
|
114
|
+
|
115
|
+
async def block_mesh(self, *, check: bool = True) -> None:
|
116
|
+
"""Run blockMesh on this case."""
|
117
|
+
await self.run(["blockMesh"], check=check)
|
118
|
+
|
119
|
+
async def decompose_par(self, *, check: bool = True) -> None:
|
120
|
+
"""Decompose this case for parallel running."""
|
121
|
+
await self.run(["decomposePar"], check=check)
|
122
|
+
|
123
|
+
async def reconstruct_par(self, *, check: bool = True) -> None:
|
124
|
+
"""Reconstruct this case after parallel running."""
|
125
|
+
await self.run(["reconstructPar"], check=check)
|
126
|
+
|
127
|
+
async def restore_0_dir(self) -> None:
|
128
|
+
"""Restore the 0 directory from the 0.orig directory."""
|
129
|
+
await aioshutil.rmtree(self.path / "0", ignore_errors=True) # type: ignore [call-arg]
|
130
|
+
await aioshutil.copytree(self.path / "0.orig", self.path / "0")
|
131
|
+
|
132
|
+
async def copy(self, dest: Union[Path, str]) -> "AsyncFoamCase":
|
133
|
+
"""
|
134
|
+
Make a copy of this case.
|
135
|
+
|
136
|
+
:param dest: The destination path.
|
137
|
+
"""
|
138
|
+
return AsyncFoamCase(await aioshutil.copytree(self.path, dest, symlinks=True))
|
139
|
+
|
140
|
+
async def clone(self, dest: Union[Path, str]) -> "AsyncFoamCase":
|
141
|
+
"""
|
142
|
+
Clone this case (make a clean copy).
|
143
|
+
|
144
|
+
:param dest: The destination path.
|
145
|
+
"""
|
146
|
+
if self._clean_script() is not None:
|
147
|
+
copy = await self.copy(dest)
|
148
|
+
await copy.clean()
|
149
|
+
return copy
|
150
|
+
|
151
|
+
dest = Path(dest)
|
152
|
+
|
153
|
+
await aioshutil.copytree(
|
154
|
+
self.path, dest, symlinks=True, ignore=self._clone_ignore()
|
155
|
+
)
|
156
|
+
|
157
|
+
return AsyncFoamCase(dest)
|
@@ -0,0 +1,354 @@
|
|
1
|
+
import sys
|
2
|
+
from pathlib import Path
|
3
|
+
from typing import (
|
4
|
+
Any,
|
5
|
+
Optional,
|
6
|
+
Tuple,
|
7
|
+
Union,
|
8
|
+
overload,
|
9
|
+
)
|
10
|
+
|
11
|
+
if sys.version_info >= (3, 9):
|
12
|
+
from collections.abc import (
|
13
|
+
Callable,
|
14
|
+
Collection,
|
15
|
+
Generator,
|
16
|
+
Iterator,
|
17
|
+
Mapping,
|
18
|
+
Sequence,
|
19
|
+
Set,
|
20
|
+
)
|
21
|
+
else:
|
22
|
+
from typing import AbstractSet as Set
|
23
|
+
from typing import (
|
24
|
+
Callable,
|
25
|
+
Collection,
|
26
|
+
Generator,
|
27
|
+
Iterator,
|
28
|
+
Mapping,
|
29
|
+
Sequence,
|
30
|
+
)
|
31
|
+
|
32
|
+
|
33
|
+
from .._files import FoamFieldFile, FoamFile
|
34
|
+
from .._util import is_sequence
|
35
|
+
|
36
|
+
|
37
|
+
class FoamCaseBase(Sequence["FoamCaseBase.TimeDirectory"]):
|
38
|
+
def __init__(self, path: Union[Path, str] = Path()):
|
39
|
+
self.path = Path(path).absolute()
|
40
|
+
|
41
|
+
class TimeDirectory(Set[FoamFieldFile]):
|
42
|
+
"""
|
43
|
+
An OpenFOAM time directory in a case.
|
44
|
+
|
45
|
+
Use to access field files in the directory, e.g. `time["U"]`.
|
46
|
+
|
47
|
+
:param path: The path to the time directory.
|
48
|
+
"""
|
49
|
+
|
50
|
+
def __init__(self, path: Union[Path, str]):
|
51
|
+
self.path = Path(path).absolute()
|
52
|
+
|
53
|
+
@property
|
54
|
+
def time(self) -> float:
|
55
|
+
"""The time that corresponds to this directory."""
|
56
|
+
return float(self.path.name)
|
57
|
+
|
58
|
+
@property
|
59
|
+
def name(self) -> str:
|
60
|
+
"""The name of this time directory."""
|
61
|
+
return self.path.name
|
62
|
+
|
63
|
+
def __getitem__(self, key: str) -> FoamFieldFile:
|
64
|
+
if (self.path / key).is_file():
|
65
|
+
return FoamFieldFile(self.path / key)
|
66
|
+
elif (self.path / f"{key}.gz").is_file():
|
67
|
+
return FoamFieldFile(self.path / f"{key}.gz")
|
68
|
+
else:
|
69
|
+
raise KeyError(key)
|
70
|
+
|
71
|
+
def __contains__(self, obj: object) -> bool:
|
72
|
+
if isinstance(obj, FoamFieldFile):
|
73
|
+
return obj.path.parent == self.path
|
74
|
+
elif isinstance(obj, str):
|
75
|
+
return (self.path / obj).is_file() or (
|
76
|
+
self.path / f"{obj}.gz"
|
77
|
+
).is_file()
|
78
|
+
else:
|
79
|
+
return False
|
80
|
+
|
81
|
+
def __iter__(self) -> Iterator[FoamFieldFile]:
|
82
|
+
for p in self.path.iterdir():
|
83
|
+
if p.is_file() and (
|
84
|
+
p.suffix != ".gz" or not p.with_suffix("").is_file()
|
85
|
+
):
|
86
|
+
yield FoamFieldFile(p)
|
87
|
+
|
88
|
+
def __len__(self) -> int:
|
89
|
+
return len(list(iter(self)))
|
90
|
+
|
91
|
+
def __fspath__(self) -> str:
|
92
|
+
return str(self.path)
|
93
|
+
|
94
|
+
def __repr__(self) -> str:
|
95
|
+
return f"{type(self).__qualname__}('{self.path}')"
|
96
|
+
|
97
|
+
def __str__(self) -> str:
|
98
|
+
return str(self.path)
|
99
|
+
|
100
|
+
@property
|
101
|
+
def _times(self) -> Sequence["FoamCaseBase.TimeDirectory"]:
|
102
|
+
times = []
|
103
|
+
for p in self.path.iterdir():
|
104
|
+
if p.is_dir():
|
105
|
+
try:
|
106
|
+
float(p.name)
|
107
|
+
except ValueError:
|
108
|
+
pass
|
109
|
+
else:
|
110
|
+
times.append(FoamCaseBase.TimeDirectory(p))
|
111
|
+
|
112
|
+
times.sort(key=lambda t: t.time)
|
113
|
+
|
114
|
+
return times
|
115
|
+
|
116
|
+
@overload
|
117
|
+
def __getitem__(
|
118
|
+
self, index: Union[int, float, str]
|
119
|
+
) -> "FoamCaseBase.TimeDirectory": ...
|
120
|
+
|
121
|
+
@overload
|
122
|
+
def __getitem__(self, index: slice) -> Sequence["FoamCaseBase.TimeDirectory"]: ...
|
123
|
+
|
124
|
+
def __getitem__(
|
125
|
+
self, index: Union[int, slice, float, str]
|
126
|
+
) -> Union["FoamCaseBase.TimeDirectory", Sequence["FoamCaseBase.TimeDirectory"]]:
|
127
|
+
if isinstance(index, str):
|
128
|
+
return FoamCaseBase.TimeDirectory(self.path / index)
|
129
|
+
elif isinstance(index, float):
|
130
|
+
for time in self._times:
|
131
|
+
if time.time == index:
|
132
|
+
return time
|
133
|
+
raise IndexError(f"Time {index} not found")
|
134
|
+
return self._times[index]
|
135
|
+
|
136
|
+
def __len__(self) -> int:
|
137
|
+
return len(self._times)
|
138
|
+
|
139
|
+
def _clean_paths(self) -> Set[Path]:
|
140
|
+
has_decompose_par_dict = (self.path / "system" / "decomposeParDict").is_file()
|
141
|
+
has_block_mesh_dict = (self.path / "system" / "blockMeshDict").is_file()
|
142
|
+
|
143
|
+
paths = set()
|
144
|
+
|
145
|
+
for p in self.path.iterdir():
|
146
|
+
if p.is_dir():
|
147
|
+
try:
|
148
|
+
t = float(p.name)
|
149
|
+
except ValueError:
|
150
|
+
pass
|
151
|
+
else:
|
152
|
+
if t != 0:
|
153
|
+
paths.add(p)
|
154
|
+
|
155
|
+
if has_decompose_par_dict and p.name.startswith("processor"):
|
156
|
+
paths.add(p)
|
157
|
+
|
158
|
+
if (self.path / "0.orig").is_dir() and (self.path / "0").is_dir():
|
159
|
+
paths.add(self.path / "0")
|
160
|
+
|
161
|
+
if has_block_mesh_dict and (self.path / "constant" / "polyMesh").exists():
|
162
|
+
paths.add(self.path / "constant" / "polyMesh")
|
163
|
+
|
164
|
+
if self._run_script() is not None:
|
165
|
+
paths.update(self.path.glob("log.*"))
|
166
|
+
|
167
|
+
return paths
|
168
|
+
|
169
|
+
def _clone_ignore(
|
170
|
+
self,
|
171
|
+
) -> Callable[[Union[Path, str], Collection[str]], Collection[str]]:
|
172
|
+
clean_paths = self._clean_paths()
|
173
|
+
|
174
|
+
def ignore(path: Union[Path, str], names: Collection[str]) -> Collection[str]:
|
175
|
+
paths = {Path(path) / name for name in names}
|
176
|
+
return {p.name for p in paths.intersection(clean_paths)}
|
177
|
+
|
178
|
+
return ignore
|
179
|
+
|
180
|
+
def _clean_script(self) -> Optional[Path]:
|
181
|
+
"""Return the path to the (All)clean script, or None if no clean script is found."""
|
182
|
+
clean = self.path / "clean"
|
183
|
+
all_clean = self.path / "Allclean"
|
184
|
+
|
185
|
+
if clean.is_file():
|
186
|
+
return clean
|
187
|
+
elif all_clean.is_file():
|
188
|
+
return all_clean
|
189
|
+
else:
|
190
|
+
return None
|
191
|
+
|
192
|
+
def _run_script(self, *, parallel: Optional[bool] = None) -> Optional[Path]:
|
193
|
+
"""Return the path to the (All)run script, or None if no run script is found."""
|
194
|
+
run = self.path / "run"
|
195
|
+
run_parallel = self.path / "run-parallel"
|
196
|
+
all_run = self.path / "Allrun"
|
197
|
+
all_run_parallel = self.path / "Allrun-parallel"
|
198
|
+
|
199
|
+
if run.is_file() or all_run.is_file():
|
200
|
+
if run_parallel.is_file() or all_run_parallel.is_file():
|
201
|
+
if parallel:
|
202
|
+
return run_parallel if run_parallel.is_file() else all_run_parallel
|
203
|
+
elif parallel is False:
|
204
|
+
return run if run.is_file() else all_run
|
205
|
+
else:
|
206
|
+
raise ValueError(
|
207
|
+
"Both (All)run and (All)run-parallel scripts are present. Please specify parallel argument."
|
208
|
+
)
|
209
|
+
return run if run.is_file() else all_run
|
210
|
+
elif parallel is not False and (
|
211
|
+
run_parallel.is_file() or all_run_parallel.is_file()
|
212
|
+
):
|
213
|
+
return run_parallel if run_parallel.is_file() else all_run_parallel
|
214
|
+
else:
|
215
|
+
return None
|
216
|
+
|
217
|
+
def _run_cmds(
|
218
|
+
self,
|
219
|
+
cmd: Optional[Union[Sequence[Union[str, Path]], str, Path]] = None,
|
220
|
+
*,
|
221
|
+
script: bool = True,
|
222
|
+
parallel: Optional[bool] = None,
|
223
|
+
check: bool = True,
|
224
|
+
) -> Generator[Tuple[str, Sequence[Any], Mapping[str, Any]], None, None]:
|
225
|
+
if cmd is not None:
|
226
|
+
if parallel:
|
227
|
+
cmd = self._parallel_cmd(cmd)
|
228
|
+
|
229
|
+
yield ("_run", (cmd,), {"check": check})
|
230
|
+
else:
|
231
|
+
script_path = self._run_script(parallel=parallel) if script else None
|
232
|
+
|
233
|
+
if script_path is not None:
|
234
|
+
yield ("_run", ([script_path],), {"check": check})
|
235
|
+
|
236
|
+
else:
|
237
|
+
if not self and (self.path / "0.orig").is_dir():
|
238
|
+
yield ("restore_0_dir", (), {})
|
239
|
+
|
240
|
+
if (self.path / "system" / "blockMeshDict").is_file():
|
241
|
+
yield ("block_mesh", (), {"check": check})
|
242
|
+
|
243
|
+
if parallel is None:
|
244
|
+
parallel = (
|
245
|
+
self._nprocessors > 0
|
246
|
+
or (self.path / "system" / "decomposeParDict").is_file()
|
247
|
+
)
|
248
|
+
|
249
|
+
if parallel:
|
250
|
+
if (
|
251
|
+
self._nprocessors == 0
|
252
|
+
and (self.path / "system" / "decomposeParDict").is_file()
|
253
|
+
):
|
254
|
+
yield ("decompose_par", (), {"check": check})
|
255
|
+
|
256
|
+
yield (
|
257
|
+
"run",
|
258
|
+
([self.application],),
|
259
|
+
{"parallel": parallel, "check": check},
|
260
|
+
)
|
261
|
+
|
262
|
+
def _parallel_cmd(
|
263
|
+
self, cmd: Union[Sequence[Union[str, Path]], str, Path]
|
264
|
+
) -> Union[Sequence[Union[str, Path]], str]:
|
265
|
+
if not is_sequence(cmd):
|
266
|
+
return f"mpiexec -np {self._nprocessors} {cmd} -parallel"
|
267
|
+
else:
|
268
|
+
return [
|
269
|
+
"mpiexec",
|
270
|
+
"-np",
|
271
|
+
str(self._nprocessors),
|
272
|
+
cmd[0],
|
273
|
+
"-parallel",
|
274
|
+
*cmd[1:],
|
275
|
+
]
|
276
|
+
|
277
|
+
@property
|
278
|
+
def name(self) -> str:
|
279
|
+
"""The name of the case."""
|
280
|
+
return self.path.name
|
281
|
+
|
282
|
+
def file(self, path: Union[Path, str]) -> FoamFile:
|
283
|
+
"""Return a FoamFile object for the given path in the case."""
|
284
|
+
return FoamFile(self.path / path)
|
285
|
+
|
286
|
+
@property
|
287
|
+
def _nsubdomains(self) -> Optional[int]:
|
288
|
+
"""Return the number of subdomains as set in the decomposeParDict, or None if no decomposeParDict is found."""
|
289
|
+
try:
|
290
|
+
nsubdomains = self.decompose_par_dict["numberOfSubdomains"]
|
291
|
+
if not isinstance(nsubdomains, int):
|
292
|
+
raise TypeError(
|
293
|
+
f"numberOfSubdomains in {self.decompose_par_dict} is not an integer"
|
294
|
+
)
|
295
|
+
return nsubdomains
|
296
|
+
except FileNotFoundError:
|
297
|
+
return None
|
298
|
+
|
299
|
+
@property
|
300
|
+
def _nprocessors(self) -> int:
|
301
|
+
"""Return the number of processor directories in the case."""
|
302
|
+
return len(list(self.path.glob("processor*")))
|
303
|
+
|
304
|
+
@property
|
305
|
+
def application(self) -> str:
|
306
|
+
"""The application name as set in the controlDict."""
|
307
|
+
application = self.control_dict["application"]
|
308
|
+
if not isinstance(application, str):
|
309
|
+
raise TypeError(f"application in {self.control_dict} is not a string")
|
310
|
+
return application
|
311
|
+
|
312
|
+
@property
|
313
|
+
def control_dict(self) -> FoamFile:
|
314
|
+
"""The controlDict file."""
|
315
|
+
return self.file("system/controlDict")
|
316
|
+
|
317
|
+
@property
|
318
|
+
def fv_schemes(self) -> FoamFile:
|
319
|
+
"""The fvSchemes file."""
|
320
|
+
return self.file("system/fvSchemes")
|
321
|
+
|
322
|
+
@property
|
323
|
+
def fv_solution(self) -> FoamFile:
|
324
|
+
"""The fvSolution file."""
|
325
|
+
return self.file("system/fvSolution")
|
326
|
+
|
327
|
+
@property
|
328
|
+
def decompose_par_dict(self) -> FoamFile:
|
329
|
+
"""The decomposeParDict file."""
|
330
|
+
return self.file("system/decomposeParDict")
|
331
|
+
|
332
|
+
@property
|
333
|
+
def block_mesh_dict(self) -> FoamFile:
|
334
|
+
"""The blockMeshDict file."""
|
335
|
+
return self.file("system/blockMeshDict")
|
336
|
+
|
337
|
+
@property
|
338
|
+
def transport_properties(self) -> FoamFile:
|
339
|
+
"""The transportProperties file."""
|
340
|
+
return self.file("constant/transportProperties")
|
341
|
+
|
342
|
+
@property
|
343
|
+
def turbulence_properties(self) -> FoamFile:
|
344
|
+
"""The turbulenceProperties file."""
|
345
|
+
return self.file("constant/turbulenceProperties")
|
346
|
+
|
347
|
+
def __fspath__(self) -> str:
|
348
|
+
return str(self.path)
|
349
|
+
|
350
|
+
def __repr__(self) -> str:
|
351
|
+
return f"{type(self).__qualname__}('{self.path}')"
|
352
|
+
|
353
|
+
def __str__(self) -> str:
|
354
|
+
return str(self.path)
|
@@ -0,0 +1,121 @@
|
|
1
|
+
import shutil
|
2
|
+
import sys
|
3
|
+
from pathlib import Path
|
4
|
+
from typing import (
|
5
|
+
Optional,
|
6
|
+
Union,
|
7
|
+
)
|
8
|
+
|
9
|
+
if sys.version_info >= (3, 9):
|
10
|
+
from collections.abc import Sequence
|
11
|
+
else:
|
12
|
+
from typing import Sequence
|
13
|
+
|
14
|
+
from .._util import run_process
|
15
|
+
from ._base import FoamCaseBase
|
16
|
+
|
17
|
+
|
18
|
+
class FoamCase(FoamCaseBase):
|
19
|
+
"""
|
20
|
+
An OpenFOAM case.
|
21
|
+
|
22
|
+
Provides methods for running and cleaning cases, as well as accessing files.
|
23
|
+
|
24
|
+
Access the time directories of the case as a sequence, e.g. `case[0]` or `case[-1]`.
|
25
|
+
|
26
|
+
:param path: The path to the case directory.
|
27
|
+
"""
|
28
|
+
|
29
|
+
def clean(
|
30
|
+
self,
|
31
|
+
*,
|
32
|
+
script: bool = True,
|
33
|
+
check: bool = False,
|
34
|
+
) -> None:
|
35
|
+
"""
|
36
|
+
Clean this case.
|
37
|
+
|
38
|
+
:param script: If True, use an (All)clean script if it exists. If False, ignore any clean scripts.
|
39
|
+
:param check: If True, raise a CalledProcessError if the clean script returns a non-zero exit code.
|
40
|
+
"""
|
41
|
+
script_path = self._clean_script() if script else None
|
42
|
+
|
43
|
+
if script_path is not None:
|
44
|
+
self.run([script_path], check=check)
|
45
|
+
else:
|
46
|
+
for p in self._clean_paths():
|
47
|
+
if p.is_dir():
|
48
|
+
shutil.rmtree(p)
|
49
|
+
else:
|
50
|
+
p.unlink()
|
51
|
+
|
52
|
+
def _run(
|
53
|
+
self,
|
54
|
+
cmd: Union[Sequence[Union[str, Path]], str, Path],
|
55
|
+
*,
|
56
|
+
check: bool = True,
|
57
|
+
) -> None:
|
58
|
+
run_process(cmd, cwd=self.path, check=check)
|
59
|
+
|
60
|
+
def run(
|
61
|
+
self,
|
62
|
+
cmd: Optional[Union[Sequence[Union[str, Path]], str, Path]] = None,
|
63
|
+
*,
|
64
|
+
script: bool = True,
|
65
|
+
parallel: Optional[bool] = None,
|
66
|
+
check: bool = True,
|
67
|
+
) -> None:
|
68
|
+
"""
|
69
|
+
Run this case, or a specified command in the context of this case.
|
70
|
+
|
71
|
+
: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.
|
72
|
+
:param script: If True and `cmd` is None, use an (All)run(-parallel) script if it exists for running the case. If False or no run script is found, autodetermine the command(s) needed to run the case.
|
73
|
+
:param parallel: If True, run in parallel using MPI. If None, autodetect whether to run in parallel.
|
74
|
+
:param check: If True, raise a CalledProcessError if any command returns a non-zero exit code.
|
75
|
+
"""
|
76
|
+
for name, args, kwargs in self._run_cmds(
|
77
|
+
cmd=cmd, script=script, parallel=parallel, check=check
|
78
|
+
):
|
79
|
+
getattr(self, name)(*args, **kwargs)
|
80
|
+
|
81
|
+
def block_mesh(self, *, check: bool = True) -> None:
|
82
|
+
"""Run blockMesh on this case."""
|
83
|
+
self.run(["blockMesh"], check=check)
|
84
|
+
|
85
|
+
def decompose_par(self, *, check: bool = True) -> None:
|
86
|
+
"""Decompose this case for parallel running."""
|
87
|
+
self.run(["decomposePar"], check=check)
|
88
|
+
|
89
|
+
def reconstruct_par(self, *, check: bool = True) -> None:
|
90
|
+
"""Reconstruct this case after parallel running."""
|
91
|
+
self.run(["reconstructPar"], check=check)
|
92
|
+
|
93
|
+
def restore_0_dir(self) -> None:
|
94
|
+
"""Restore the 0 directory from the 0.orig directory."""
|
95
|
+
shutil.rmtree(self.path / "0", ignore_errors=True)
|
96
|
+
shutil.copytree(self.path / "0.orig", self.path / "0")
|
97
|
+
|
98
|
+
def copy(self, dest: Union[Path, str]) -> "FoamCase":
|
99
|
+
"""
|
100
|
+
Make a copy of this case.
|
101
|
+
|
102
|
+
:param dest: The destination path.
|
103
|
+
"""
|
104
|
+
return FoamCase(shutil.copytree(self.path, dest, symlinks=True))
|
105
|
+
|
106
|
+
def clone(self, dest: Union[Path, str]) -> "FoamCase":
|
107
|
+
"""
|
108
|
+
Clone this case (make a clean copy).
|
109
|
+
|
110
|
+
:param dest: The destination path.
|
111
|
+
"""
|
112
|
+
if self._clean_script() is not None:
|
113
|
+
copy = self.copy(dest)
|
114
|
+
copy.clean()
|
115
|
+
return copy
|
116
|
+
|
117
|
+
dest = Path(dest)
|
118
|
+
|
119
|
+
shutil.copytree(self.path, dest, symlinks=True, ignore=self._clone_ignore())
|
120
|
+
|
121
|
+
return FoamCase(dest)
|
@@ -134,7 +134,7 @@ _DIMENSIONS = (
|
|
134
134
|
Literal("[").suppress() + common.number * 7 + Literal("]").suppress()
|
135
135
|
).set_parse_action(lambda tks: FoamDict.DimensionSet(*tks))
|
136
136
|
_TENSOR = _list_of(common.number) | common.number
|
137
|
-
_IDENTIFIER = Word(identchars + "$", printables, exclude_chars=";")
|
137
|
+
_IDENTIFIER = Word(identchars + "$", printables, exclude_chars="{;}")
|
138
138
|
_DIMENSIONED = (Opt(_IDENTIFIER) + _DIMENSIONS + _TENSOR).set_parse_action(
|
139
139
|
lambda tks: FoamDict.Dimensioned(*reversed(tks.as_list()))
|
140
140
|
)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: foamlib
|
3
|
-
Version: 0.3.
|
3
|
+
Version: 0.3.13
|
4
4
|
Summary: A Python interface for interacting with OpenFOAM
|
5
5
|
Author-email: "Gabriel S. Gerlero" <ggerlero@cimec.unl.edu.ar>
|
6
6
|
Project-URL: Homepage, https://github.com/gerlero/foamlib
|
@@ -42,7 +42,7 @@ Requires-Dist: foamlib[test]; extra == "typing"
|
|
42
42
|
Requires-Dist: mypy<2,>=1; extra == "typing"
|
43
43
|
Provides-Extra: docs
|
44
44
|
Requires-Dist: foamlib[numpy]; extra == "docs"
|
45
|
-
Requires-Dist: sphinx<
|
45
|
+
Requires-Dist: sphinx<9,>=7; extra == "docs"
|
46
46
|
Requires-Dist: sphinx_rtd_theme; extra == "docs"
|
47
47
|
Provides-Extra: dev
|
48
48
|
Requires-Dist: foamlib[numpy]; extra == "dev"
|