Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
229 changes: 117 additions & 112 deletions src/foamlib/_files/_parsing/_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,7 @@ def __call__(
) from e

try:
ret = np.fromstring(data, sep=" ", dtype=self._dtype)
ret = np.array(data.split(), dtype=self._dtype)
except ValueError as e:
raise ParseError(
contents,
Expand Down Expand Up @@ -414,140 +414,141 @@ def __call__(
_parse_ascii_tensor_list = _ASCIINumericListParser(dtype=float, elshape=(9,))


_THREE_FACE_LIKE = re.compile(
rb"3(?:"
+ _SKIP.pattern
+ rb")?\((?:"
+ _SKIP.pattern
+ rb")?(?:"
+ _POSSIBLE_INTEGER.pattern
+ rb"(?:"
+ _SKIP.pattern
+ rb"))(?:"
+ _POSSIBLE_INTEGER.pattern
+ rb"(?:"
+ _SKIP.pattern
+ rb"))(?:"
+ _POSSIBLE_INTEGER.pattern
+ rb")(?:"
+ _SKIP.pattern
+ rb")?\)"
_SUB_LIST_LIKE = re.compile(
rb"(?:" + _POSSIBLE_INTEGER.pattern + rb")(?:" + _SKIP.pattern + rb")?\([^()]*?\)"
Comment thread
RamogninoF marked this conversation as resolved.
)
_UNCOMMENTED_THREE_FACE_LIKE = re.compile(
rb"3\s*\(\s*(?:"
+ _POSSIBLE_INTEGER.pattern
+ rb"\s*)(?:"
+ _POSSIBLE_INTEGER.pattern
+ rb"\s*)(?:"
+ _POSSIBLE_INTEGER.pattern
+ rb")\s*\)",
_UNCOMMENTED_SUB_LIST_LIKE = re.compile(
rb"(?:" + _POSSIBLE_INTEGER.pattern + rb")\s*\([^()]*?\)",
re.ASCII,
)
_FOUR_FACE_LIKE = re.compile(
rb"4(?:"
+ _SKIP.pattern
+ rb")?\((?:"
+ _SKIP.pattern
+ rb")?(?:"
+ _POSSIBLE_INTEGER.pattern
+ rb"(?:"
+ _SKIP.pattern
+ rb"))(?:"
+ _POSSIBLE_INTEGER.pattern
+ rb"(?:"
+ _SKIP.pattern
+ rb"))(?:"
+ _POSSIBLE_INTEGER.pattern
+ rb"(?:"
_LIST_OF_LISTS_LIKE = re.compile(
rb"(?:(?:"
+ _SKIP.pattern
+ rb"))(?:"
+ _POSSIBLE_INTEGER.pattern
+ rb")(?:"
+ rb")?"
+ _SUB_LIST_LIKE.pattern
+ rb")*(?:"
+ _SKIP.pattern
+ rb")?\)"
)
_UNCOMMENTED_FOUR_FACE_LIKE = re.compile(
rb"4\s*\(\s*(?:"
+ _POSSIBLE_INTEGER.pattern
+ rb"\s*)(?:"
+ _POSSIBLE_INTEGER.pattern
+ rb"\s*)(?:"
+ _POSSIBLE_INTEGER.pattern
+ rb"\s*)(?:"
+ _POSSIBLE_INTEGER.pattern
+ rb")\s*\)",
_UNCOMMENTED_LIST_OF_LISTS_LIKE = re.compile(
rb"(?:\s*" + _UNCOMMENTED_SUB_LIST_LIKE.pattern + rb")*\s*\)",
re.ASCII,
)
_FACES_LIKE_LIST = re.compile(
rb"(?:(?:"
+ _SKIP.pattern
+ rb")?(?:"
+ _THREE_FACE_LIKE.pattern
+ rb"|"
+ _FOUR_FACE_LIKE.pattern
+ rb"))*(?:"
+ _SKIP.pattern
+ rb")?\)"
)
_UNCOMMENTED_FACES_LIKE_LIST = re.compile(
rb"(?:\s*(?:"
+ _UNCOMMENTED_THREE_FACE_LIKE.pattern
+ rb"|"
+ _UNCOMMENTED_FOUR_FACE_LIKE.pattern
+ rb"))*\s*\)",
_SUBLIST_CAPTURE = re.compile(
rb"(" + _POSSIBLE_INTEGER.pattern + rb")\s*\(([^()]*?)\)",
re.ASCII,
)


def _parse_ascii_faces_like_list(
contents: bytes | bytearray, pos: int
) -> tuple[list[np.ndarray[tuple[Literal[3, 4]], np.dtype[np.int64]]], int]:
try:
count, pos = _parse_number(contents, pos, target=int)
except ParseError:
count = None
else:
if count < 0:
raise ParseError(contents, pos, expected="non-negative list count")
pos = _skip(contents, pos)
class _ASCIINumericListListParser(Generic[_DType]):
def __init__(self, *, dtype: type[_DType]) -> None:
self._dtype = dtype

pos = _expect(contents, pos, b"(")
def __call__(
self,
contents: bytes | bytearray,
pos: int,
*,
empty_ok: bool = False,
) -> tuple[list[np.ndarray[tuple[int], np.dtype[np.float64 | np.int64]]], int]:
try:
count, pos = _parse_number(contents, pos, target=int)
except ParseError:
count = None
else:
if count < 0:
raise ParseError(contents, pos, expected="non-negative list count")
pos = _skip(contents, pos)

if match := _UNCOMMENTED_FACES_LIKE_LIST.match(contents, pos):
data = contents[pos : match.end() - 1]
pos = match.end()
pos = _expect(contents, pos, b"(")

elif match := _FACES_LIKE_LIST.match(contents, pos):
data = contents[pos : match.end() - 1]
pos = match.end()
if match := _UNCOMMENTED_LIST_OF_LISTS_LIKE.match(contents, pos):
data = contents[pos : match.end() - 1]
pos = match.end()

data = _COMMENTS.sub(b" ", data)
elif match := _LIST_OF_LISTS_LIKE.match(contents, pos):
data = contents[pos : match.end() - 1]
pos = match.end()

if not match:
raise ParseError(contents, pos, expected="faces-like list")
data = _COMMENTS.sub(b" ", data)

data = data.replace(b"(", b" ").replace(b")", b" ")
try:
data = data.decode("ascii")
except UnicodeDecodeError as e:
raise ParseError(contents, pos, expected="faces-like list") from e
if not match:
raise ParseError(contents, pos, expected="numeric list of lists")

try:
values = np.fromstring(data, sep=" ", dtype=int)
except ValueError as e:
raise ParseError(contents, pos, expected="faces-like list") from e
# Resolve to explicit numpy dtype to ensure platform-consistent bit width
# (Python's `int` maps to int32 on Windows with numpy, but OpenFOAM labels
# should always be 64-bit when read in ASCII).
np_dtype: type = np.int64 if self._dtype is int else np.float64

ret: list[np.ndarray] = []
i = 0
while i < len(values):
n = values[i]
ret.append(values[i + 1 : i + n + 1])
i += n + 1
ret: list[np.ndarray] = []

if count is not None and len(ret) != count:
raise ParseError(contents, pos, expected=f"{count} faces (got {len(ret)})")
if count is None:
# No outer count: validate each sub-list individually so a wrong
# inline count (e.g. "2(1 2 3)") is caught immediately.
# In practice OpenFOAM only omits the outer count for short lists,
# so the per-sublist Python loop is not a performance concern.
for m in _SUBLIST_CAPTURE.finditer(data):
n = int(m.group(1))
if n < 0:
raise ParseError(contents, pos, expected="numeric list of lists")
try:
inner = np.array(
m.group(2).decode("ascii").split(), dtype=np_dtype
)
except (ValueError, UnicodeDecodeError) as e:
raise ParseError(
contents, pos, expected="numeric list of lists"
) from e
if len(inner) != n:
raise ParseError(contents, pos, expected="numeric list of lists")
ret.append(inner)
else:
# Outer count present: use the fast flat-array approach.
# The outer count check below catches any net sublist-count mismatch.
# The in-loop guards prevent crashes on negative or truncated counts.

# Use np.array(data.split()) rather than np.fromstring to:
# - avoid DeprecationWarning from np.fromstring when data contains
# trailing non-numeric content (which we use to detect type mismatch)
# - raise ValueError immediately on any non-parseable token (e.g. a
# float '0.1' when dtype=np.int64), which is caught below as ParseError
data_flat = data.replace(b"(", b" ").replace(b")", b" ")
try:
values = np.array(data_flat.decode("ascii").split(), dtype=np_dtype)
except (ValueError, UnicodeDecodeError) as e:
raise ParseError(
contents, pos, expected="numeric list of lists"
) from e

return ret, pos
i = 0
while i < len(values):
n = int(values[i])
if n < 0 or i + n + 1 > len(values):
raise ParseError(contents, pos, expected="numeric list of lists")
ret.append(values[i + 1 : i + n + 1])
i += n + 1

if count is None:
if not empty_ok and len(ret) == 0:
raise ParseError(
contents, pos, expected="non-empty numeric list of lists"
)
elif len(ret) != count:
raise ParseError(
contents, pos, expected=f"{count} elements (got {len(ret)})"
)

return ret, pos


_parse_ascii_integer_list_list = _ASCIINumericListListParser(dtype=int)
_parse_ascii_float_list_list = _ASCIINumericListListParser(dtype=float)


def _parse_ascii_faces_like_list(
contents: bytes | bytearray, pos: int
) -> tuple[list[np.ndarray[tuple[int], np.dtype[np.int64]]], int]:
return _parse_ascii_integer_list_list(contents, pos)


def _parse_binary_numeric_list(
Expand Down Expand Up @@ -916,6 +917,10 @@ def _parse_standalone_data_entry(
return _parse_ascii_vector_list(contents, pos)
with contextlib.suppress(ParseError):
return _parse_ascii_faces_like_list(contents, pos)
# _parse_ascii_float_list_list is tried after faces-like (integer list-of-lists)
# to handle sparse/non-uniform float lists that look like n(v1 v2 ...) per row.
with contextlib.suppress(ParseError):
return _parse_ascii_float_list_list(contents, pos)

try:
entry1, pos1 = _parse_data(contents, pos)
Expand Down
Loading