foamlib 0.6.15__tar.gz → 0.7.1__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.
Files changed (28) hide show
  1. {foamlib-0.6.15 → foamlib-0.7.1}/PKG-INFO +12 -6
  2. {foamlib-0.6.15 → foamlib-0.7.1}/README.md +11 -5
  3. {foamlib-0.6.15 → foamlib-0.7.1}/foamlib/__init__.py +2 -3
  4. {foamlib-0.6.15 → foamlib-0.7.1}/foamlib/_files/__init__.py +0 -2
  5. {foamlib-0.6.15 → foamlib-0.7.1}/foamlib/_files/_files.py +34 -27
  6. {foamlib-0.6.15 → foamlib-0.7.1}/foamlib/_files/_parsing.py +95 -54
  7. {foamlib-0.6.15 → foamlib-0.7.1}/foamlib/_files/_serialization.py +22 -56
  8. foamlib-0.7.1/foamlib/_files/_types.py +78 -0
  9. {foamlib-0.6.15 → foamlib-0.7.1}/foamlib/_files/_util.py +3 -3
  10. {foamlib-0.6.15 → foamlib-0.7.1}/foamlib.egg-info/PKG-INFO +12 -6
  11. {foamlib-0.6.15 → foamlib-0.7.1}/foamlib.egg-info/SOURCES.txt +1 -1
  12. foamlib-0.6.15/foamlib/_files/_base.py +0 -76
  13. {foamlib-0.6.15 → foamlib-0.7.1}/LICENSE.txt +0 -0
  14. {foamlib-0.6.15 → foamlib-0.7.1}/foamlib/_cases/__init__.py +0 -0
  15. {foamlib-0.6.15 → foamlib-0.7.1}/foamlib/_cases/_async.py +0 -0
  16. {foamlib-0.6.15 → foamlib-0.7.1}/foamlib/_cases/_base.py +0 -0
  17. {foamlib-0.6.15 → foamlib-0.7.1}/foamlib/_cases/_run.py +0 -0
  18. {foamlib-0.6.15 → foamlib-0.7.1}/foamlib/_cases/_slurm.py +0 -0
  19. {foamlib-0.6.15 → foamlib-0.7.1}/foamlib/_cases/_subprocess.py +0 -0
  20. {foamlib-0.6.15 → foamlib-0.7.1}/foamlib/_cases/_sync.py +0 -0
  21. {foamlib-0.6.15 → foamlib-0.7.1}/foamlib/_cases/_util.py +0 -0
  22. {foamlib-0.6.15 → foamlib-0.7.1}/foamlib/_files/_io.py +0 -0
  23. {foamlib-0.6.15 → foamlib-0.7.1}/foamlib/py.typed +0 -0
  24. {foamlib-0.6.15 → foamlib-0.7.1}/foamlib.egg-info/dependency_links.txt +0 -0
  25. {foamlib-0.6.15 → foamlib-0.7.1}/foamlib.egg-info/requires.txt +0 -0
  26. {foamlib-0.6.15 → foamlib-0.7.1}/foamlib.egg-info/top_level.txt +0 -0
  27. {foamlib-0.6.15 → foamlib-0.7.1}/pyproject.toml +0 -0
  28. {foamlib-0.6.15 → foamlib-0.7.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: foamlib
3
- Version: 0.6.15
3
+ Version: 0.7.1
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
@@ -69,13 +69,19 @@ Requires-Dist: foamlib[docs]; extra == "dev"
69
69
  [![Docker](https://github.com/gerlero/foamlib/actions/workflows/docker.yml/badge.svg)](https://github.com/gerlero/foamlib/actions/workflows/docker.yml)
70
70
  [![Docker image](https://img.shields.io/badge/docker%20image-microfluidica%2Ffoamlib-0085a0)](https://hub.docker.com/r/microfluidica/foamlib/)
71
71
 
72
- ## 👋 Basics
72
+ **foamlib** provides a simple, modern, ergonomic and fast Python interface for interacting with [OpenFOAM](https://www.openfoam.com).
73
+
74
+ <p align="center">
75
+ <img alt="benchmark" src="https://github.com/gerlero/foamlib/raw/main/benchmark.png" height="250">
76
+ <br>
77
+ <i>Parsing a </i>volVectorField<i> with 200k cells.</i>
78
+ </p>
73
79
 
74
- **foamlib** provides a simple, modern and ergonomic Python interface for interacting with [OpenFOAM](https://www.openfoam.com).
80
+ ## 👋 Basics
75
81
 
76
- It offers the following Python classes:
82
+ **foamlib** offers the following Python classes:
77
83
 
78
- * [`FoamFile`](https://foamlib.readthedocs.io/en/stable/files.html#foamlib.FoamFile) (and [`FoamFieldFile`](https://foamlib.readthedocs.io/en/stable/files.html#foamlib.FoamFieldFile)): read-write access to OpenFOAM configuration and field files as if they were Python `dict`s, using `foamlib`'s own parser. Supports ASCII and binary field formats (with or without compression).
84
+ * [`FoamFile`](https://foamlib.readthedocs.io/en/stable/files.html#foamlib.FoamFile) (and [`FoamFieldFile`](https://foamlib.readthedocs.io/en/stable/files.html#foamlib.FoamFieldFile)): read-write access to OpenFOAM configuration and field files as if they were Python `dict`s, using `foamlib`'s own parser and in-place editor. Supports ASCII and binary field formats (with or without compression).
79
85
  * [`FoamCase`](https://foamlib.readthedocs.io/en/stable/cases.html#foamlib.FoamCase): a class for configuring, running, and accessing the results of OpenFOAM cases.
80
86
  * [`AsyncFoamCase`](https://foamlib.readthedocs.io/en/stable/cases.html#foamlib.AsyncFoamCase): variant of `FoamCase` with asynchronous methods for running multiple cases at once.
81
87
  * [`AsyncSlurmFoamCase`](https://foamlib.readthedocs.io/en/stable/cases.html#foamlib.AsyncSlurmFoamCase): subclass of `AsyncFoamCase` used for running cases on a Slurm cluster.
@@ -185,7 +191,7 @@ async def cost(x):
185
191
  await clone.run(fallback=True) # Run locally if Slurm is not available
186
192
  return abs(clone[-1]["U"].internal_field[0][0])
187
193
 
188
- result = differential_evolution(cost, bounds=[(-1, 1)], workers=AsyncFoamCase.map, polish=False)
194
+ result = differential_evolution(cost, bounds=[(-1, 1)], workers=AsyncSlurmFoamCase.map, polish=False)
189
195
  ```
190
196
 
191
197
  ### 📄 Use it to create a `run` (or `clean`) script
@@ -14,13 +14,19 @@
14
14
  [![Docker](https://github.com/gerlero/foamlib/actions/workflows/docker.yml/badge.svg)](https://github.com/gerlero/foamlib/actions/workflows/docker.yml)
15
15
  [![Docker image](https://img.shields.io/badge/docker%20image-microfluidica%2Ffoamlib-0085a0)](https://hub.docker.com/r/microfluidica/foamlib/)
16
16
 
17
- ## 👋 Basics
17
+ **foamlib** provides a simple, modern, ergonomic and fast Python interface for interacting with [OpenFOAM](https://www.openfoam.com).
18
+
19
+ <p align="center">
20
+ <img alt="benchmark" src="https://github.com/gerlero/foamlib/raw/main/benchmark.png" height="250">
21
+ <br>
22
+ <i>Parsing a </i>volVectorField<i> with 200k cells.</i>
23
+ </p>
18
24
 
19
- **foamlib** provides a simple, modern and ergonomic Python interface for interacting with [OpenFOAM](https://www.openfoam.com).
25
+ ## 👋 Basics
20
26
 
21
- It offers the following Python classes:
27
+ **foamlib** offers the following Python classes:
22
28
 
23
- * [`FoamFile`](https://foamlib.readthedocs.io/en/stable/files.html#foamlib.FoamFile) (and [`FoamFieldFile`](https://foamlib.readthedocs.io/en/stable/files.html#foamlib.FoamFieldFile)): read-write access to OpenFOAM configuration and field files as if they were Python `dict`s, using `foamlib`'s own parser. Supports ASCII and binary field formats (with or without compression).
29
+ * [`FoamFile`](https://foamlib.readthedocs.io/en/stable/files.html#foamlib.FoamFile) (and [`FoamFieldFile`](https://foamlib.readthedocs.io/en/stable/files.html#foamlib.FoamFieldFile)): read-write access to OpenFOAM configuration and field files as if they were Python `dict`s, using `foamlib`'s own parser and in-place editor. Supports ASCII and binary field formats (with or without compression).
24
30
  * [`FoamCase`](https://foamlib.readthedocs.io/en/stable/cases.html#foamlib.FoamCase): a class for configuring, running, and accessing the results of OpenFOAM cases.
25
31
  * [`AsyncFoamCase`](https://foamlib.readthedocs.io/en/stable/cases.html#foamlib.AsyncFoamCase): variant of `FoamCase` with asynchronous methods for running multiple cases at once.
26
32
  * [`AsyncSlurmFoamCase`](https://foamlib.readthedocs.io/en/stable/cases.html#foamlib.AsyncSlurmFoamCase): subclass of `AsyncFoamCase` used for running cases on a Slurm cluster.
@@ -130,7 +136,7 @@ async def cost(x):
130
136
  await clone.run(fallback=True) # Run locally if Slurm is not available
131
137
  return abs(clone[-1]["U"].internal_field[0][0])
132
138
 
133
- result = differential_evolution(cost, bounds=[(-1, 1)], workers=AsyncFoamCase.map, polish=False)
139
+ result = differential_evolution(cost, bounds=[(-1, 1)], workers=AsyncSlurmFoamCase.map, polish=False)
134
140
  ```
135
141
 
136
142
  ### 📄 Use it to create a `run` (or `clean`) script
@@ -1,6 +1,6 @@
1
1
  """A Python interface for interacting with OpenFOAM."""
2
2
 
3
- __version__ = "0.6.15"
3
+ __version__ = "0.7.1"
4
4
 
5
5
  from ._cases import (
6
6
  AsyncFoamCase,
@@ -10,7 +10,7 @@ from ._cases import (
10
10
  FoamCaseBase,
11
11
  FoamCaseRunBase,
12
12
  )
13
- from ._files import FoamFieldFile, FoamFile, FoamFileBase
13
+ from ._files import FoamFieldFile, FoamFile
14
14
 
15
15
  __all__ = [
16
16
  "AsyncFoamCase",
@@ -21,5 +21,4 @@ __all__ = [
21
21
  "FoamCaseBase",
22
22
  "FoamCaseRunBase",
23
23
  "FoamFieldFile",
24
- "FoamFileBase",
25
24
  ]
@@ -1,8 +1,6 @@
1
- from ._base import FoamFileBase
2
1
  from ._files import FoamFieldFile, FoamFile
3
2
 
4
3
  __all__ = [
5
4
  "FoamFile",
6
5
  "FoamFieldFile",
7
- "FoamFileBase",
8
6
  ]
@@ -15,17 +15,25 @@ if sys.version_info >= (3, 9):
15
15
  else:
16
16
  from typing import Iterator, Mapping, MutableMapping, Sequence
17
17
 
18
- from ._base import FoamFileBase
19
18
  from ._io import FoamFileIO
20
19
  from ._serialization import Kind, dumps, normalize
20
+ from ._types import (
21
+ Data,
22
+ DataEntry,
23
+ Dict_,
24
+ Dimensioned,
25
+ DimensionSet,
26
+ Field,
27
+ File,
28
+ MutableData,
29
+ )
21
30
  from ._util import is_sequence
22
31
 
23
32
 
24
33
  class FoamFile(
25
- FoamFileBase,
26
34
  MutableMapping[
27
35
  Optional[Union[str, Tuple[str, ...]]],
28
- FoamFileBase._MutableData,
36
+ MutableData,
29
37
  ],
30
38
  FoamFileIO,
31
39
  ):
@@ -37,8 +45,11 @@ class FoamFile(
37
45
  Use as a context manager to make multiple changes to the file while saving all changes only once at the end.
38
46
  """
39
47
 
48
+ Dimensioned = Dimensioned
49
+ DimensionSet = DimensionSet
50
+
40
51
  class SubDict(
41
- MutableMapping[str, FoamFileBase._MutableData],
52
+ MutableMapping[str, MutableData],
42
53
  ):
43
54
  """An OpenFOAM dictionary within a file as a mutable mapping."""
44
55
 
@@ -46,15 +57,13 @@ class FoamFile(
46
57
  self._file = _file
47
58
  self._keywords = _keywords
48
59
 
49
- def __getitem__(
50
- self, keyword: str
51
- ) -> FoamFileBase._DataEntry | FoamFile.SubDict:
60
+ def __getitem__(self, keyword: str) -> DataEntry | FoamFile.SubDict:
52
61
  return self._file[(*self._keywords, keyword)]
53
62
 
54
63
  def __setitem__(
55
64
  self,
56
65
  keyword: str,
57
- data: FoamFileBase.Data,
66
+ data: Data,
58
67
  ) -> None:
59
68
  self._file[(*self._keywords, keyword)] = data
60
69
 
@@ -83,7 +92,7 @@ class FoamFile(
83
92
  def __repr__(self) -> str:
84
93
  return f"{type(self).__qualname__}('{self._file}', {self._keywords})"
85
94
 
86
- def as_dict(self) -> FoamFileBase._Dict:
95
+ def as_dict(self) -> Dict_:
87
96
  """Return a nested dict representation of the dictionary."""
88
97
  ret = self._file.as_dict(include_header=True)
89
98
 
@@ -91,9 +100,9 @@ class FoamFile(
91
100
  assert isinstance(ret, dict)
92
101
  v = ret[k]
93
102
  assert isinstance(v, dict)
94
- ret = cast(FoamFileBase._File, v)
103
+ ret = cast(File, v)
95
104
 
96
- return cast(FoamFileBase._Dict, ret)
105
+ return cast(Dict_, ret)
97
106
 
98
107
  @property
99
108
  def version(self) -> float:
@@ -165,7 +174,7 @@ class FoamFile(
165
174
 
166
175
  def __getitem__(
167
176
  self, keywords: str | tuple[str, ...] | None
168
- ) -> FoamFileBase._DataEntry | FoamFile.SubDict:
177
+ ) -> DataEntry | FoamFile.SubDict:
169
178
  if not keywords:
170
179
  keywords = ()
171
180
  elif not isinstance(keywords, tuple):
@@ -181,9 +190,7 @@ class FoamFile(
181
190
  return FoamFile.SubDict(self, keywords)
182
191
  return deepcopy(value)
183
192
 
184
- def __setitem__(
185
- self, keywords: str | tuple[str, ...] | None, data: FoamFileBase.Data
186
- ) -> None:
193
+ def __setitem__(self, keywords: str | tuple[str, ...] | None, data: Data) -> None:
187
194
  if not keywords:
188
195
  keywords = ()
189
196
  elif not isinstance(keywords, tuple):
@@ -368,7 +375,7 @@ class FoamFile(
368
375
  def __fspath__(self) -> str:
369
376
  return str(self.path)
370
377
 
371
- def as_dict(self, *, include_header: bool = False) -> FoamFileBase._File:
378
+ def as_dict(self, *, include_header: bool = False) -> File:
372
379
  """
373
380
  Return a nested dict representation of the file.
374
381
 
@@ -411,17 +418,17 @@ class FoamFieldFile(FoamFile):
411
418
  @property
412
419
  def value(
413
420
  self,
414
- ) -> FoamFile._Field:
421
+ ) -> Field:
415
422
  """Alias of `self["value"]`."""
416
423
  return cast(
417
- FoamFile._Field,
424
+ Field,
418
425
  self["value"],
419
426
  )
420
427
 
421
428
  @value.setter
422
429
  def value(
423
430
  self,
424
- value: FoamFile._Field,
431
+ value: Field,
425
432
  ) -> None:
426
433
  self["value"] = value
427
434
 
@@ -431,7 +438,7 @@ class FoamFieldFile(FoamFile):
431
438
 
432
439
  def __getitem__(
433
440
  self, keywords: str | tuple[str, ...] | None
434
- ) -> FoamFileBase._DataEntry | FoamFile.SubDict:
441
+ ) -> DataEntry | FoamFile.SubDict:
435
442
  if not keywords:
436
443
  keywords = ()
437
444
  elif not isinstance(keywords, tuple):
@@ -446,29 +453,29 @@ class FoamFieldFile(FoamFile):
446
453
  return ret
447
454
 
448
455
  @property
449
- def dimensions(self) -> FoamFile.DimensionSet | Sequence[float]:
456
+ def dimensions(self) -> DimensionSet | Sequence[float]:
450
457
  """Alias of `self["dimensions"]`."""
451
458
  ret = self["dimensions"]
452
- if not isinstance(ret, FoamFile.DimensionSet):
459
+ if not isinstance(ret, DimensionSet):
453
460
  msg = "dimensions is not a DimensionSet"
454
461
  raise TypeError(msg)
455
462
  return ret
456
463
 
457
464
  @dimensions.setter
458
- def dimensions(self, value: FoamFile.DimensionSet | Sequence[float]) -> None:
465
+ def dimensions(self, value: DimensionSet | Sequence[float]) -> None:
459
466
  self["dimensions"] = value
460
467
 
461
468
  @property
462
469
  def internal_field(
463
470
  self,
464
- ) -> FoamFile._Field:
471
+ ) -> Field:
465
472
  """Alias of `self["internalField"]`."""
466
- return cast(FoamFile._Field, self["internalField"])
473
+ return cast(Field, self["internalField"])
467
474
 
468
475
  @internal_field.setter
469
476
  def internal_field(
470
477
  self,
471
- value: FoamFile._Field,
478
+ value: Field,
472
479
  ) -> None:
473
480
  self["internalField"] = value
474
481
 
@@ -483,5 +490,5 @@ class FoamFieldFile(FoamFile):
483
490
  return ret
484
491
 
485
492
  @boundary_field.setter
486
- def boundary_field(self, value: Mapping[str, FoamFile._Dict]) -> None:
493
+ def boundary_field(self, value: Mapping[str, Dict_]) -> None:
487
494
  self["boundaryField"] = value
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import array
4
+ import re
4
5
  import sys
5
6
  from typing import Tuple, Union, cast
6
7
 
@@ -28,15 +29,15 @@ from pyparsing import (
28
29
  ParserElement,
29
30
  ParseResults,
30
31
  QuotedString,
32
+ Regex,
31
33
  Word,
32
34
  common,
33
35
  counted_array,
34
- cpp_style_comment,
35
36
  identchars,
36
37
  printables,
37
38
  )
38
39
 
39
- from ._base import FoamFileBase
40
+ from ._types import DataEntry, Dimensioned, DimensionSet, File
40
41
 
41
42
 
42
43
  def _list_of(entry: ParserElement) -> ParserElement:
@@ -56,6 +57,59 @@ def _list_of(entry: ParserElement) -> ParserElement:
56
57
  )
57
58
 
58
59
 
60
+ def _counted_tensor_list(*, size: int, ignore: Regex) -> ParserElement:
61
+ float_pattern = r"[+-]?((\d+\.?\d*(e[+-]?\d+)?)|nan|inf(inity)?)"
62
+ ignore_pattern = rf"(?:{ignore.re.pattern}|\s)+"
63
+
64
+ if size == 1:
65
+ tensor_pattern = float_pattern
66
+ tensor = common.ieee_float
67
+ else:
68
+ tensor_pattern = rf"\((?:{ignore_pattern})?(?:{float_pattern}{ignore_pattern}){{{size - 1}}}{float_pattern}(?:{ignore_pattern})?\)"
69
+ tensor = (
70
+ Literal("(").suppress()
71
+ + Group(common.ieee_float[size], aslist=True)
72
+ + Literal(")").suppress()
73
+ )
74
+
75
+ list_ = Forward()
76
+
77
+ def count_parse_action(tks: ParseResults) -> None:
78
+ nonlocal list_
79
+ length = tks[0]
80
+ assert isinstance(length, int)
81
+
82
+ list_ <<= Regex(
83
+ rf"\((?:{ignore_pattern})?(?:{tensor_pattern}{ignore_pattern}){{{length - 1}}}{tensor_pattern}(?:{ignore_pattern})?\)",
84
+ re.IGNORECASE,
85
+ )
86
+
87
+ count = common.integer.add_parse_action(count_parse_action)
88
+
89
+ def list_parse_action(
90
+ tks: ParseResults,
91
+ ) -> list[list[float]] | list[list[list[float]]]:
92
+ values = (
93
+ re.sub(ignore.re, " ", tks[0]).replace("(", " ").replace(")", " ").split()
94
+ )
95
+
96
+ if size == 1:
97
+ return [[float(v) for v in values]]
98
+
99
+ return [
100
+ [
101
+ [float(v) for v in values[i : i + size]]
102
+ for i in range(0, len(values), size)
103
+ ]
104
+ ]
105
+
106
+ list_.add_parse_action(list_parse_action)
107
+
108
+ return (count.suppress() + list_) | (
109
+ common.integer + Literal("{").suppress() + tensor + Literal("}").suppress()
110
+ ).set_parse_action(lambda tks: [[tks[1]] * tks[0]])
111
+
112
+
59
113
  def _keyword_entry_of(
60
114
  keyword: ParserElement,
61
115
  data_entries: ParserElement,
@@ -96,6 +150,9 @@ def _unpack_binary_field(
96
150
  return [values]
97
151
 
98
152
 
153
+ # https://github.com/pyparsing/pyparsing/pull/584
154
+ _COMMENT = Regex(r"(?:/\*(?:[^*]|\*(?!/))*\*/)|(?://(?:\\\n|[^\n])*)")
155
+
99
156
  _IDENTCHARS = identchars + "$"
100
157
  _IDENTBODYCHARS = (
101
158
  printables.replace(";", "")
@@ -120,11 +177,11 @@ _SWITCH = (
120
177
  ).set_parse_action(lambda: False)
121
178
  _DIMENSIONS = (
122
179
  Literal("[").suppress() + common.number[0, 7] + Literal("]").suppress()
123
- ).set_parse_action(lambda tks: FoamFileBase.DimensionSet(*tks))
180
+ ).set_parse_action(lambda tks: DimensionSet(*tks))
124
181
  _TENSOR = common.ieee_float | (
125
182
  Literal("(").suppress()
126
183
  + Group(
127
- common.ieee_float[9] | common.ieee_float[6] | common.ieee_float[3], aslist=True
184
+ common.ieee_float[3] | common.ieee_float[6] | common.ieee_float[9], aslist=True
128
185
  )
129
186
  + Literal(")").suppress()
130
187
  )
@@ -133,7 +190,7 @@ _IDENTIFIER = Combine(
133
190
  + Opt(Literal("(") + Word(_IDENTBODYCHARS, exclude_chars="()") + Literal(")"))
134
191
  )
135
192
  _DIMENSIONED = (Opt(_IDENTIFIER) + _DIMENSIONS + _TENSOR).set_parse_action(
136
- lambda tks: FoamFileBase.Dimensioned(*reversed(tks.as_list()))
193
+ lambda tks: Dimensioned(*reversed(tks.as_list()))
137
194
  )
138
195
  _FIELD = (Keyword("uniform", _IDENTBODYCHARS).suppress() + _TENSOR) | (
139
196
  Keyword("nonuniform", _IDENTBODYCHARS).suppress()
@@ -145,7 +202,7 @@ _FIELD = (Keyword("uniform", _IDENTBODYCHARS).suppress() + _TENSOR) | (
145
202
  Literal("scalar").suppress()
146
203
  + Literal(">").suppress()
147
204
  + (
148
- _list_of(common.ieee_float)
205
+ _counted_tensor_list(size=1, ignore=_COMMENT)
149
206
  | (
150
207
  (
151
208
  (
@@ -169,11 +226,7 @@ _FIELD = (Keyword("uniform", _IDENTBODYCHARS).suppress() + _TENSOR) | (
169
226
  Literal("vector").suppress()
170
227
  + Literal(">").suppress()
171
228
  + (
172
- _list_of(
173
- Literal("(").suppress()
174
- + Group(common.ieee_float[3], aslist=True)
175
- + Literal(")").suppress()
176
- )
229
+ _counted_tensor_list(size=3, ignore=_COMMENT)
177
230
  | (
178
231
  (
179
232
  (
@@ -194,14 +247,10 @@ _FIELD = (Keyword("uniform", _IDENTBODYCHARS).suppress() + _TENSOR) | (
194
247
  )
195
248
  )
196
249
  | (
197
- Literal("vector").suppress()
250
+ Literal("symmTensor").suppress()
198
251
  + Literal(">").suppress()
199
252
  + (
200
- _list_of(
201
- Literal("(").suppress()
202
- + Group(common.ieee_float[6], aslist=True)
203
- + Literal(")").suppress()
204
- )
253
+ _counted_tensor_list(size=6, ignore=_COMMENT)
205
254
  | (
206
255
  (
207
256
  (
@@ -225,11 +274,7 @@ _FIELD = (Keyword("uniform", _IDENTBODYCHARS).suppress() + _TENSOR) | (
225
274
  Literal("tensor").suppress()
226
275
  + Literal(">").suppress()
227
276
  + (
228
- _list_of(
229
- Literal("(").suppress()
230
- + Group(common.ieee_float[9], aslist=True)
231
- + Literal(")").suppress()
232
- )
277
+ _counted_tensor_list(size=9, ignore=_COMMENT)
233
278
  | (
234
279
  (
235
280
  (
@@ -253,46 +298,45 @@ _FIELD = (Keyword("uniform", _IDENTBODYCHARS).suppress() + _TENSOR) | (
253
298
  )
254
299
  )
255
300
  _TOKEN = QuotedString('"', unquote_results=False) | _IDENTIFIER
256
- _DATA = Forward()
257
- _KEYWORD = _TOKEN | _list_of(_IDENTIFIER).set_parse_action(
258
- lambda tks: "(" + " ".join(tks[0]) + ")"
301
+ DATA = Forward()
302
+ KEYWORD = (
303
+ _TOKEN
304
+ | _list_of(_IDENTIFIER)
305
+ .set_parse_action(lambda tks: "(" + " ".join(tks[0]) + ")")
306
+ .ignore(_COMMENT)
307
+ .parse_with_tabs()
259
308
  )
260
- _KEYWORD_ENTRY = Dict(Group(_keyword_entry_of(_KEYWORD, _DATA)), asdict=True)
309
+ _KEYWORD_ENTRY = Dict(Group(_keyword_entry_of(KEYWORD, DATA)), asdict=True)
261
310
  _DATA_ENTRY = Forward()
262
311
  _LIST_ENTRY = _KEYWORD_ENTRY | _DATA_ENTRY
263
312
  _LIST = _list_of(_LIST_ENTRY)
264
313
  _NUMBER = common.signed_integer ^ common.ieee_float
265
314
  _DATA_ENTRY <<= _FIELD | _LIST | _DIMENSIONED | _DIMENSIONS | _NUMBER | _SWITCH | _TOKEN
266
315
 
267
- _DATA <<= _DATA_ENTRY[1, ...].set_parse_action(
268
- lambda tks: tuple(tks) if len(tks) > 1 else [tks[0]]
316
+ DATA <<= (
317
+ _DATA_ENTRY[1, ...]
318
+ .set_parse_action(lambda tks: tuple(tks) if len(tks) > 1 else [tks[0]])
319
+ .ignore(_COMMENT)
320
+ .parse_with_tabs()
269
321
  )
270
322
 
271
323
  _FILE = (
272
324
  Dict(
273
- Group(_keyword_entry_of(_KEYWORD, Opt(_DATA, default=""), located=True))[...]
274
- + Opt(
275
- Group(
276
- Located(
277
- _DATA_ENTRY[1, ...].set_parse_action(
278
- lambda tks: [None, tuple(tks) if len(tks) > 1 else tks[0]]
279
- )
280
- )
281
- )
282
- )
283
- + Group(_keyword_entry_of(_KEYWORD, Opt(_DATA, default=""), located=True))[...]
325
+ Group(_keyword_entry_of(KEYWORD, Opt(DATA, default=""), located=True))[...]
326
+ + Opt(Group(Located(DATA.copy().add_parse_action(lambda tks: ["", tks[0]]))))
327
+ + Group(_keyword_entry_of(KEYWORD, Opt(DATA, default=""), located=True))[...]
284
328
  )
285
- .ignore(cpp_style_comment)
329
+ .ignore(_COMMENT)
286
330
  .ignore(Literal("#include") + ... + LineEnd()) # type: ignore [no-untyped-call]
287
331
  .parse_with_tabs()
288
332
  )
289
333
 
290
334
 
291
- class Parsed(Mapping[Tuple[str, ...], Union[FoamFileBase._DataEntry, EllipsisType]]):
335
+ class Parsed(Mapping[Tuple[str, ...], Union[DataEntry, EllipsisType]]):
292
336
  def __init__(self, contents: bytes) -> None:
293
337
  self._parsed: MutableMapping[
294
338
  tuple[str, ...],
295
- tuple[int, FoamFileBase._DataEntry | EllipsisType, int],
339
+ tuple[int, DataEntry | EllipsisType, int],
296
340
  ] = {}
297
341
  for parse_result in _FILE.parse_string(
298
342
  contents.decode("latin-1"), parse_all=True
@@ -305,12 +349,10 @@ class Parsed(Mapping[Tuple[str, ...], Union[FoamFileBase._DataEntry, EllipsisTyp
305
349
  @staticmethod
306
350
  def _flatten_result(
307
351
  parse_result: ParseResults, *, _keywords: tuple[str, ...] = ()
308
- ) -> Mapping[
309
- tuple[str, ...], tuple[int, FoamFileBase._DataEntry | EllipsisType, int]
310
- ]:
352
+ ) -> Mapping[tuple[str, ...], tuple[int, DataEntry | EllipsisType, int]]:
311
353
  ret: MutableMapping[
312
354
  tuple[str, ...],
313
- tuple[int, FoamFileBase._DataEntry | EllipsisType, int],
355
+ tuple[int, DataEntry | EllipsisType, int],
314
356
  ] = {}
315
357
  start = parse_result.locn_start
316
358
  assert isinstance(start, int)
@@ -319,7 +361,8 @@ class Parsed(Mapping[Tuple[str, ...], Union[FoamFileBase._DataEntry, EllipsisTyp
319
361
  end = parse_result.locn_end
320
362
  assert isinstance(end, int)
321
363
  keyword, *data = item
322
- if keyword is None:
364
+ assert isinstance(keyword, str)
365
+ if not keyword:
323
366
  assert not _keywords
324
367
  assert len(data) == 1
325
368
  assert not isinstance(data[0], ParseResults)
@@ -336,16 +379,14 @@ class Parsed(Mapping[Tuple[str, ...], Union[FoamFileBase._DataEntry, EllipsisTyp
336
379
  ret[(*_keywords, keyword)] = (start, d, end)
337
380
  return ret
338
381
 
339
- def __getitem__(
340
- self, keywords: tuple[str, ...]
341
- ) -> FoamFileBase._DataEntry | EllipsisType:
382
+ def __getitem__(self, keywords: tuple[str, ...]) -> DataEntry | EllipsisType:
342
383
  _, data, _ = self._parsed[keywords]
343
384
  return data
344
385
 
345
386
  def put(
346
387
  self,
347
388
  keywords: tuple[str, ...],
348
- data: FoamFileBase._DataEntry | EllipsisType,
389
+ data: DataEntry | EllipsisType,
349
390
  content: bytes,
350
391
  ) -> None:
351
392
  start, end = self.entry_location(keywords, missing_ok=True)
@@ -413,14 +454,14 @@ class Parsed(Mapping[Tuple[str, ...], Union[FoamFileBase._DataEntry, EllipsisTyp
413
454
 
414
455
  return start, end
415
456
 
416
- def as_dict(self) -> FoamFileBase._File:
417
- ret: FoamFileBase._File = {}
457
+ def as_dict(self) -> File:
458
+ ret: File = {}
418
459
  for keywords, (_, data, _) in self._parsed.items():
419
460
  r = ret
420
461
  for k in keywords[:-1]:
421
462
  v = r[k]
422
463
  assert isinstance(v, dict)
423
- r = cast(FoamFileBase._File, v)
464
+ r = cast(File, v)
424
465
 
425
466
  assert isinstance(r, dict)
426
467
  if keywords:
@@ -1,9 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import array
4
- import contextlib
5
4
  import itertools
6
- import re
7
5
  import sys
8
6
  from enum import Enum, auto
9
7
  from typing import cast, overload
@@ -13,7 +11,8 @@ if sys.version_info >= (3, 9):
13
11
  else:
14
12
  from typing import Mapping, Sequence
15
13
 
16
- from ._base import FoamFileBase
14
+ from ._parsing import DATA, KEYWORD
15
+ from ._types import Data, DataEntry, Dimensioned, DimensionSet
17
16
  from ._util import is_sequence
18
17
 
19
18
  try:
@@ -34,24 +33,15 @@ class Kind(Enum):
34
33
  DIMENSIONS = auto()
35
34
 
36
35
 
37
- _TOKENS = re.compile(r'(?:[^\s"]|"(?:[^"])*")+')
38
-
39
-
40
36
  @overload
41
- def normalize(
42
- data: FoamFileBase._DataEntry, *, kind: Kind = Kind.DEFAULT
43
- ) -> FoamFileBase._DataEntry: ...
37
+ def normalize(data: DataEntry, *, kind: Kind = Kind.DEFAULT) -> DataEntry: ...
44
38
 
45
39
 
46
40
  @overload
47
- def normalize(
48
- data: FoamFileBase.Data, *, kind: Kind = Kind.DEFAULT
49
- ) -> FoamFileBase.Data: ...
41
+ def normalize(data: Data, *, kind: Kind = Kind.DEFAULT) -> Data: ...
50
42
 
51
43
 
52
- def normalize(
53
- data: FoamFileBase.Data, *, kind: Kind = Kind.DEFAULT
54
- ) -> FoamFileBase.Data:
44
+ def normalize(data: Data, *, kind: Kind = Kind.DEFAULT) -> Data:
55
45
  if numpy and isinstance(data, np.ndarray):
56
46
  ret = data.tolist()
57
47
  assert isinstance(ret, list)
@@ -67,54 +57,30 @@ def normalize(
67
57
  and all(isinstance(d, (int, float)) for d in data)
68
58
  ):
69
59
  data = cast(Sequence[float], data)
70
- return FoamFileBase.DimensionSet(*data)
60
+ return DimensionSet(*data)
71
61
 
72
62
  if is_sequence(data) and (kind == Kind.SINGLE_ENTRY or not isinstance(data, tuple)):
73
- return [normalize(d, kind=Kind.SINGLE_ENTRY) for d in data]
74
-
75
- if isinstance(data, str):
76
- data = data.strip()
77
-
78
- if data.startswith("(") and data.endswith(")"):
79
- data = data[1:-1].split()
80
- if kind == Kind.KEYWORD:
81
- return "(" + " ".join(data) + ")"
82
- return [normalize(d, kind=Kind.SINGLE_ENTRY) for d in data]
83
-
84
- if data.startswith("[") and data.endswith("]"):
85
- data = data[1:-1].split()
86
- return normalize(data, kind=Kind.DIMENSIONS)
87
-
88
- with contextlib.suppress(ValueError):
89
- return int(data)
90
-
91
- with contextlib.suppress(ValueError):
92
- return float(data)
63
+ if len(data) == 1 and isinstance(data[0], Mapping) and len(data[0]) > 1:
64
+ return [normalize({k: v}) for k, v in data[0].items()]
93
65
 
94
- if kind != Kind.KEYWORD:
95
- if data in ("yes", "true", "on", "y", "t"):
96
- return True
97
- if data in ("no", "false", "off", "n", "f"):
98
- return False
99
-
100
- tokens: list[str] = re.findall(_TOKENS, data)
66
+ return [normalize(d, kind=Kind.SINGLE_ENTRY) for d in data]
101
67
 
102
- if len(tokens) == 1:
103
- return tokens[0]
68
+ if isinstance(data, Dimensioned):
69
+ value = normalize(data.value, kind=Kind.SINGLE_ENTRY)
70
+ assert isinstance(value, (int, float, list))
71
+ return Dimensioned(value, data.dimensions, data.name)
104
72
 
73
+ if isinstance(data, str):
105
74
  if kind == Kind.KEYWORD:
106
- return " ".join(tokens)
107
-
108
- return tuple(tokens)
75
+ data = KEYWORD.parse_string(data, parse_all=True)[0]
76
+ assert isinstance(data, str)
77
+ return data
109
78
 
110
- if isinstance(data, FoamFileBase.Dimensioned):
111
- value = normalize(data.value, kind=Kind.SINGLE_ENTRY)
112
- assert isinstance(value, (int, float, list))
113
- return FoamFileBase.Dimensioned(value, data.dimensions, data.name)
79
+ return cast(DataEntry, DATA.parse_string(data, parse_all=True)[0])
114
80
 
115
81
  if isinstance(
116
82
  data,
117
- (int, float, bool, tuple, FoamFileBase.DimensionSet),
83
+ (int, float, bool, tuple, DimensionSet),
118
84
  ):
119
85
  return data
120
86
 
@@ -123,7 +89,7 @@ def normalize(
123
89
 
124
90
 
125
91
  def dumps(
126
- data: FoamFileBase.Data,
92
+ data: Data,
127
93
  *,
128
94
  kind: Kind = Kind.DEFAULT,
129
95
  ) -> bytes:
@@ -144,7 +110,7 @@ def dumps(
144
110
 
145
111
  return b" ".join(entries)
146
112
 
147
- if isinstance(data, FoamFileBase.DimensionSet):
113
+ if isinstance(data, DimensionSet):
148
114
  return b"[" + b" ".join(dumps(v) for v in data) + b"]"
149
115
 
150
116
  if kind in (
@@ -201,7 +167,7 @@ def dumps(
201
167
 
202
168
  return b"nonuniform List<" + tensor_kind + b"> " + dumps(len(data)) + contents
203
169
 
204
- if isinstance(data, FoamFileBase.Dimensioned):
170
+ if isinstance(data, Dimensioned):
205
171
  if data.name is not None:
206
172
  return (
207
173
  dumps(data.name)
@@ -0,0 +1,78 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ from dataclasses import dataclass
5
+ from typing import TYPE_CHECKING, Dict, NamedTuple, Optional, Tuple, Union
6
+
7
+ if TYPE_CHECKING:
8
+ import numpy as np
9
+
10
+ if sys.version_info >= (3, 9):
11
+ from collections.abc import Mapping, MutableMapping, Sequence
12
+ else:
13
+ from typing import Mapping, MutableMapping, Sequence
14
+
15
+
16
+ class DimensionSet(NamedTuple):
17
+ mass: float = 0
18
+ length: float = 0
19
+ time: float = 0
20
+ temperature: float = 0
21
+ moles: float = 0
22
+ current: float = 0
23
+ luminous_intensity: float = 0
24
+
25
+ def __repr__(self) -> str:
26
+ return f"{type(self).__name__}({', '.join(f'{n}={v}' for n, v in zip(self._fields, self) if v != 0)})"
27
+
28
+
29
+ Tensor = Union[
30
+ float,
31
+ Sequence[float],
32
+ "np.ndarray[Tuple[()], np.dtype[np.generic]]",
33
+ "np.ndarray[Tuple[int], np.dtype[np.generic]]",
34
+ ]
35
+
36
+
37
+ @dataclass
38
+ class Dimensioned:
39
+ value: Tensor = 0
40
+ dimensions: DimensionSet | Sequence[float] = ()
41
+ name: str | None = None
42
+
43
+ def __post_init__(self) -> None:
44
+ if not isinstance(self.dimensions, DimensionSet):
45
+ self.dimensions = DimensionSet(*self.dimensions)
46
+
47
+
48
+ Field = Union[
49
+ Tensor, Sequence[Tensor], "np.ndarray[Tuple[int, int], np.dtype[np.generic]]"
50
+ ]
51
+
52
+ DataEntry = Union[
53
+ str,
54
+ int,
55
+ float,
56
+ bool,
57
+ Dimensioned,
58
+ DimensionSet,
59
+ Sequence["Data"],
60
+ Tensor,
61
+ Field,
62
+ ]
63
+
64
+ Data = Union[
65
+ DataEntry,
66
+ Mapping[str, "Data"],
67
+ ]
68
+ """
69
+ A value that can be stored in an OpenFOAM file.
70
+ """
71
+
72
+ MutableData = Union[
73
+ DataEntry,
74
+ MutableMapping[str, "MutableData"],
75
+ ]
76
+
77
+ Dict_ = Dict[str, Union["Data", "Dict_"]]
78
+ File = Dict[Optional[str], Union["Data", "Dict_"]]
@@ -14,10 +14,10 @@ else:
14
14
  from typing_extensions import TypeGuard
15
15
 
16
16
  if TYPE_CHECKING:
17
- from ._base import FoamFileBase
17
+ from ._types import Data
18
18
 
19
19
 
20
20
  def is_sequence(
21
- value: FoamFileBase.Data,
22
- ) -> TypeGuard[Sequence[FoamFileBase.Data]]:
21
+ value: Data,
22
+ ) -> TypeGuard[Sequence[Data]]:
23
23
  return isinstance(value, Sequence) and not isinstance(value, str)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: foamlib
3
- Version: 0.6.15
3
+ Version: 0.7.1
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
@@ -69,13 +69,19 @@ Requires-Dist: foamlib[docs]; extra == "dev"
69
69
  [![Docker](https://github.com/gerlero/foamlib/actions/workflows/docker.yml/badge.svg)](https://github.com/gerlero/foamlib/actions/workflows/docker.yml)
70
70
  [![Docker image](https://img.shields.io/badge/docker%20image-microfluidica%2Ffoamlib-0085a0)](https://hub.docker.com/r/microfluidica/foamlib/)
71
71
 
72
- ## 👋 Basics
72
+ **foamlib** provides a simple, modern, ergonomic and fast Python interface for interacting with [OpenFOAM](https://www.openfoam.com).
73
+
74
+ <p align="center">
75
+ <img alt="benchmark" src="https://github.com/gerlero/foamlib/raw/main/benchmark.png" height="250">
76
+ <br>
77
+ <i>Parsing a </i>volVectorField<i> with 200k cells.</i>
78
+ </p>
73
79
 
74
- **foamlib** provides a simple, modern and ergonomic Python interface for interacting with [OpenFOAM](https://www.openfoam.com).
80
+ ## 👋 Basics
75
81
 
76
- It offers the following Python classes:
82
+ **foamlib** offers the following Python classes:
77
83
 
78
- * [`FoamFile`](https://foamlib.readthedocs.io/en/stable/files.html#foamlib.FoamFile) (and [`FoamFieldFile`](https://foamlib.readthedocs.io/en/stable/files.html#foamlib.FoamFieldFile)): read-write access to OpenFOAM configuration and field files as if they were Python `dict`s, using `foamlib`'s own parser. Supports ASCII and binary field formats (with or without compression).
84
+ * [`FoamFile`](https://foamlib.readthedocs.io/en/stable/files.html#foamlib.FoamFile) (and [`FoamFieldFile`](https://foamlib.readthedocs.io/en/stable/files.html#foamlib.FoamFieldFile)): read-write access to OpenFOAM configuration and field files as if they were Python `dict`s, using `foamlib`'s own parser and in-place editor. Supports ASCII and binary field formats (with or without compression).
79
85
  * [`FoamCase`](https://foamlib.readthedocs.io/en/stable/cases.html#foamlib.FoamCase): a class for configuring, running, and accessing the results of OpenFOAM cases.
80
86
  * [`AsyncFoamCase`](https://foamlib.readthedocs.io/en/stable/cases.html#foamlib.AsyncFoamCase): variant of `FoamCase` with asynchronous methods for running multiple cases at once.
81
87
  * [`AsyncSlurmFoamCase`](https://foamlib.readthedocs.io/en/stable/cases.html#foamlib.AsyncSlurmFoamCase): subclass of `AsyncFoamCase` used for running cases on a Slurm cluster.
@@ -185,7 +191,7 @@ async def cost(x):
185
191
  await clone.run(fallback=True) # Run locally if Slurm is not available
186
192
  return abs(clone[-1]["U"].internal_field[0][0])
187
193
 
188
- result = differential_evolution(cost, bounds=[(-1, 1)], workers=AsyncFoamCase.map, polish=False)
194
+ result = differential_evolution(cost, bounds=[(-1, 1)], workers=AsyncSlurmFoamCase.map, polish=False)
189
195
  ```
190
196
 
191
197
  ### 📄 Use it to create a `run` (or `clean`) script
@@ -17,9 +17,9 @@ foamlib/_cases/_subprocess.py
17
17
  foamlib/_cases/_sync.py
18
18
  foamlib/_cases/_util.py
19
19
  foamlib/_files/__init__.py
20
- foamlib/_files/_base.py
21
20
  foamlib/_files/_files.py
22
21
  foamlib/_files/_io.py
23
22
  foamlib/_files/_parsing.py
24
23
  foamlib/_files/_serialization.py
24
+ foamlib/_files/_types.py
25
25
  foamlib/_files/_util.py
@@ -1,76 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import sys
4
- from dataclasses import dataclass
5
- from typing import TYPE_CHECKING, Dict, NamedTuple, Optional, Tuple, Union
6
-
7
- if TYPE_CHECKING:
8
- import numpy as np
9
-
10
- if sys.version_info >= (3, 9):
11
- from collections.abc import Mapping, MutableMapping, Sequence
12
- else:
13
- from typing import Mapping, MutableMapping, Sequence
14
-
15
-
16
- class FoamFileBase:
17
- class DimensionSet(NamedTuple):
18
- mass: float = 0
19
- length: float = 0
20
- time: float = 0
21
- temperature: float = 0
22
- moles: float = 0
23
- current: float = 0
24
- luminous_intensity: float = 0
25
-
26
- def __repr__(self) -> str:
27
- return f"{type(self).__qualname__}({', '.join(f'{n}={v}' for n, v in zip(self._fields, self) if v != 0)})"
28
-
29
- _Tensor = Union[
30
- float,
31
- Sequence[float],
32
- "np.ndarray[Tuple[()], np.dtype[np.generic]]",
33
- "np.ndarray[Tuple[int], np.dtype[np.generic]]",
34
- ]
35
-
36
- @dataclass
37
- class Dimensioned:
38
- value: FoamFileBase._Tensor = 0
39
- dimensions: FoamFileBase.DimensionSet | Sequence[float] = ()
40
- name: str | None = None
41
-
42
- def __post_init__(self) -> None:
43
- if not isinstance(self.dimensions, FoamFileBase.DimensionSet):
44
- self.dimensions = FoamFileBase.DimensionSet(*self.dimensions)
45
-
46
- _Field = Union[
47
- _Tensor, Sequence[_Tensor], "np.ndarray[Tuple[int, int], np.dtype[np.generic]]"
48
- ]
49
-
50
- _DataEntry = Union[
51
- str,
52
- int,
53
- float,
54
- bool,
55
- Dimensioned,
56
- DimensionSet,
57
- Sequence["Data"],
58
- _Tensor,
59
- _Field,
60
- ]
61
-
62
- Data = Union[
63
- _DataEntry,
64
- Mapping[str, "Data"],
65
- ]
66
- """
67
- A value that can be stored in an OpenFOAM file.
68
- """
69
-
70
- _MutableData = Union[
71
- _DataEntry,
72
- MutableMapping[str, "_MutableData"],
73
- ]
74
-
75
- _Dict = Dict[str, Union["Data", "_Dict"]]
76
- _File = Dict[Optional[str], Union["Data", "_Dict"]]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes