diff --git a/foamlib/_files/_files.py b/foamlib/_files/_files.py index bfb594d..9ced37d 100644 --- a/foamlib/_files/_files.py +++ b/foamlib/_files/_files.py @@ -1,6 +1,5 @@ from __future__ import annotations -import os import sys from copy import deepcopy from typing import Any, Optional, Tuple, Union, cast @@ -15,6 +14,8 @@ else: from typing import Iterator, Mapping, MutableMapping, Sequence +import numpy as np + from ._io import FoamFileIO from ._serialization import Kind, dumps, normalize from ._types import ( @@ -27,7 +28,6 @@ File, MutableEntry, ) -from ._util import is_sequence class FoamFile( @@ -228,50 +228,36 @@ def __setitem__(self, keywords: str | tuple[str, ...] | None, data: Entry) -> No or keywords[2].endswith("Gradient") ) ): - if self.format == "binary": - arch = self.get(("FoamFile", "arch"), default=None) - assert arch is None or isinstance(arch, str) - if (arch is not None and "scalar=32" in arch) or ( - arch is None - and os.environ.get("WM_PRECISION_OPTION", default="DP") == "SP" - ): - kind = Kind.SINGLE_PRECISION_BINARY_FIELD - else: - kind = Kind.DOUBLE_PRECISION_BINARY_FIELD - else: - kind = Kind.ASCII_FIELD + kind = ( + Kind.BINARY_FIELD if self.format == "binary" else Kind.ASCII_FIELD + ) elif keywords == ("dimensions",): kind = Kind.DIMENSIONS if ( - kind - in ( - Kind.ASCII_FIELD, - Kind.DOUBLE_PRECISION_BINARY_FIELD, - Kind.SINGLE_PRECISION_BINARY_FIELD, - ) + kind in (Kind.ASCII_FIELD, Kind.BINARY_FIELD) ) and self.class_ == "dictionary": - if isinstance(data, (int, float)): - self.class_ = "volScalarField" - - elif is_sequence(data) and data: - if isinstance(data[0], (int, float)): - if len(data) == 3: - self.class_ = "volVectorField" - elif len(data) == 6: - self.class_ = "volSymmTensorField" - elif len(data) == 9: - self.class_ = "volTensorField" - elif ( - is_sequence(data[0]) - and data[0] - and isinstance(data[0][0], (int, float)) - ): - if len(data[0]) == 3: + try: + shape = np.shape(data) # type: ignore [arg-type] + except ValueError: + pass + else: + if not shape: + self.class_ = "volScalarField" + elif shape == (3,): + self.class_ = "volVectorField" + elif shape == (6,): + self.class_ = "volSymmTensorField" + elif shape == (9,): + self.class_ = "volTensorField" + elif len(shape) == 1: + self.class_ = "volScalarField" + elif len(shape) == 2: + if shape[1] == 3: self.class_ = "volVectorField" - elif len(data[0]) == 6: + elif shape[1] == 6: self.class_ = "volSymmTensorField" - elif len(data[0]) == 9: + elif shape[1] == 9: self.class_ = "volTensorField" parsed = self._get_parsed(missing_ok=True) diff --git a/foamlib/_files/_parsing.py b/foamlib/_files/_parsing.py index 8946873..1e097d1 100644 --- a/foamlib/_files/_parsing.py +++ b/foamlib/_files/_parsing.py @@ -1,6 +1,5 @@ from __future__ import annotations -import array import re import sys from enum import Enum, auto @@ -16,6 +15,7 @@ else: EllipsisType = type(...) +import numpy as np from pyparsing import ( Combine, Dict, @@ -47,13 +47,16 @@ class _Tensor(Enum): TENSOR = auto() @property - def shape(self) -> tuple[int, ...]: - return { - _Tensor.SCALAR: (), - _Tensor.VECTOR: (3,), - _Tensor.SYMM_TENSOR: (6,), - _Tensor.TENSOR: (9,), - }[self] + def shape(self) -> tuple[()] | tuple[int]: + if self == _Tensor.SCALAR: + return () + if self == _Tensor.VECTOR: + return (3,) + if self == _Tensor.SYMM_TENSOR: + return (6,) + if self == _Tensor.TENSOR: + return (9,) + raise NotImplementedError @property def size(self) -> int: @@ -84,7 +87,7 @@ def parser(self) -> ParserElement: Literal("(").suppress() + Group(common.ieee_float[self.size], aslist=True) + Literal(")").suppress() - ) + ).add_parse_action(lambda tks: np.array(tks[0], dtype=float)) def __str__(self) -> str: return { @@ -116,40 +119,22 @@ def _list_of(entry: ParserElement) -> ParserElement: def _parse_ascii_field( s: str, tensor_kind: _Tensor, *, ignore: Regex | None -) -> list[float] | list[list[float]]: - values = [ - float(v) - for v in (re.sub(ignore.re, " ", s) if ignore is not None else s) - .replace("(", " ") - .replace(")", " ") - .split() - ] - - if tensor_kind == _Tensor.SCALAR: - return values +) -> np.ndarray[tuple[int] | tuple[int, int], np.dtype[np.float64]]: + if ignore is not None: + s = re.sub(ignore.re, " ", s) + s = s.replace("(", " ").replace(")", " ") - return [ - values[i : i + tensor_kind.size] - for i in range(0, len(values), tensor_kind.size) - ] + return np.fromstring(s, dtype=float, sep=" ").reshape(-1, *tensor_kind.shape) def _unpack_binary_field( b: bytes, tensor_kind: _Tensor, *, length: int -) -> list[float] | list[list[float]]: +) -> np.ndarray[tuple[int] | tuple[int, int], np.dtype[np.float64 | np.float32]]: float_size = len(b) / tensor_kind.size / length assert float_size in (4, 8) - arr = array.array("f" if float_size == 4 else "d", b) - values = arr.tolist() - - if tensor_kind == _Tensor.SCALAR: - return values - - return [ - values[i : i + tensor_kind.size] - for i in range(0, len(values), tensor_kind.size) - ] + dtype = np.float32 if float_size == 4 else float + return np.frombuffer(b, dtype=dtype).reshape(-1, *tensor_kind.shape) def _tensor_list( @@ -187,7 +172,7 @@ def count_parse_action(tks: ParseResults) -> None: ) | Regex( rf"\((?s:.{{{length * tensor_kind.size * 8}}}|.{{{length * tensor_kind.size * 4}}})\)" - ).set_parse_action( + ).add_parse_action( lambda tks: [ _unpack_binary_field( tks[0][1:-1].encode("latin-1"), tensor_kind, length=length @@ -196,7 +181,9 @@ def count_parse_action(tks: ParseResults) -> None: ) | ( Literal("{").suppress() + tensor_kind.parser() + Literal("}").suppress() - ).set_parse_action(lambda tks: [[tks[0]] * length]) + ).add_parse_action( + lambda tks: [np.full((length, *tensor_kind.shape), tks[0], dtype=float)] + ) ) count = common.integer.copy().add_parse_action(count_parse_action) diff --git a/foamlib/_files/_serialization.py b/foamlib/_files/_serialization.py index 5fdb37f..ef210b0 100644 --- a/foamlib/_files/_serialization.py +++ b/foamlib/_files/_serialization.py @@ -1,34 +1,25 @@ from __future__ import annotations -import array -import itertools import sys from enum import Enum, auto -from typing import cast, overload +from typing import overload if sys.version_info >= (3, 9): - from collections.abc import Mapping, Sequence + from collections.abc import Mapping else: - from typing import Mapping, Sequence + from typing import Mapping -from ._parsing import parse_data -from ._types import Data, Dimensioned, DimensionSet, Entry -from ._util import is_sequence - -try: - import numpy as np +import numpy as np - numpy = True -except ModuleNotFoundError: - numpy = False +from ._parsing import parse_data +from ._types import Data, Dimensioned, DimensionSet, Entry, is_sequence class Kind(Enum): DEFAULT = auto() SINGLE_ENTRY = auto() ASCII_FIELD = auto() - DOUBLE_PRECISION_BINARY_FIELD = auto() - SINGLE_PRECISION_BINARY_FIELD = auto() + BINARY_FIELD = auto() DIMENSIONS = auto() @@ -41,9 +32,29 @@ def normalize(data: Entry, *, kind: Kind = Kind.DEFAULT) -> Entry: ... def normalize(data: Entry, *, kind: Kind = Kind.DEFAULT) -> Entry: - if numpy and isinstance(data, np.ndarray): + if kind in (Kind.ASCII_FIELD, Kind.BINARY_FIELD): + if is_sequence(data): + try: + arr = np.asarray(data) + except ValueError: + pass + else: + if not np.issubdtype(arr.dtype, np.floating): + arr = arr.astype(float) + + if arr.ndim == 1 or (arr.ndim == 2 and arr.shape[1] in (3, 6, 9)): + return arr + + return data + + if isinstance(data, int): + return float(data) + + return data + + if isinstance(data, np.ndarray): ret = data.tolist() - assert isinstance(ret, list) + assert isinstance(ret, (int, float, list)) return ret if isinstance(data, Mapping): @@ -55,7 +66,6 @@ def normalize(data: Entry, *, kind: Kind = Kind.DEFAULT) -> Entry: and len(data) <= 7 and all(isinstance(d, (int, float)) for d in data) ): - data = cast(Sequence[float], data) return DimensionSet(*data) if isinstance(data, tuple) and kind == Kind.SINGLE_ENTRY and len(data) == 2: @@ -65,17 +75,12 @@ def normalize(data: Entry, *, kind: Kind = Kind.DEFAULT) -> Entry: if is_sequence(data) and (kind == Kind.SINGLE_ENTRY or not isinstance(data, tuple)): return [normalize(d, kind=Kind.SINGLE_ENTRY) for d in data] - if isinstance(data, Dimensioned): - value = normalize(data.value, kind=Kind.SINGLE_ENTRY) - assert isinstance(value, (int, float, list)) - return Dimensioned(value, data.dimensions, data.name) - if isinstance(data, str): return parse_data(data) if isinstance( data, - (int, float, bool, tuple, DimensionSet), + (int, float, bool, tuple, DimensionSet, Dimensioned), ): return data @@ -107,58 +112,35 @@ def dumps( if isinstance(data, DimensionSet): return b"[" + b" ".join(dumps(v) for v in data) + b"]" - if kind in ( - Kind.ASCII_FIELD, - Kind.DOUBLE_PRECISION_BINARY_FIELD, - Kind.SINGLE_PRECISION_BINARY_FIELD, - ) and ( - isinstance(data, (int, float)) - or ( - is_sequence(data) - and data - and isinstance(data[0], (int, float)) - and len(data) in (3, 6, 9) - ) + if kind in (Kind.ASCII_FIELD, Kind.BINARY_FIELD) and ( + isinstance(data, (int, float, np.ndarray)) ): - return b"uniform " + dumps(data, kind=Kind.SINGLE_ENTRY) - - if kind in ( - Kind.ASCII_FIELD, - Kind.DOUBLE_PRECISION_BINARY_FIELD, - Kind.SINGLE_PRECISION_BINARY_FIELD, - ) and is_sequence(data): - if data and isinstance(data[0], (int, float)): + shape = np.shape(data) + if shape in ((), (3,), (6,), (9,)): + return b"uniform " + dumps(data, kind=Kind.SINGLE_ENTRY) + + assert isinstance(data, np.ndarray) + ndim = len(shape) + if ndim == 1: tensor_kind = b"scalar" - elif is_sequence(data[0]) and data[0] and isinstance(data[0][0], (int, float)): - if len(data[0]) == 3: + + elif ndim == 2: + if shape[1] == 3: tensor_kind = b"vector" - elif len(data[0]) == 6: + elif shape[1] == 6: tensor_kind = b"symmTensor" - elif len(data[0]) == 9: + elif shape[1] == 9: tensor_kind = b"tensor" else: return dumps(data) + else: return dumps(data) - if kind in ( - Kind.DOUBLE_PRECISION_BINARY_FIELD, - Kind.SINGLE_PRECISION_BINARY_FIELD, - ): - typecode = "f" if kind == Kind.SINGLE_PRECISION_BINARY_FIELD else "d" - if tensor_kind == b"scalar": - data = cast(Sequence[float], data) - contents = b"(" + array.array(typecode, data).tobytes() + b")" - else: - data = cast(Sequence[Sequence[float]], data) - contents = ( - b"(" - + array.array( - typecode, itertools.chain.from_iterable(data) - ).tobytes() - + b")" - ) + if kind == Kind.BINARY_FIELD: + contents = b"(" + data.tobytes() + b")" else: + assert kind == Kind.ASCII_FIELD contents = dumps(data, kind=Kind.SINGLE_ENTRY) return b"nonuniform List<" + tensor_kind + b"> " + dumps(len(data)) + contents diff --git a/foamlib/_files/_types.py b/foamlib/_files/_types.py index 19f3443..b46131d 100644 --- a/foamlib/_files/_types.py +++ b/foamlib/_files/_types.py @@ -2,16 +2,20 @@ import sys from dataclasses import dataclass -from typing import TYPE_CHECKING, Dict, NamedTuple, Optional, Union +from typing import Dict, NamedTuple, Optional, Tuple, Union -if TYPE_CHECKING: - import numpy as np +import numpy as np if sys.version_info >= (3, 9): from collections.abc import Mapping, MutableMapping, Sequence else: from typing import Mapping, MutableMapping, Sequence +if sys.version_info >= (3, 10): + from typing import TypeGuard +else: + from typing_extensions import TypeGuard + class DimensionSet(NamedTuple): mass: float = 0 @@ -29,7 +33,7 @@ def __repr__(self) -> str: Tensor = Union[ float, Sequence[float], - "np.ndarray[tuple[()] | tuple[int], np.dtype[np.float64 | np.int_]]", + np.ndarray[Union[Tuple[()], Tuple[int]], np.dtype[np.float64]], ] @@ -40,14 +44,30 @@ class Dimensioned: name: str | None = None def __post_init__(self) -> None: + if is_sequence(self.value): + self.value = np.asarray(self.value, dtype=float) + else: + assert isinstance(self.value, (int, float, np.ndarray)) + self.value = float(self.value) + if not isinstance(self.dimensions, DimensionSet): self.dimensions = DimensionSet(*self.dimensions) + def __eq__(self, other: object) -> bool: + if not isinstance(other, Dimensioned): + return NotImplemented + + return ( + self.dimensions == other.dimensions + and np.array_equal(self.value, other.value) + and self.name == other.name + ) + Field = Union[ Tensor, Sequence[Tensor], - "np.ndarray[tuple[int] | tuple[int, int], np.dtype[np.float64 | np.int_]]", + np.ndarray[Union[Tuple[int], Tuple[int, int]], np.dtype[np.float64 | np.float32]], ] Data = Union[ @@ -58,7 +78,6 @@ def __post_init__(self) -> None: Dimensioned, DimensionSet, Sequence["Entry"], - Tensor, Field, ] @@ -70,6 +89,18 @@ def __post_init__(self) -> None: A value that can be stored in an OpenFOAM file. """ + +def is_sequence( + value: Entry, +) -> TypeGuard[ + Sequence[Entry] + | np.ndarray[tuple[int] | tuple[int, int], np.dtype[np.float64 | np.float32]] +]: + return (isinstance(value, Sequence) and not isinstance(value, str)) or ( + isinstance(value, np.ndarray) and value.ndim > 0 + ) + + MutableEntry = Union[ Data, MutableMapping[str, "MutableEntry"], diff --git a/foamlib/_files/_util.py b/foamlib/_files/_util.py deleted file mode 100644 index d7da07d..0000000 --- a/foamlib/_files/_util.py +++ /dev/null @@ -1,23 +0,0 @@ -from __future__ import annotations - -import sys -from typing import TYPE_CHECKING - -if sys.version_info >= (3, 9): - from collections.abc import Sequence -else: - from typing import Sequence - -if sys.version_info >= (3, 10): - from typing import TypeGuard -else: - from typing_extensions import TypeGuard - -if TYPE_CHECKING: - from ._types import Entry - - -def is_sequence( - value: Entry, -) -> TypeGuard[Sequence[Entry]]: - return isinstance(value, Sequence) and not isinstance(value, str) diff --git a/pyproject.toml b/pyproject.toml index 8edadbe..63ece30 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,6 +30,8 @@ classifiers = [ dependencies = [ "aioshutil>=1,<2", + "numpy>=1.25.0,<3; python_version>='3.10'", + "numpy>=1,<3", "pyparsing>=3,<4", "typing-extensions>=4,<5; python_version<'3.11'", ] @@ -37,13 +39,8 @@ dependencies = [ dynamic = ["version"] [project.optional-dependencies] -numpy = [ - "numpy>=1.25.0,<3; python_version>='3.10'", - "numpy>=1,<3" -] lint = ["ruff"] test = [ - "foamlib[numpy]", "pytest>=7,<9", "pytest-asyncio>=0.21,<0.25", "pytest-cov", @@ -53,12 +50,10 @@ typing = [ "mypy>=1,<2", ] docs = [ - "foamlib[numpy]", "sphinx>=5,<9", "sphinx_rtd_theme", ] dev = [ - "foamlib[numpy]", "foamlib[lint]", "foamlib[test]", "foamlib[typing]", diff --git a/tests/test_cases/test_cavity.py b/tests/test_cases/test_cavity.py index 21053c4..c682ab7 100644 --- a/tests/test_cases/test_cavity.py +++ b/tests/test_cases/test_cavity.py @@ -2,13 +2,13 @@ import stat import sys from pathlib import Path -from typing import Sequence if sys.version_info >= (3, 9): from collections.abc import Generator else: from typing import Generator +import numpy as np import pytest from foamlib import FoamCase @@ -48,7 +48,7 @@ def test_run(cavity: FoamCase) -> None: cavity.run(parallel=False) assert len(cavity) > 0 internal = cavity[-1]["U"].internal_field - assert isinstance(internal, Sequence) + assert isinstance(internal, np.ndarray) assert len(internal) == 400 @@ -61,5 +61,5 @@ def test_double_clean(cavity: FoamCase) -> None: def test_cell_centers(cavity: FoamCase) -> None: cavity.block_mesh() C = cavity[0].cell_centers() - assert isinstance(C.internal_field, list) + assert isinstance(C.internal_field, np.ndarray) assert len(C.internal_field) == 400 diff --git a/tests/test_cases/test_cavity_async.py b/tests/test_cases/test_cavity_async.py index 03e0509..9602ec0 100644 --- a/tests/test_cases/test_cavity_async.py +++ b/tests/test_cases/test_cavity_async.py @@ -9,6 +9,7 @@ else: from typing import AsyncGenerator +import numpy as np import pytest import pytest_asyncio from foamlib import AsyncFoamCase @@ -50,7 +51,7 @@ async def test_run(cavity: AsyncFoamCase) -> None: await cavity.run(parallel=False) assert len(cavity) > 0 internal = cavity[-1]["U"].internal_field - assert isinstance(internal, Sequence) + assert isinstance(internal, np.ndarray) assert len(internal) == 400 @@ -65,7 +66,7 @@ async def test_double_clean(cavity: AsyncFoamCase) -> None: async def test_cell_centers(cavity: AsyncFoamCase) -> None: await cavity.block_mesh() C = await cavity[0].cell_centers() - assert isinstance(C.internal_field, list) + assert isinstance(C.internal_field, np.ndarray) assert len(C.internal_field) == 400 diff --git a/tests/test_files/test_dumps.py b/tests/test_files/test_dumps.py index 1028bd4..b7a0b60 100644 --- a/tests/test_files/test_dumps.py +++ b/tests/test_files/test_dumps.py @@ -1,3 +1,4 @@ +import numpy as np from foamlib import FoamFile from foamlib._files._serialization import Kind, dumps @@ -11,34 +12,32 @@ def test_serialize_data() -> None: assert dumps("word") == b"word" assert dumps(("word", "word")) == b"word word" assert dumps('"a string"') == b'"a string"' - assert dumps(1, kind=Kind.ASCII_FIELD) == b"uniform 1" + assert dumps(1, kind=Kind.ASCII_FIELD) == b"uniform 1.0" assert dumps(1.0, kind=Kind.ASCII_FIELD) == b"uniform 1.0" assert dumps(1.0e-3, kind=Kind.ASCII_FIELD) == b"uniform 0.001" assert dumps([1.0, 2.0, 3.0]) == b"(1.0 2.0 3.0)" - assert dumps([1, 2, 3], kind=Kind.ASCII_FIELD) == b"uniform (1 2 3)" + assert dumps([1, 2, 3], kind=Kind.ASCII_FIELD) == b"uniform (1.0 2.0 3.0)" assert ( dumps([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], kind=Kind.ASCII_FIELD) - == b"nonuniform List 10(1 2 3 4 5 6 7 8 9 10)" + == b"nonuniform List 10(1.0 2.0 3.0 4.0 5.0 6.0 7.0 8.0 9.0 10.0)" ) assert ( dumps([[1, 2, 3], [4, 5, 6]], kind=Kind.ASCII_FIELD) - == b"nonuniform List 2((1 2 3) (4 5 6))" + == b"nonuniform List 2((1.0 2.0 3.0) (4.0 5.0 6.0))" ) - assert dumps(1, kind=Kind.DOUBLE_PRECISION_BINARY_FIELD) == b"uniform 1" - assert dumps(1.0, kind=Kind.DOUBLE_PRECISION_BINARY_FIELD) == b"uniform 1.0" + assert dumps(1, kind=Kind.BINARY_FIELD) == b"uniform 1.0" + assert dumps(1.0, kind=Kind.BINARY_FIELD) == b"uniform 1.0" + assert dumps([1, 2, 3], kind=Kind.BINARY_FIELD) == b"uniform (1.0 2.0 3.0)" assert ( - dumps([1, 2, 3], kind=Kind.DOUBLE_PRECISION_BINARY_FIELD) == b"uniform (1 2 3)" - ) - assert ( - dumps([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], kind=Kind.DOUBLE_PRECISION_BINARY_FIELD) + dumps([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], kind=Kind.BINARY_FIELD) == b'nonuniform List 10(\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x00\x08@\x00\x00\x00\x00\x00\x00\x10@\x00\x00\x00\x00\x00\x00\x14@\x00\x00\x00\x00\x00\x00\x18@\x00\x00\x00\x00\x00\x00\x1c@\x00\x00\x00\x00\x00\x00 @\x00\x00\x00\x00\x00\x00"@\x00\x00\x00\x00\x00\x00$@)' ) assert ( - dumps([[1, 2, 3], [4, 5, 6]], kind=Kind.DOUBLE_PRECISION_BINARY_FIELD) + dumps([[1, 2, 3], [4, 5, 6]], kind=Kind.BINARY_FIELD) == b"nonuniform List 2(\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x00\x08@\x00\x00\x00\x00\x00\x00\x10@\x00\x00\x00\x00\x00\x00\x14@\x00\x00\x00\x00\x00\x00\x18@)" ) assert ( - dumps([1, 2], kind=Kind.SINGLE_PRECISION_BINARY_FIELD) + dumps(np.array([1, 2], dtype=np.float32), kind=Kind.BINARY_FIELD) == b"nonuniform List 2(\x00\x00\x80?\x00\x00\x00@)" ) assert ( diff --git a/tests/test_files/test_files.py b/tests/test_files/test_files.py index 4f83109..964d507 100644 --- a/tests/test_files/test_files.py +++ b/tests/test_files/test_files.py @@ -98,7 +98,9 @@ def test_new_field(tmp_path: Path) -> None: Path(tmp_path / "testField").touch() f = FoamFieldFile(tmp_path / "testField") f.internal_field = [1, 2, 3] - assert f.internal_field == [1, 2, 3] + field = f.internal_field + assert isinstance(field, np.ndarray) + assert np.array_equal(f.internal_field, [1, 2, 3]) assert f.class_ == "volVectorField" @@ -166,9 +168,8 @@ def test_internal_field(cavity: FoamCase) -> None: assert cavity[0]["p"].internal_field == pytest.approx(p_arr) U = cavity[0]["U"].internal_field - assert isinstance(U, Sequence) - for u, u_arr in zip(U, U_arr): - assert u == pytest.approx(u_arr) + assert isinstance(U, np.ndarray) + assert U_arr == pytest.approx(U) p_arr = np.arange(size) * 1e-6 U_arr = np.full((size, 3), [-1e-6, 1e-6, 0]) * np.arange(size)[:, np.newaxis] @@ -178,9 +179,8 @@ def test_internal_field(cavity: FoamCase) -> None: assert cavity[0]["p"].internal_field == pytest.approx(p_arr) U = cavity[0]["U"].internal_field - assert isinstance(U, Sequence) - for u, u_arr in zip(U, U_arr): - assert u == pytest.approx(u_arr) + assert isinstance(U, np.ndarray) + assert U_arr == pytest.approx(U) cavity.run(parallel=False) @@ -200,27 +200,23 @@ def test_binary_field(cavity: FoamCase) -> None: cavity.run(parallel=False) p_bin = cavity[-1]["p"].internal_field - assert isinstance(p_bin, Sequence) + assert isinstance(p_bin, np.ndarray) U_bin = cavity[-1]["U"].internal_field - assert isinstance(U_bin, Sequence) - assert isinstance(U_bin[0], Sequence) - assert len(U_bin[0]) == 3 - size = len(p_bin) - assert len(U_bin) == size + assert isinstance(U_bin, np.ndarray) + assert U_bin.shape == (len(p_bin), 3) cavity.clean() - p_arr = np.arange(size) * 1e-6 - U_arr = np.full((size, 3), [-1e-6, 1e-6, 0]) * np.arange(size)[:, np.newaxis] + p_arr = np.arange(len(p_bin)) * 1e-6 + U_arr = np.full_like(U_bin, [-1e-6, 1e-6, 0]) * np.arange(len(U_bin))[:, np.newaxis] cavity[0]["p"].internal_field = p_arr cavity[0]["U"].internal_field = U_arr assert cavity[0]["p"].internal_field == pytest.approx(p_arr) U = cavity[0]["U"].internal_field - assert isinstance(U, Sequence) - for u, u_arr in zip(U, U_arr): - assert u == pytest.approx(u_arr) + assert isinstance(U, np.ndarray) + assert U_arr == pytest.approx(U) cavity.run(parallel=False) @@ -231,26 +227,22 @@ def test_compressed_field(cavity: FoamCase) -> None: cavity.run(parallel=False) p_bin = cavity[-1]["p"].internal_field - assert isinstance(p_bin, Sequence) + assert isinstance(p_bin, np.ndarray) U_bin = cavity[-1]["U"].internal_field - assert isinstance(U_bin, Sequence) - assert isinstance(U_bin[0], Sequence) - assert len(U_bin[0]) == 3 - size = len(p_bin) - assert len(U_bin) == size + assert isinstance(U_bin, np.ndarray) + assert U_bin.shape == (len(p_bin), 3) cavity.clean() - p_arr = np.arange(size) * 1e-6 - U_arr = np.full((size, 3), [-1e-6, 1e-6, 0]) * np.arange(size)[:, np.newaxis] + p_arr = np.arange(len(p_bin)) * 1e-6 + U_arr = np.full_like(U_bin, [-1e-6, 1e-6, 0]) * np.arange(len(U_bin))[:, np.newaxis] cavity[0]["p"].internal_field = p_arr cavity[0]["U"].internal_field = U_arr assert cavity[0]["p"].internal_field == pytest.approx(p_arr) U = cavity[0]["U"].internal_field - assert isinstance(U, Sequence) - for u, u_arr in zip(U, U_arr): - assert u == pytest.approx(u_arr) + assert isinstance(U, np.ndarray) + assert U_arr == pytest.approx(U) cavity.run(parallel=False) diff --git a/tests/test_files/test_parsing.py b/tests/test_files/test_parsing.py index 87b78f6..60a051b 100644 --- a/tests/test_files/test_parsing.py +++ b/tests/test_files/test_parsing.py @@ -1,3 +1,4 @@ +import numpy as np from foamlib import FoamFile from foamlib._files._parsing import Parsed @@ -15,9 +16,15 @@ def test_parse_value() -> None: assert Parsed(b"uniform 1.0")[()] == 1.0 assert Parsed(b"uniform 1.0e-3")[()] == 1.0e-3 assert Parsed(b"(1.0 2.0 3.0)")[()] == [1.0, 2.0, 3.0] - assert Parsed(b"uniform (1 2 3)")[()] == [1, 2, 3] - assert Parsed(b"nonuniform List 2(1 2)")[()] == [1, 2] - assert Parsed(b"nonuniform List 2{1}")[()] == [1, 1] + field = Parsed(b"uniform (1 2 3)")[()] + assert isinstance(field, np.ndarray) + assert np.array_equal(field, [1, 2, 3]) + field = Parsed(b"nonuniform List 2(1 2)")[()] + assert isinstance(field, np.ndarray) + assert np.array_equal(field, [1, 2]) + field = Parsed(b"nonuniform List 2{1}")[()] + assert isinstance(field, np.ndarray) + assert np.array_equal(field, [1, 1]) assert Parsed(b"3(1 2 3)")[()] == [1, 2, 3] assert Parsed(b"2((1 2 3) (4 5 6))")[()] == [ [1, 2, 3], @@ -27,24 +34,37 @@ def test_parse_value() -> None: [1, 2, 3], [1, 2, 3], ] - assert Parsed(b"nonuniform List 2((1 2 3) (4 5 6))")[()] == [ - [1, 2, 3], - [4, 5, 6], - ] - assert Parsed(b"nonuniform List 2{(1 2 3)}")[()] == [ - [1, 2, 3], - [1, 2, 3], - ] - assert Parsed( + field = Parsed(b"nonuniform List 2((1 2 3) (4 5 6))")[()] + assert isinstance(field, np.ndarray) + assert np.array_equal( + field, + [ + [1, 2, 3], + [4, 5, 6], + ], + ) + field = Parsed(b"nonuniform List 2{(1 2 3)}")[()] + assert isinstance(field, np.ndarray) + assert np.array_equal( + field, + [ + [1, 2, 3], + [1, 2, 3], + ], + ) + field = Parsed( b"nonuniform List 2(\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\x00@)" - )[()] == [1, 2] - assert Parsed( + )[()] + assert isinstance(field, np.ndarray) + assert np.array_equal(field, [1, 2]) + field = Parsed( b"nonuniform List 2(\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x00\x08@\x00\x00\x00\x00\x00\x00\x10@\x00\x00\x00\x00\x00\x00\x14@\x00\x00\x00\x00\x00\x00\x18@)" - )[()] == [[1, 2, 3], [4, 5, 6]] - assert Parsed(b"nonuniform List 2(\x00\x00\x80?\x00\x00\x00@)")[()] == [ - 1, - 2, - ] + )[()] + assert isinstance(field, np.ndarray) + assert np.array_equal(field, [[1, 2, 3], [4, 5, 6]]) + field = Parsed(b"nonuniform List 2(\x00\x00\x80?\x00\x00\x00@)")[()] + assert isinstance(field, np.ndarray) + assert np.array_equal(field, [1, 2]) assert Parsed(b"[1 1 -2 0 0 0 0]")[()] == FoamFile.DimensionSet( mass=1, length=1, time=-2 )