foamlib 0.4.4__tar.gz → 0.5.0__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.4.4 → foamlib-0.5.0}/PKG-INFO +10 -1
- {foamlib-0.4.4 → foamlib-0.5.0}/README.md +8 -0
- {foamlib-0.4.4 → foamlib-0.5.0}/foamlib/__init__.py +1 -3
- {foamlib-0.4.4 → foamlib-0.5.0}/foamlib/_cases/__init__.py +1 -2
- {foamlib-0.4.4 → foamlib-0.5.0}/foamlib/_cases/_async.py +93 -54
- foamlib-0.5.0/foamlib/_cases/_base.py +213 -0
- foamlib-0.4.4/foamlib/_cases/_base.py → foamlib-0.5.0/foamlib/_cases/_recipes.py +32 -196
- foamlib-0.5.0/foamlib/_cases/_subprocess.py +86 -0
- {foamlib-0.4.4 → foamlib-0.5.0}/foamlib/_cases/_sync.py +72 -46
- foamlib-0.5.0/foamlib/_cases/_util.py +27 -0
- {foamlib-0.4.4 → foamlib-0.5.0}/foamlib/_files/_files.py +4 -8
- {foamlib-0.4.4 → foamlib-0.5.0}/foamlib/_files/_serialization.py +1 -1
- {foamlib-0.4.4 → foamlib-0.5.0}/foamlib.egg-info/PKG-INFO +10 -1
- {foamlib-0.4.4 → foamlib-0.5.0}/foamlib.egg-info/SOURCES.txt +4 -2
- {foamlib-0.4.4 → foamlib-0.5.0}/pyproject.toml +1 -0
- foamlib-0.4.4/foamlib/_cases/_util.py +0 -35
- {foamlib-0.4.4 → foamlib-0.5.0}/LICENSE.txt +0 -0
- {foamlib-0.4.4 → foamlib-0.5.0}/foamlib/_files/__init__.py +0 -0
- {foamlib-0.4.4 → foamlib-0.5.0}/foamlib/_files/_base.py +0 -0
- {foamlib-0.4.4 → foamlib-0.5.0}/foamlib/_files/_io.py +0 -0
- {foamlib-0.4.4 → foamlib-0.5.0}/foamlib/_files/_parsing.py +0 -0
- {foamlib-0.4.4/foamlib → foamlib-0.5.0/foamlib/_files}/_util.py +0 -0
- {foamlib-0.4.4 → foamlib-0.5.0}/foamlib/py.typed +0 -0
- {foamlib-0.4.4 → foamlib-0.5.0}/foamlib.egg-info/dependency_links.txt +0 -0
- {foamlib-0.4.4 → foamlib-0.5.0}/foamlib.egg-info/requires.txt +0 -0
- {foamlib-0.4.4 → foamlib-0.5.0}/foamlib.egg-info/top_level.txt +0 -0
- {foamlib-0.4.4 → foamlib-0.5.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: foamlib
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.5.0
|
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
|
@@ -19,6 +19,7 @@ Classifier: Programming Language :: Python :: 3.9
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.10
|
20
20
|
Classifier: Programming Language :: Python :: 3.11
|
21
21
|
Classifier: Programming Language :: Python :: 3.12
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
22
23
|
Classifier: Topic :: Scientific/Engineering
|
23
24
|
Classifier: Topic :: Software Development
|
24
25
|
Classifier: Typing :: Typed
|
@@ -123,6 +124,14 @@ my_pitz.clean()
|
|
123
124
|
my_pitz.control_dict["writeInterval"] = 10
|
124
125
|
```
|
125
126
|
|
127
|
+
### Make multiple file reads and writes in a single go
|
128
|
+
|
129
|
+
```python
|
130
|
+
with my_pitz.fv_schemes as f:
|
131
|
+
f["gradSchemes"]["default"] = f["divSchemes"]["default"]
|
132
|
+
f["snGradSchemes"]["default"] = "uncorrected"
|
133
|
+
```
|
134
|
+
|
126
135
|
### Run a case asynchronously
|
127
136
|
|
128
137
|
```python
|
@@ -70,6 +70,14 @@ my_pitz.clean()
|
|
70
70
|
my_pitz.control_dict["writeInterval"] = 10
|
71
71
|
```
|
72
72
|
|
73
|
+
### Make multiple file reads and writes in a single go
|
74
|
+
|
75
|
+
```python
|
76
|
+
with my_pitz.fv_schemes as f:
|
77
|
+
f["gradSchemes"]["default"] = f["divSchemes"]["default"]
|
78
|
+
f["snGradSchemes"]["default"] = "uncorrected"
|
79
|
+
```
|
80
|
+
|
73
81
|
### Run a case asynchronously
|
74
82
|
|
75
83
|
```python
|
@@ -1,11 +1,10 @@
|
|
1
1
|
"""A Python interface for interacting with OpenFOAM."""
|
2
2
|
|
3
|
-
__version__ = "0.
|
3
|
+
__version__ = "0.5.0"
|
4
4
|
|
5
5
|
from ._cases import (
|
6
6
|
AsyncFoamCase,
|
7
7
|
CalledProcessError,
|
8
|
-
CalledProcessWarning,
|
9
8
|
FoamCase,
|
10
9
|
FoamCaseBase,
|
11
10
|
)
|
@@ -19,5 +18,4 @@ __all__ = [
|
|
19
18
|
"FoamFieldFile",
|
20
19
|
"FoamFileBase",
|
21
20
|
"CalledProcessError",
|
22
|
-
"CalledProcessWarning",
|
23
21
|
]
|
@@ -1,12 +1,11 @@
|
|
1
1
|
from ._async import AsyncFoamCase
|
2
2
|
from ._base import FoamCaseBase
|
3
|
+
from ._subprocess import CalledProcessError
|
3
4
|
from ._sync import FoamCase
|
4
|
-
from ._util import CalledProcessError, CalledProcessWarning
|
5
5
|
|
6
6
|
__all__ = [
|
7
7
|
"FoamCaseBase",
|
8
8
|
"FoamCase",
|
9
9
|
"AsyncFoamCase",
|
10
10
|
"CalledProcessError",
|
11
|
-
"CalledProcessWarning",
|
12
11
|
]
|
@@ -1,6 +1,8 @@
|
|
1
1
|
import asyncio
|
2
2
|
import multiprocessing
|
3
|
+
import os
|
3
4
|
import sys
|
5
|
+
import tempfile
|
4
6
|
from contextlib import asynccontextmanager
|
5
7
|
from pathlib import Path
|
6
8
|
from typing import (
|
@@ -14,14 +16,19 @@ if sys.version_info >= (3, 9):
|
|
14
16
|
else:
|
15
17
|
from typing import AsyncGenerator, Collection, Sequence
|
16
18
|
|
19
|
+
if sys.version_info >= (3, 11):
|
20
|
+
from typing import Self
|
21
|
+
else:
|
22
|
+
from typing_extensions import Self
|
23
|
+
|
17
24
|
import aioshutil
|
18
25
|
|
19
|
-
from
|
20
|
-
from .
|
21
|
-
from ._util import
|
26
|
+
from ._recipes import _FoamCaseRecipes
|
27
|
+
from ._subprocess import run_async
|
28
|
+
from ._util import awaitableasynccontextmanager
|
22
29
|
|
23
30
|
|
24
|
-
class AsyncFoamCase(
|
31
|
+
class AsyncFoamCase(_FoamCaseRecipes):
|
25
32
|
"""
|
26
33
|
An OpenFOAM case with asynchronous support.
|
27
34
|
|
@@ -63,17 +70,19 @@ class AsyncFoamCase(FoamCaseBase):
|
|
63
70
|
AsyncFoamCase._cpus_cond.notify(cpus)
|
64
71
|
|
65
72
|
@staticmethod
|
66
|
-
async def _rmtree(
|
73
|
+
async def _rmtree(
|
74
|
+
path: Union["os.PathLike[str]", str], ignore_errors: bool = False
|
75
|
+
) -> None:
|
67
76
|
await aioshutil.rmtree(path, ignore_errors=ignore_errors) # type: ignore [call-arg]
|
68
77
|
|
69
78
|
@staticmethod
|
70
79
|
async def _copytree(
|
71
|
-
src:
|
72
|
-
dest:
|
80
|
+
src: Union["os.PathLike[str]", str],
|
81
|
+
dest: Union["os.PathLike[str]", str],
|
73
82
|
*,
|
74
83
|
symlinks: bool = False,
|
75
84
|
ignore: Optional[
|
76
|
-
Callable[[Union[
|
85
|
+
Callable[[Union["os.PathLike[str]", str], Collection[str]], Collection[str]]
|
77
86
|
] = None,
|
78
87
|
) -> None:
|
79
88
|
await aioshutil.copytree(src, dest, symlinks=symlinks, ignore=ignore)
|
@@ -95,60 +104,54 @@ class AsyncFoamCase(FoamCaseBase):
|
|
95
104
|
|
96
105
|
async def _run(
|
97
106
|
self,
|
98
|
-
cmd: Union[Sequence[Union[str,
|
107
|
+
cmd: Union[Sequence[Union[str, "os.PathLike[str]"]], str],
|
99
108
|
*,
|
100
109
|
parallel: bool = False,
|
101
110
|
cpus: int = 1,
|
102
111
|
check: bool = True,
|
112
|
+
log: bool = True,
|
103
113
|
) -> None:
|
104
|
-
|
105
|
-
if
|
106
|
-
if
|
107
|
-
cmd =
|
108
|
-
|
109
|
-
|
110
|
-
|
114
|
+
with self._output(cmd, log=log) as (stdout, stderr):
|
115
|
+
if parallel:
|
116
|
+
if isinstance(cmd, str):
|
117
|
+
cmd = [
|
118
|
+
"mpiexec",
|
119
|
+
"-n",
|
120
|
+
str(cpus),
|
121
|
+
"/bin/sh",
|
122
|
+
"-c",
|
123
|
+
f"{cmd} -parallel",
|
124
|
+
]
|
125
|
+
else:
|
126
|
+
cmd = ["mpiexec", "-n", str(cpus), *cmd, "-parallel"]
|
127
|
+
|
128
|
+
async with self._cpus(cpus):
|
129
|
+
await run_async(
|
130
|
+
cmd,
|
131
|
+
check=check,
|
111
132
|
cwd=self.path,
|
112
|
-
env=self._env(shell=
|
113
|
-
stdout=
|
114
|
-
stderr=
|
115
|
-
if check
|
116
|
-
else asyncio.subprocess.DEVNULL,
|
133
|
+
env=self._env(shell=isinstance(cmd, str)),
|
134
|
+
stdout=stdout,
|
135
|
+
stderr=stderr,
|
117
136
|
)
|
118
137
|
|
119
|
-
else:
|
120
|
-
if parallel:
|
121
|
-
cmd = ["mpiexec", "-np", str(cpus), *cmd, "-parallel"]
|
122
|
-
|
123
|
-
if sys.version_info < (3, 8):
|
124
|
-
cmd = (str(arg) for arg in cmd)
|
125
|
-
proc = await asyncio.create_subprocess_exec(
|
126
|
-
*cmd,
|
127
|
-
cwd=self.path,
|
128
|
-
env=self._env(shell=False),
|
129
|
-
stdout=asyncio.subprocess.DEVNULL,
|
130
|
-
stderr=asyncio.subprocess.PIPE
|
131
|
-
if check
|
132
|
-
else asyncio.subprocess.DEVNULL,
|
133
|
-
)
|
134
|
-
|
135
|
-
stdout, stderr = await proc.communicate()
|
136
|
-
|
137
|
-
assert stdout is None
|
138
|
-
assert proc.returncode is not None
|
139
|
-
|
140
|
-
if check:
|
141
|
-
check_returncode(proc.returncode, cmd, stderr.decode())
|
142
|
-
|
143
138
|
async def run(
|
144
139
|
self,
|
145
|
-
cmd: Optional[Union[Sequence[Union[str,
|
140
|
+
cmd: Optional[Union[Sequence[Union[str, "os.PathLike[str]"]], str]] = None,
|
146
141
|
*,
|
147
142
|
script: bool = True,
|
148
143
|
parallel: Optional[bool] = None,
|
149
144
|
cpus: Optional[int] = None,
|
150
145
|
check: bool = True,
|
151
146
|
) -> None:
|
147
|
+
"""
|
148
|
+
Run this case, or a specified command in the context of this case.
|
149
|
+
|
150
|
+
: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.
|
151
|
+
: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.
|
152
|
+
:param parallel: If True, run in parallel using MPI. If None, autodetect whether to run in parallel.
|
153
|
+
:param check: If True, raise a CalledProcessError if any command returns a non-zero exit code.
|
154
|
+
"""
|
152
155
|
for name, args, kwargs in self._run_cmds(
|
153
156
|
cmd=cmd, script=script, parallel=parallel, cpus=cpus, check=check
|
154
157
|
):
|
@@ -171,24 +174,60 @@ class AsyncFoamCase(FoamCaseBase):
|
|
171
174
|
for name, args, kwargs in self._restore_0_dir_cmds():
|
172
175
|
await getattr(self, name)(*args, **kwargs)
|
173
176
|
|
174
|
-
|
177
|
+
@awaitableasynccontextmanager
|
178
|
+
@asynccontextmanager
|
179
|
+
async def copy(
|
180
|
+
self, dst: Optional[Union["os.PathLike[str]", str]] = None
|
181
|
+
) -> "AsyncGenerator[Self]":
|
175
182
|
"""
|
176
183
|
Make a copy of this case.
|
177
184
|
|
178
|
-
|
185
|
+
Use as an async context manager to automatically delete the copy when done.
|
186
|
+
|
187
|
+
:param dst: The destination path. If None, copy to a temporary directory.
|
179
188
|
"""
|
180
|
-
|
189
|
+
if dst is None:
|
190
|
+
dst = Path(tempfile.mkdtemp(), self.name)
|
191
|
+
tmp = True
|
192
|
+
else:
|
193
|
+
tmp = False
|
194
|
+
|
195
|
+
for name, args, kwargs in self._copy_cmds(dst):
|
181
196
|
await getattr(self, name)(*args, **kwargs)
|
182
197
|
|
183
|
-
|
198
|
+
yield type(self)(dst)
|
184
199
|
|
185
|
-
|
200
|
+
if tmp:
|
201
|
+
assert isinstance(dst, Path)
|
202
|
+
await self._rmtree(dst.parent)
|
203
|
+
else:
|
204
|
+
await self._rmtree(dst)
|
205
|
+
|
206
|
+
@awaitableasynccontextmanager
|
207
|
+
@asynccontextmanager
|
208
|
+
async def clone(
|
209
|
+
self, dst: Optional[Union["os.PathLike[str]", str]] = None
|
210
|
+
) -> "AsyncGenerator[Self]":
|
186
211
|
"""
|
187
212
|
Clone this case (make a clean copy).
|
188
213
|
|
189
|
-
|
214
|
+
Use as an async context manager to automatically delete the clone when done.
|
215
|
+
|
216
|
+
:param dst: The destination path. If None, clone to a temporary directory.
|
190
217
|
"""
|
191
|
-
|
218
|
+
if dst is None:
|
219
|
+
dst = Path(tempfile.mkdtemp(), self.name)
|
220
|
+
tmp = True
|
221
|
+
else:
|
222
|
+
tmp = False
|
223
|
+
|
224
|
+
for name, args, kwargs in self._clone_cmds(dst):
|
192
225
|
await getattr(self, name)(*args, **kwargs)
|
193
226
|
|
194
|
-
|
227
|
+
yield type(self)(dst)
|
228
|
+
|
229
|
+
if tmp:
|
230
|
+
assert isinstance(dst, Path)
|
231
|
+
await self._rmtree(dst.parent)
|
232
|
+
else:
|
233
|
+
await self._rmtree(dst)
|
@@ -0,0 +1,213 @@
|
|
1
|
+
import os
|
2
|
+
import shutil
|
3
|
+
import sys
|
4
|
+
from pathlib import Path
|
5
|
+
from typing import (
|
6
|
+
Optional,
|
7
|
+
Union,
|
8
|
+
overload,
|
9
|
+
)
|
10
|
+
|
11
|
+
if sys.version_info >= (3, 9):
|
12
|
+
from collections.abc import (
|
13
|
+
Iterator,
|
14
|
+
Sequence,
|
15
|
+
Set,
|
16
|
+
)
|
17
|
+
else:
|
18
|
+
from typing import AbstractSet as Set
|
19
|
+
from typing import (
|
20
|
+
Iterator,
|
21
|
+
Sequence,
|
22
|
+
)
|
23
|
+
|
24
|
+
from .._files import FoamFieldFile, FoamFile
|
25
|
+
|
26
|
+
|
27
|
+
class FoamCaseBase(Sequence["FoamCaseBase.TimeDirectory"]):
|
28
|
+
def __init__(self, path: Union["os.PathLike[str]", str] = Path()):
|
29
|
+
self.path = Path(path).absolute()
|
30
|
+
|
31
|
+
class TimeDirectory(Set[FoamFieldFile]):
|
32
|
+
"""
|
33
|
+
An OpenFOAM time directory in a case.
|
34
|
+
|
35
|
+
Use to access field files in the directory, e.g. `time["U"]`.
|
36
|
+
|
37
|
+
:param path: The path to the time directory.
|
38
|
+
"""
|
39
|
+
|
40
|
+
def __init__(self, path: Union["os.PathLike[str]", str]):
|
41
|
+
self.path = Path(path).absolute()
|
42
|
+
|
43
|
+
@property
|
44
|
+
def time(self) -> float:
|
45
|
+
"""The time that corresponds to this directory."""
|
46
|
+
return float(self.path.name)
|
47
|
+
|
48
|
+
@property
|
49
|
+
def name(self) -> str:
|
50
|
+
"""The name of this time directory."""
|
51
|
+
return self.path.name
|
52
|
+
|
53
|
+
def __getitem__(self, key: str) -> FoamFieldFile:
|
54
|
+
if (self.path / f"{key}.gz").is_file() and not (self.path / key).is_file():
|
55
|
+
return FoamFieldFile(self.path / f"{key}.gz")
|
56
|
+
else:
|
57
|
+
return FoamFieldFile(self.path / key)
|
58
|
+
|
59
|
+
def __contains__(self, obj: object) -> bool:
|
60
|
+
if isinstance(obj, FoamFieldFile):
|
61
|
+
return obj.path.parent == self.path
|
62
|
+
elif isinstance(obj, str):
|
63
|
+
return (self.path / obj).is_file() or (
|
64
|
+
self.path / f"{obj}.gz"
|
65
|
+
).is_file()
|
66
|
+
else:
|
67
|
+
return False
|
68
|
+
|
69
|
+
def __iter__(self) -> Iterator[FoamFieldFile]:
|
70
|
+
for p in self.path.iterdir():
|
71
|
+
if p.is_file() and (
|
72
|
+
p.suffix != ".gz" or not p.with_suffix("").is_file()
|
73
|
+
):
|
74
|
+
yield FoamFieldFile(p)
|
75
|
+
|
76
|
+
def __len__(self) -> int:
|
77
|
+
return len(list(iter(self)))
|
78
|
+
|
79
|
+
def __delitem__(self, key: str) -> None:
|
80
|
+
if (self.path / f"{key}.gz").is_file() and not (self.path / key).is_file():
|
81
|
+
(self.path / f"{key}.gz").unlink()
|
82
|
+
else:
|
83
|
+
(self.path / key).unlink()
|
84
|
+
|
85
|
+
def __fspath__(self) -> str:
|
86
|
+
return str(self.path)
|
87
|
+
|
88
|
+
def __repr__(self) -> str:
|
89
|
+
return f"{type(self).__qualname__}('{self.path}')"
|
90
|
+
|
91
|
+
def __str__(self) -> str:
|
92
|
+
return str(self.path)
|
93
|
+
|
94
|
+
@property
|
95
|
+
def _times(self) -> Sequence["FoamCaseBase.TimeDirectory"]:
|
96
|
+
times = []
|
97
|
+
for p in self.path.iterdir():
|
98
|
+
if p.is_dir():
|
99
|
+
try:
|
100
|
+
float(p.name)
|
101
|
+
except ValueError:
|
102
|
+
pass
|
103
|
+
else:
|
104
|
+
times.append(FoamCaseBase.TimeDirectory(p))
|
105
|
+
|
106
|
+
times.sort(key=lambda t: t.time)
|
107
|
+
|
108
|
+
return times
|
109
|
+
|
110
|
+
@overload
|
111
|
+
def __getitem__(
|
112
|
+
self, index: Union[int, float, str]
|
113
|
+
) -> "FoamCaseBase.TimeDirectory": ...
|
114
|
+
|
115
|
+
@overload
|
116
|
+
def __getitem__(self, index: slice) -> Sequence["FoamCaseBase.TimeDirectory"]: ...
|
117
|
+
|
118
|
+
def __getitem__(
|
119
|
+
self, index: Union[int, slice, float, str]
|
120
|
+
) -> Union["FoamCaseBase.TimeDirectory", Sequence["FoamCaseBase.TimeDirectory"]]:
|
121
|
+
if isinstance(index, str):
|
122
|
+
return FoamCaseBase.TimeDirectory(self.path / index)
|
123
|
+
elif isinstance(index, float):
|
124
|
+
for time in self._times:
|
125
|
+
if time.time == index:
|
126
|
+
return time
|
127
|
+
raise IndexError(f"Time {index} not found")
|
128
|
+
return self._times[index]
|
129
|
+
|
130
|
+
def __len__(self) -> int:
|
131
|
+
return len(self._times)
|
132
|
+
|
133
|
+
def __delitem__(self, key: Union[int, float, str]) -> None:
|
134
|
+
shutil.rmtree(self[key].path)
|
135
|
+
|
136
|
+
@property
|
137
|
+
def name(self) -> str:
|
138
|
+
"""The name of the case."""
|
139
|
+
return self.path.name
|
140
|
+
|
141
|
+
def file(self, path: Union["os.PathLike[str]", str]) -> FoamFile:
|
142
|
+
"""Return a FoamFile object for the given path in the case."""
|
143
|
+
return FoamFile(self.path / path)
|
144
|
+
|
145
|
+
@property
|
146
|
+
def _nsubdomains(self) -> Optional[int]:
|
147
|
+
"""Return the number of subdomains as set in the decomposeParDict, or None if no decomposeParDict is found."""
|
148
|
+
try:
|
149
|
+
nsubdomains = self.decompose_par_dict["numberOfSubdomains"]
|
150
|
+
if not isinstance(nsubdomains, int):
|
151
|
+
raise TypeError(
|
152
|
+
f"numberOfSubdomains in {self.decompose_par_dict} is not an integer"
|
153
|
+
)
|
154
|
+
return nsubdomains
|
155
|
+
except FileNotFoundError:
|
156
|
+
return None
|
157
|
+
|
158
|
+
@property
|
159
|
+
def _nprocessors(self) -> int:
|
160
|
+
"""Return the number of processor directories in the case."""
|
161
|
+
return len(list(self.path.glob("processor*")))
|
162
|
+
|
163
|
+
@property
|
164
|
+
def application(self) -> str:
|
165
|
+
"""The application name as set in the controlDict."""
|
166
|
+
application = self.control_dict["application"]
|
167
|
+
if not isinstance(application, str):
|
168
|
+
raise TypeError(f"application in {self.control_dict} is not a string")
|
169
|
+
return application
|
170
|
+
|
171
|
+
@property
|
172
|
+
def control_dict(self) -> FoamFile:
|
173
|
+
"""The controlDict file."""
|
174
|
+
return self.file("system/controlDict")
|
175
|
+
|
176
|
+
@property
|
177
|
+
def fv_schemes(self) -> FoamFile:
|
178
|
+
"""The fvSchemes file."""
|
179
|
+
return self.file("system/fvSchemes")
|
180
|
+
|
181
|
+
@property
|
182
|
+
def fv_solution(self) -> FoamFile:
|
183
|
+
"""The fvSolution file."""
|
184
|
+
return self.file("system/fvSolution")
|
185
|
+
|
186
|
+
@property
|
187
|
+
def decompose_par_dict(self) -> FoamFile:
|
188
|
+
"""The decomposeParDict file."""
|
189
|
+
return self.file("system/decomposeParDict")
|
190
|
+
|
191
|
+
@property
|
192
|
+
def block_mesh_dict(self) -> FoamFile:
|
193
|
+
"""The blockMeshDict file."""
|
194
|
+
return self.file("system/blockMeshDict")
|
195
|
+
|
196
|
+
@property
|
197
|
+
def transport_properties(self) -> FoamFile:
|
198
|
+
"""The transportProperties file."""
|
199
|
+
return self.file("constant/transportProperties")
|
200
|
+
|
201
|
+
@property
|
202
|
+
def turbulence_properties(self) -> FoamFile:
|
203
|
+
"""The turbulenceProperties file."""
|
204
|
+
return self.file("constant/turbulenceProperties")
|
205
|
+
|
206
|
+
def __fspath__(self) -> str:
|
207
|
+
return str(self.path)
|
208
|
+
|
209
|
+
def __repr__(self) -> str:
|
210
|
+
return f"{type(self).__qualname__}('{self.path}')"
|
211
|
+
|
212
|
+
def __str__(self) -> str:
|
213
|
+
return str(self.path)
|
@@ -1,13 +1,15 @@
|
|
1
1
|
import os
|
2
|
+
import shlex
|
2
3
|
import shutil
|
3
4
|
import sys
|
5
|
+
from contextlib import contextmanager
|
4
6
|
from pathlib import Path
|
5
7
|
from typing import (
|
8
|
+
IO,
|
6
9
|
Any,
|
7
10
|
Optional,
|
8
11
|
Tuple,
|
9
12
|
Union,
|
10
|
-
overload,
|
11
13
|
)
|
12
14
|
|
13
15
|
if sys.version_info >= (3, 9):
|
@@ -15,7 +17,6 @@ if sys.version_info >= (3, 9):
|
|
15
17
|
Callable,
|
16
18
|
Collection,
|
17
19
|
Generator,
|
18
|
-
Iterator,
|
19
20
|
Mapping,
|
20
21
|
Sequence,
|
21
22
|
Set,
|
@@ -26,121 +27,15 @@ else:
|
|
26
27
|
Callable,
|
27
28
|
Collection,
|
28
29
|
Generator,
|
29
|
-
Iterator,
|
30
30
|
Mapping,
|
31
31
|
Sequence,
|
32
32
|
)
|
33
33
|
|
34
|
+
from ._base import FoamCaseBase
|
35
|
+
from ._subprocess import DEVNULL, STDOUT
|
34
36
|
|
35
|
-
from .._files import FoamFieldFile, FoamFile
|
36
|
-
|
37
|
-
|
38
|
-
class FoamCaseBase(Sequence["FoamCaseBase.TimeDirectory"]):
|
39
|
-
def __init__(self, path: Union[Path, str, "FoamCaseBase"] = Path()):
|
40
|
-
self.path = Path(path).absolute()
|
41
|
-
|
42
|
-
class TimeDirectory(Set[FoamFieldFile]):
|
43
|
-
"""
|
44
|
-
An OpenFOAM time directory in a case.
|
45
|
-
|
46
|
-
Use to access field files in the directory, e.g. `time["U"]`.
|
47
|
-
|
48
|
-
:param path: The path to the time directory.
|
49
|
-
"""
|
50
|
-
|
51
|
-
def __init__(self, path: Union[Path, str]):
|
52
|
-
self.path = Path(path).absolute()
|
53
|
-
|
54
|
-
@property
|
55
|
-
def time(self) -> float:
|
56
|
-
"""The time that corresponds to this directory."""
|
57
|
-
return float(self.path.name)
|
58
|
-
|
59
|
-
@property
|
60
|
-
def name(self) -> str:
|
61
|
-
"""The name of this time directory."""
|
62
|
-
return self.path.name
|
63
|
-
|
64
|
-
def __getitem__(self, key: str) -> FoamFieldFile:
|
65
|
-
if (self.path / f"{key}.gz").is_file() and not (self.path / key).is_file():
|
66
|
-
return FoamFieldFile(self.path / f"{key}.gz")
|
67
|
-
else:
|
68
|
-
return FoamFieldFile(self.path / key)
|
69
|
-
|
70
|
-
def __contains__(self, obj: object) -> bool:
|
71
|
-
if isinstance(obj, FoamFieldFile):
|
72
|
-
return obj.path.parent == self.path
|
73
|
-
elif isinstance(obj, str):
|
74
|
-
return (self.path / obj).is_file() or (
|
75
|
-
self.path / f"{obj}.gz"
|
76
|
-
).is_file()
|
77
|
-
else:
|
78
|
-
return False
|
79
|
-
|
80
|
-
def __iter__(self) -> Iterator[FoamFieldFile]:
|
81
|
-
for p in self.path.iterdir():
|
82
|
-
if p.is_file() and (
|
83
|
-
p.suffix != ".gz" or not p.with_suffix("").is_file()
|
84
|
-
):
|
85
|
-
yield FoamFieldFile(p)
|
86
|
-
|
87
|
-
def __len__(self) -> int:
|
88
|
-
return len(list(iter(self)))
|
89
|
-
|
90
|
-
def __delitem__(self, key: str) -> None:
|
91
|
-
if (self.path / f"{key}.gz").is_file() and not (self.path / key).is_file():
|
92
|
-
(self.path / f"{key}.gz").unlink()
|
93
|
-
else:
|
94
|
-
(self.path / key).unlink()
|
95
|
-
|
96
|
-
def __fspath__(self) -> str:
|
97
|
-
return str(self.path)
|
98
|
-
|
99
|
-
def __repr__(self) -> str:
|
100
|
-
return f"{type(self).__qualname__}('{self.path}')"
|
101
|
-
|
102
|
-
def __str__(self) -> str:
|
103
|
-
return str(self.path)
|
104
|
-
|
105
|
-
@property
|
106
|
-
def _times(self) -> Sequence["FoamCaseBase.TimeDirectory"]:
|
107
|
-
times = []
|
108
|
-
for p in self.path.iterdir():
|
109
|
-
if p.is_dir():
|
110
|
-
try:
|
111
|
-
float(p.name)
|
112
|
-
except ValueError:
|
113
|
-
pass
|
114
|
-
else:
|
115
|
-
times.append(FoamCaseBase.TimeDirectory(p))
|
116
|
-
|
117
|
-
times.sort(key=lambda t: t.time)
|
118
|
-
|
119
|
-
return times
|
120
|
-
|
121
|
-
@overload
|
122
|
-
def __getitem__(
|
123
|
-
self, index: Union[int, float, str]
|
124
|
-
) -> "FoamCaseBase.TimeDirectory": ...
|
125
|
-
|
126
|
-
@overload
|
127
|
-
def __getitem__(self, index: slice) -> Sequence["FoamCaseBase.TimeDirectory"]: ...
|
128
|
-
|
129
|
-
def __getitem__(
|
130
|
-
self, index: Union[int, slice, float, str]
|
131
|
-
) -> Union["FoamCaseBase.TimeDirectory", Sequence["FoamCaseBase.TimeDirectory"]]:
|
132
|
-
if isinstance(index, str):
|
133
|
-
return FoamCaseBase.TimeDirectory(self.path / index)
|
134
|
-
elif isinstance(index, float):
|
135
|
-
for time in self._times:
|
136
|
-
if time.time == index:
|
137
|
-
return time
|
138
|
-
raise IndexError(f"Time {index} not found")
|
139
|
-
return self._times[index]
|
140
|
-
|
141
|
-
def __len__(self) -> int:
|
142
|
-
return len(self._times)
|
143
37
|
|
38
|
+
class _FoamCaseRecipes(FoamCaseBase):
|
144
39
|
def _clean_paths(self) -> Set[Path]:
|
145
40
|
has_decompose_par_dict = (self.path / "system" / "decomposeParDict").is_file()
|
146
41
|
has_block_mesh_dict = (self.path / "system" / "blockMeshDict").is_file()
|
@@ -176,10 +71,12 @@ class FoamCaseBase(Sequence["FoamCaseBase.TimeDirectory"]):
|
|
176
71
|
|
177
72
|
def _clone_ignore(
|
178
73
|
self,
|
179
|
-
) -> Callable[[Union[
|
74
|
+
) -> Callable[[Union["os.PathLike[str]", str], Collection[str]], Collection[str]]:
|
180
75
|
clean_paths = self._clean_paths()
|
181
76
|
|
182
|
-
def ignore(
|
77
|
+
def ignore(
|
78
|
+
path: Union["os.PathLike[str]", str], names: Collection[str]
|
79
|
+
) -> Collection[str]:
|
183
80
|
paths = {Path(path) / name for name in names}
|
184
81
|
return {p.name for p in paths.intersection(clean_paths)}
|
185
82
|
|
@@ -253,8 +150,26 @@ class FoamCaseBase(Sequence["FoamCaseBase.TimeDirectory"]):
|
|
253
150
|
else:
|
254
151
|
return None
|
255
152
|
|
153
|
+
@contextmanager
|
154
|
+
def _output(
|
155
|
+
self, cmd: Union[Sequence[Union[str, "os.PathLike[str]"]], str], *, log: bool
|
156
|
+
) -> Generator[Tuple[Union[int, IO[bytes]], Union[int, IO[bytes]]], None, None]:
|
157
|
+
if log:
|
158
|
+
if isinstance(cmd, str):
|
159
|
+
name = shlex.split(cmd)[0]
|
160
|
+
else:
|
161
|
+
if isinstance(cmd[0], os.PathLike):
|
162
|
+
name = Path(cmd[0]).name
|
163
|
+
else:
|
164
|
+
name = cmd[0]
|
165
|
+
|
166
|
+
with (self.path / f"log.{name}").open("ab") as stdout:
|
167
|
+
yield stdout, STDOUT
|
168
|
+
else:
|
169
|
+
yield DEVNULL, DEVNULL
|
170
|
+
|
256
171
|
def _copy_cmds(
|
257
|
-
self, dest: Union[
|
172
|
+
self, dest: Union["os.PathLike[str]", str]
|
258
173
|
) -> Generator[Tuple[str, Sequence[Any], Mapping[str, Any]], None, None]:
|
259
174
|
yield (
|
260
175
|
"_copytree",
|
@@ -271,7 +186,7 @@ class FoamCaseBase(Sequence["FoamCaseBase.TimeDirectory"]):
|
|
271
186
|
script_path = self._clean_script() if script else None
|
272
187
|
|
273
188
|
if script_path is not None:
|
274
|
-
yield ("_run", ([script_path],), {"cpus": 0, "check": check})
|
189
|
+
yield ("_run", ([script_path],), {"cpus": 0, "check": check, "log": False})
|
275
190
|
else:
|
276
191
|
for p in self._clean_paths():
|
277
192
|
if p.is_dir():
|
@@ -280,7 +195,7 @@ class FoamCaseBase(Sequence["FoamCaseBase.TimeDirectory"]):
|
|
280
195
|
p.unlink()
|
281
196
|
|
282
197
|
def _clone_cmds(
|
283
|
-
self, dest: Union[
|
198
|
+
self, dest: Union["os.PathLike[str]", str]
|
284
199
|
) -> Generator[Tuple[str, Sequence[Any], Mapping[str, Any]], None, None]:
|
285
200
|
if self._clean_script() is not None:
|
286
201
|
yield ("copy", (dest,), {})
|
@@ -310,7 +225,7 @@ class FoamCaseBase(Sequence["FoamCaseBase.TimeDirectory"]):
|
|
310
225
|
|
311
226
|
def _run_cmds(
|
312
227
|
self,
|
313
|
-
cmd: Optional[Union[Sequence[Union[str,
|
228
|
+
cmd: Optional[Union[Sequence[Union[str, "os.PathLike[str]"]], str]] = None,
|
314
229
|
*,
|
315
230
|
script: bool = True,
|
316
231
|
parallel: Optional[bool] = None,
|
@@ -382,82 +297,3 @@ class FoamCaseBase(Sequence["FoamCaseBase.TimeDirectory"]):
|
|
382
297
|
([self.application],),
|
383
298
|
{"parallel": parallel, "cpus": cpus, "check": check},
|
384
299
|
)
|
385
|
-
|
386
|
-
@property
|
387
|
-
def name(self) -> str:
|
388
|
-
"""The name of the case."""
|
389
|
-
return self.path.name
|
390
|
-
|
391
|
-
def file(self, path: Union[Path, str]) -> FoamFile:
|
392
|
-
"""Return a FoamFile object for the given path in the case."""
|
393
|
-
return FoamFile(self.path / path)
|
394
|
-
|
395
|
-
@property
|
396
|
-
def _nsubdomains(self) -> Optional[int]:
|
397
|
-
"""Return the number of subdomains as set in the decomposeParDict, or None if no decomposeParDict is found."""
|
398
|
-
try:
|
399
|
-
nsubdomains = self.decompose_par_dict["numberOfSubdomains"]
|
400
|
-
if not isinstance(nsubdomains, int):
|
401
|
-
raise TypeError(
|
402
|
-
f"numberOfSubdomains in {self.decompose_par_dict} is not an integer"
|
403
|
-
)
|
404
|
-
return nsubdomains
|
405
|
-
except FileNotFoundError:
|
406
|
-
return None
|
407
|
-
|
408
|
-
@property
|
409
|
-
def _nprocessors(self) -> int:
|
410
|
-
"""Return the number of processor directories in the case."""
|
411
|
-
return len(list(self.path.glob("processor*")))
|
412
|
-
|
413
|
-
@property
|
414
|
-
def application(self) -> str:
|
415
|
-
"""The application name as set in the controlDict."""
|
416
|
-
application = self.control_dict["application"]
|
417
|
-
if not isinstance(application, str):
|
418
|
-
raise TypeError(f"application in {self.control_dict} is not a string")
|
419
|
-
return application
|
420
|
-
|
421
|
-
@property
|
422
|
-
def control_dict(self) -> FoamFile:
|
423
|
-
"""The controlDict file."""
|
424
|
-
return self.file("system/controlDict")
|
425
|
-
|
426
|
-
@property
|
427
|
-
def fv_schemes(self) -> FoamFile:
|
428
|
-
"""The fvSchemes file."""
|
429
|
-
return self.file("system/fvSchemes")
|
430
|
-
|
431
|
-
@property
|
432
|
-
def fv_solution(self) -> FoamFile:
|
433
|
-
"""The fvSolution file."""
|
434
|
-
return self.file("system/fvSolution")
|
435
|
-
|
436
|
-
@property
|
437
|
-
def decompose_par_dict(self) -> FoamFile:
|
438
|
-
"""The decomposeParDict file."""
|
439
|
-
return self.file("system/decomposeParDict")
|
440
|
-
|
441
|
-
@property
|
442
|
-
def block_mesh_dict(self) -> FoamFile:
|
443
|
-
"""The blockMeshDict file."""
|
444
|
-
return self.file("system/blockMeshDict")
|
445
|
-
|
446
|
-
@property
|
447
|
-
def transport_properties(self) -> FoamFile:
|
448
|
-
"""The transportProperties file."""
|
449
|
-
return self.file("constant/transportProperties")
|
450
|
-
|
451
|
-
@property
|
452
|
-
def turbulence_properties(self) -> FoamFile:
|
453
|
-
"""The turbulenceProperties file."""
|
454
|
-
return self.file("constant/turbulenceProperties")
|
455
|
-
|
456
|
-
def __fspath__(self) -> str:
|
457
|
-
return str(self.path)
|
458
|
-
|
459
|
-
def __repr__(self) -> str:
|
460
|
-
return f"{type(self).__qualname__}('{self.path}')"
|
461
|
-
|
462
|
-
def __str__(self) -> str:
|
463
|
-
return str(self.path)
|
@@ -0,0 +1,86 @@
|
|
1
|
+
import asyncio
|
2
|
+
import os
|
3
|
+
import subprocess
|
4
|
+
import sys
|
5
|
+
from typing import IO, Optional, Union
|
6
|
+
|
7
|
+
if sys.version_info >= (3, 9):
|
8
|
+
from collections.abc import Mapping, Sequence
|
9
|
+
else:
|
10
|
+
from typing import Mapping, Sequence
|
11
|
+
|
12
|
+
CalledProcessError = subprocess.CalledProcessError
|
13
|
+
CompletedProcess = subprocess.CompletedProcess
|
14
|
+
|
15
|
+
DEVNULL = subprocess.DEVNULL
|
16
|
+
PIPE = subprocess.PIPE
|
17
|
+
STDOUT = subprocess.STDOUT
|
18
|
+
|
19
|
+
|
20
|
+
def run_sync(
|
21
|
+
cmd: Union[Sequence[Union[str, "os.PathLike[str]"]], str],
|
22
|
+
*,
|
23
|
+
check: bool = True,
|
24
|
+
cwd: Optional["os.PathLike[str]"] = None,
|
25
|
+
env: Optional[Mapping[str, str]] = None,
|
26
|
+
stdout: Optional[Union[int, IO[bytes]]] = None,
|
27
|
+
stderr: Optional[Union[int, IO[bytes]]] = None,
|
28
|
+
) -> "CompletedProcess[bytes]":
|
29
|
+
if not isinstance(cmd, str) and sys.version_info < (3, 8):
|
30
|
+
cmd = [str(arg) for arg in cmd]
|
31
|
+
|
32
|
+
return subprocess.run(
|
33
|
+
cmd,
|
34
|
+
cwd=cwd,
|
35
|
+
env=env,
|
36
|
+
stdout=stdout,
|
37
|
+
stderr=stderr,
|
38
|
+
shell=isinstance(cmd, str),
|
39
|
+
check=check,
|
40
|
+
)
|
41
|
+
|
42
|
+
|
43
|
+
async def run_async(
|
44
|
+
cmd: Union[Sequence[Union[str, "os.PathLike[str]"]], str],
|
45
|
+
*,
|
46
|
+
check: bool = True,
|
47
|
+
cwd: Optional["os.PathLike[str]"] = None,
|
48
|
+
env: Optional[Mapping[str, str]] = None,
|
49
|
+
stdout: Optional[Union[int, IO[bytes]]] = None,
|
50
|
+
stderr: Optional[Union[int, IO[bytes]]] = None,
|
51
|
+
) -> "CompletedProcess[bytes]":
|
52
|
+
if isinstance(cmd, str):
|
53
|
+
proc = await asyncio.create_subprocess_shell(
|
54
|
+
cmd,
|
55
|
+
cwd=cwd,
|
56
|
+
env=env,
|
57
|
+
stdout=stdout,
|
58
|
+
stderr=stderr,
|
59
|
+
)
|
60
|
+
|
61
|
+
else:
|
62
|
+
if sys.version_info < (3, 8):
|
63
|
+
cmd = [str(arg) for arg in cmd]
|
64
|
+
proc = await asyncio.create_subprocess_exec(
|
65
|
+
*cmd,
|
66
|
+
cwd=cwd,
|
67
|
+
env=env,
|
68
|
+
stdout=stdout,
|
69
|
+
stderr=stderr,
|
70
|
+
)
|
71
|
+
|
72
|
+
output, error = await proc.communicate()
|
73
|
+
|
74
|
+
assert proc.returncode is not None
|
75
|
+
|
76
|
+
if check and proc.returncode != 0:
|
77
|
+
raise CalledProcessError(
|
78
|
+
returncode=proc.returncode,
|
79
|
+
cmd=cmd,
|
80
|
+
output=output,
|
81
|
+
stderr=error,
|
82
|
+
)
|
83
|
+
|
84
|
+
return CompletedProcess(
|
85
|
+
cmd, returncode=proc.returncode, stdout=output, stderr=error
|
86
|
+
)
|
@@ -1,10 +1,13 @@
|
|
1
|
+
import os
|
1
2
|
import shutil
|
2
|
-
import subprocess
|
3
3
|
import sys
|
4
|
+
import tempfile
|
4
5
|
from pathlib import Path
|
6
|
+
from types import TracebackType
|
5
7
|
from typing import (
|
6
8
|
Callable,
|
7
9
|
Optional,
|
10
|
+
Type,
|
8
11
|
Union,
|
9
12
|
)
|
10
13
|
|
@@ -13,12 +16,16 @@ if sys.version_info >= (3, 9):
|
|
13
16
|
else:
|
14
17
|
from typing import Collection, Sequence
|
15
18
|
|
16
|
-
|
17
|
-
from
|
18
|
-
|
19
|
+
if sys.version_info >= (3, 11):
|
20
|
+
from typing import Self
|
21
|
+
else:
|
22
|
+
from typing_extensions import Self
|
23
|
+
|
24
|
+
from ._recipes import _FoamCaseRecipes
|
25
|
+
from ._subprocess import run_sync
|
19
26
|
|
20
27
|
|
21
|
-
class FoamCase(
|
28
|
+
class FoamCase(_FoamCaseRecipes):
|
22
29
|
"""
|
23
30
|
An OpenFOAM case.
|
24
31
|
|
@@ -30,21 +37,34 @@ class FoamCase(FoamCaseBase):
|
|
30
37
|
"""
|
31
38
|
|
32
39
|
@staticmethod
|
33
|
-
def _rmtree(
|
40
|
+
def _rmtree(
|
41
|
+
path: Union["os.PathLike[str]", str], *, ignore_errors: bool = False
|
42
|
+
) -> None:
|
34
43
|
shutil.rmtree(path, ignore_errors=ignore_errors)
|
35
44
|
|
36
45
|
@staticmethod
|
37
46
|
def _copytree(
|
38
|
-
src:
|
39
|
-
dest:
|
47
|
+
src: Union["os.PathLike[str]", str],
|
48
|
+
dest: Union["os.PathLike[str]", str],
|
40
49
|
*,
|
41
50
|
symlinks: bool = False,
|
42
51
|
ignore: Optional[
|
43
|
-
Callable[[Union[
|
52
|
+
Callable[[Union["os.PathLike[str]", str], Collection[str]], Collection[str]]
|
44
53
|
] = None,
|
45
54
|
) -> None:
|
46
55
|
shutil.copytree(src, dest, symlinks=symlinks, ignore=ignore)
|
47
56
|
|
57
|
+
def __enter__(self) -> "FoamCase":
|
58
|
+
return self
|
59
|
+
|
60
|
+
def __exit__(
|
61
|
+
self,
|
62
|
+
exc_type: Optional[Type[BaseException]],
|
63
|
+
exc_val: Optional[BaseException],
|
64
|
+
exc_tb: Optional[TracebackType],
|
65
|
+
) -> None:
|
66
|
+
self._rmtree(self.path)
|
67
|
+
|
48
68
|
def clean(
|
49
69
|
self,
|
50
70
|
*,
|
@@ -62,43 +82,39 @@ class FoamCase(FoamCaseBase):
|
|
62
82
|
|
63
83
|
def _run(
|
64
84
|
self,
|
65
|
-
cmd: Union[Sequence[Union[str,
|
85
|
+
cmd: Union[Sequence[Union[str, "os.PathLike[str]"]], str],
|
66
86
|
*,
|
67
87
|
parallel: bool = False,
|
68
88
|
cpus: int = 1,
|
69
89
|
check: bool = True,
|
90
|
+
log: bool = True,
|
70
91
|
) -> None:
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
shell=shell,
|
94
|
-
)
|
95
|
-
|
96
|
-
if check:
|
97
|
-
check_returncode(proc.returncode, cmd, proc.stderr)
|
92
|
+
with self._output(cmd, log=log) as (stdout, stderr):
|
93
|
+
if parallel:
|
94
|
+
if isinstance(cmd, str):
|
95
|
+
cmd = [
|
96
|
+
"mpiexec",
|
97
|
+
"-n",
|
98
|
+
str(cpus),
|
99
|
+
"/bin/sh",
|
100
|
+
"-c",
|
101
|
+
f"{cmd} -parallel",
|
102
|
+
]
|
103
|
+
else:
|
104
|
+
cmd = ["mpiexec", "-n", str(cpus), *cmd, "-parallel"]
|
105
|
+
|
106
|
+
run_sync(
|
107
|
+
cmd,
|
108
|
+
check=check,
|
109
|
+
cwd=self.path,
|
110
|
+
env=self._env(shell=isinstance(cmd, str)),
|
111
|
+
stdout=stdout,
|
112
|
+
stderr=stderr,
|
113
|
+
)
|
98
114
|
|
99
115
|
def run(
|
100
116
|
self,
|
101
|
-
cmd: Optional[Union[Sequence[Union[str,
|
117
|
+
cmd: Optional[Union[Sequence[Union[str, "os.PathLike[str]"]], str]] = None,
|
102
118
|
*,
|
103
119
|
script: bool = True,
|
104
120
|
parallel: Optional[bool] = None,
|
@@ -134,24 +150,34 @@ class FoamCase(FoamCaseBase):
|
|
134
150
|
for name, args, kwargs in self._restore_0_dir_cmds():
|
135
151
|
getattr(self, name)(*args, **kwargs)
|
136
152
|
|
137
|
-
def copy(self,
|
153
|
+
def copy(self, dst: Optional[Union["os.PathLike[str]", str]] = None) -> "Self":
|
138
154
|
"""
|
139
155
|
Make a copy of this case.
|
140
156
|
|
141
|
-
|
157
|
+
Use as a context manager to automatically delete the copy when done.
|
158
|
+
|
159
|
+
:param dst: The destination path. If None, copy to a temporary directory.
|
142
160
|
"""
|
143
|
-
|
161
|
+
if dst is None:
|
162
|
+
dst = Path(tempfile.mkdtemp(), self.name)
|
163
|
+
|
164
|
+
for name, args, kwargs in self._copy_cmds(dst):
|
144
165
|
getattr(self, name)(*args, **kwargs)
|
145
166
|
|
146
|
-
return
|
167
|
+
return type(self)(dst)
|
147
168
|
|
148
|
-
def clone(self,
|
169
|
+
def clone(self, dst: Optional[Union["os.PathLike[str]", str]] = None) -> "Self":
|
149
170
|
"""
|
150
171
|
Clone this case (make a clean copy).
|
151
172
|
|
152
|
-
|
173
|
+
Use as a context manager to automatically delete the clone when done.
|
174
|
+
|
175
|
+
:param dst: The destination path. If None, clone to a temporary directory.
|
153
176
|
"""
|
154
|
-
|
177
|
+
if dst is None:
|
178
|
+
dst = Path(tempfile.mkdtemp(), self.name)
|
179
|
+
|
180
|
+
for name, args, kwargs in self._clone_cmds(dst):
|
155
181
|
getattr(self, name)(*args, **kwargs)
|
156
182
|
|
157
|
-
return
|
183
|
+
return type(self)(dst)
|
@@ -0,0 +1,27 @@
|
|
1
|
+
from types import TracebackType
|
2
|
+
from typing import Any, AsyncContextManager, Callable, Optional, Type
|
3
|
+
|
4
|
+
|
5
|
+
class _AwaitableAsyncContextManager:
|
6
|
+
def __init__(self, cm: "AsyncContextManager[Any]"):
|
7
|
+
self._cm = cm
|
8
|
+
|
9
|
+
def __await__(self) -> Any:
|
10
|
+
return self._cm.__aenter__().__await__()
|
11
|
+
|
12
|
+
async def __aenter__(self) -> Any:
|
13
|
+
return await self._cm.__aenter__()
|
14
|
+
|
15
|
+
async def __aexit__(
|
16
|
+
self,
|
17
|
+
exc_type: Optional[Type[BaseException]],
|
18
|
+
exc_val: Optional[BaseException],
|
19
|
+
exc_tb: Optional[TracebackType],
|
20
|
+
) -> Any:
|
21
|
+
return await self._cm.__aexit__(exc_type, exc_val, exc_tb)
|
22
|
+
|
23
|
+
|
24
|
+
def awaitableasynccontextmanager(
|
25
|
+
cm: Callable[..., "AsyncContextManager[Any]"],
|
26
|
+
) -> Callable[..., _AwaitableAsyncContextManager]:
|
27
|
+
return lambda *args, **kwargs: _AwaitableAsyncContextManager(cm(*args, **kwargs))
|
@@ -16,10 +16,10 @@ if sys.version_info >= (3, 11):
|
|
16
16
|
else:
|
17
17
|
from typing_extensions import Self
|
18
18
|
|
19
|
-
from .._util import is_sequence
|
20
19
|
from ._base import FoamFileBase
|
21
20
|
from ._io import _FoamFileIO
|
22
21
|
from ._serialization import Kind, dumpb
|
22
|
+
from ._util import is_sequence
|
23
23
|
|
24
24
|
try:
|
25
25
|
import numpy as np
|
@@ -105,13 +105,9 @@ class FoamFile(
|
|
105
105
|
"""
|
106
106
|
Create the file.
|
107
107
|
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
If False (the default), raise a FileExistsError if the file already exists.
|
112
|
-
If True, do nothing if the file already exists.
|
113
|
-
parents : bool, optional
|
114
|
-
If True, also create parent directories as needed.
|
108
|
+
:param exist_ok: If False, raise a FileExistsError if the file already exists.
|
109
|
+
|
110
|
+
:param parents: If True, also create parent directories as needed.
|
115
111
|
"""
|
116
112
|
if self.path.exists():
|
117
113
|
if not exist_ok:
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: foamlib
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.5.0
|
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
|
@@ -19,6 +19,7 @@ Classifier: Programming Language :: Python :: 3.9
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.10
|
20
20
|
Classifier: Programming Language :: Python :: 3.11
|
21
21
|
Classifier: Programming Language :: Python :: 3.12
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
22
23
|
Classifier: Topic :: Scientific/Engineering
|
23
24
|
Classifier: Topic :: Software Development
|
24
25
|
Classifier: Typing :: Typed
|
@@ -123,6 +124,14 @@ my_pitz.clean()
|
|
123
124
|
my_pitz.control_dict["writeInterval"] = 10
|
124
125
|
```
|
125
126
|
|
127
|
+
### Make multiple file reads and writes in a single go
|
128
|
+
|
129
|
+
```python
|
130
|
+
with my_pitz.fv_schemes as f:
|
131
|
+
f["gradSchemes"]["default"] = f["divSchemes"]["default"]
|
132
|
+
f["snGradSchemes"]["default"] = "uncorrected"
|
133
|
+
```
|
134
|
+
|
126
135
|
### Run a case asynchronously
|
127
136
|
|
128
137
|
```python
|
@@ -2,7 +2,6 @@ LICENSE.txt
|
|
2
2
|
README.md
|
3
3
|
pyproject.toml
|
4
4
|
foamlib/__init__.py
|
5
|
-
foamlib/_util.py
|
6
5
|
foamlib/py.typed
|
7
6
|
foamlib.egg-info/PKG-INFO
|
8
7
|
foamlib.egg-info/SOURCES.txt
|
@@ -12,6 +11,8 @@ foamlib.egg-info/top_level.txt
|
|
12
11
|
foamlib/_cases/__init__.py
|
13
12
|
foamlib/_cases/_async.py
|
14
13
|
foamlib/_cases/_base.py
|
14
|
+
foamlib/_cases/_recipes.py
|
15
|
+
foamlib/_cases/_subprocess.py
|
15
16
|
foamlib/_cases/_sync.py
|
16
17
|
foamlib/_cases/_util.py
|
17
18
|
foamlib/_files/__init__.py
|
@@ -19,4 +20,5 @@ foamlib/_files/_base.py
|
|
19
20
|
foamlib/_files/_files.py
|
20
21
|
foamlib/_files/_io.py
|
21
22
|
foamlib/_files/_parsing.py
|
22
|
-
foamlib/_files/_serialization.py
|
23
|
+
foamlib/_files/_serialization.py
|
24
|
+
foamlib/_files/_util.py
|
@@ -22,6 +22,7 @@ classifiers = [
|
|
22
22
|
"Programming Language :: Python :: 3.10",
|
23
23
|
"Programming Language :: Python :: 3.11",
|
24
24
|
"Programming Language :: Python :: 3.12",
|
25
|
+
"Programming Language :: Python :: 3.13",
|
25
26
|
"Topic :: Scientific/Engineering",
|
26
27
|
"Topic :: Software Development",
|
27
28
|
"Typing :: Typed",
|
@@ -1,35 +0,0 @@
|
|
1
|
-
import subprocess
|
2
|
-
import sys
|
3
|
-
from pathlib import Path
|
4
|
-
from typing import Optional, Union
|
5
|
-
from warnings import warn
|
6
|
-
|
7
|
-
if sys.version_info >= (3, 9):
|
8
|
-
from collections.abc import Sequence
|
9
|
-
else:
|
10
|
-
from typing import Sequence
|
11
|
-
|
12
|
-
|
13
|
-
class CalledProcessError(subprocess.CalledProcessError):
|
14
|
-
"""Exception raised when a process fails and `check=True`."""
|
15
|
-
|
16
|
-
def __str__(self) -> str:
|
17
|
-
msg = super().__str__()
|
18
|
-
if self.stderr:
|
19
|
-
msg += f"\n{self.stderr}"
|
20
|
-
return msg
|
21
|
-
|
22
|
-
|
23
|
-
class CalledProcessWarning(Warning):
|
24
|
-
"""Warning raised when a process prints to stderr and `check=True`."""
|
25
|
-
|
26
|
-
|
27
|
-
def check_returncode(
|
28
|
-
retcode: int,
|
29
|
-
cmd: Union[Sequence[Union[str, Path]], str, Path],
|
30
|
-
stderr: Optional[str],
|
31
|
-
) -> None:
|
32
|
-
if retcode != 0:
|
33
|
-
raise CalledProcessError(retcode, cmd, None, stderr)
|
34
|
-
elif stderr:
|
35
|
-
warn(f"Command {cmd} printed to stderr.\n{stderr}", CalledProcessWarning)
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|