tecio-python 0.3.2__py3-none-any.whl → 0.4.0__py3-none-any.whl
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.
- tecio/__init__.py +29 -3
- tecio/_dat_read.py +296 -81
- tecio/_dat_write.py +206 -146
- tecio/_io.py +338 -73
- tecio/_meta.py +6 -6
- tecio/_plt_legacy.py +400 -0
- tecio/_plt_read.py +180 -41
- tecio/_plt_write.py +195 -125
- tecio/_reader.py +82 -37
- tecio/_szl_read.py +68 -61
- tecio/_szl_write.py +249 -188
- tecio/_writer.py +380 -134
- tecio/cli/tec2mat.py +21 -11
- tecio/cli/tecaux.py +77 -93
- tecio/cli/tecdump.py +1 -1
- tecio/cli/tecextract.py +68 -86
- tecio/cli/tecfix.py +45 -64
- tecio/cli/tecmerge.py +343 -66
- tecio/cli/teconvert.py +22 -37
- tecio/cli/tecscale.py +185 -109
- tecio/cli/tecslice.py +75 -100
- tecio/cli/tecstats.py +41 -41
- tecio_python-0.4.0.data/data/share/plugins/matlab/contourfe.m +111 -0
- tecio_python-0.4.0.data/data/share/plugins/matlab/tecmat2struct.m +137 -0
- {tecio_python-0.3.2.data/data/share/tecio → tecio_python-0.4.0.data/data/share}/plugins/paraview/TecplotTecioReader.py +9 -0
- tecio_python-0.4.0.data/data/share/plugins/paraview/TecplotTecioWriter.py +651 -0
- {tecio_python-0.3.2.dist-info → tecio_python-0.4.0.dist-info}/METADATA +1 -1
- tecio_python-0.4.0.dist-info/RECORD +38 -0
- {tecio_python-0.3.2.dist-info → tecio_python-0.4.0.dist-info}/WHEEL +1 -1
- tecio_python-0.3.2.dist-info/RECORD +0 -34
- {tecio_python-0.3.2.dist-info → tecio_python-0.4.0.dist-info}/entry_points.txt +0 -0
- {tecio_python-0.3.2.dist-info → tecio_python-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {tecio_python-0.3.2.dist-info → tecio_python-0.4.0.dist-info}/licenses/NOTICE +0 -0
- {tecio_python-0.3.2.dist-info → tecio_python-0.4.0.dist-info}/top_level.txt +0 -0
tecio/__init__.py
CHANGED
|
@@ -4,12 +4,14 @@ Wraps the TecIO C library for ``.szplt``, ``.plt``, and ``.dat`` formats.
|
|
|
4
4
|
Requires Python 3.10+, NumPy, and a Tecplot 360 installation.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
# ruff: noqa: A004
|
|
8
|
+
|
|
7
9
|
from importlib import metadata
|
|
8
10
|
|
|
9
11
|
try:
|
|
10
12
|
__version__ = metadata.version("tecio")
|
|
11
13
|
except metadata.PackageNotFoundError:
|
|
12
|
-
__version__ = "0.
|
|
14
|
+
__version__ = "0.4.0"
|
|
13
15
|
|
|
14
16
|
from . import cli, libtecio
|
|
15
17
|
from ._constants import (
|
|
@@ -28,7 +30,20 @@ from ._constants import (
|
|
|
28
30
|
from ._containers import VariableList, ZoneList
|
|
29
31
|
from ._dat_read import TecplotDatReader
|
|
30
32
|
from ._dat_write import TecplotDatWriter
|
|
31
|
-
from ._io import
|
|
33
|
+
from ._io import (
|
|
34
|
+
AppendReadWrite,
|
|
35
|
+
AppendWrite,
|
|
36
|
+
FileSummary,
|
|
37
|
+
ZoneSummary,
|
|
38
|
+
get_file_type,
|
|
39
|
+
get_num_variables,
|
|
40
|
+
get_num_zones,
|
|
41
|
+
get_title,
|
|
42
|
+
get_variable_list,
|
|
43
|
+
get_zone_list,
|
|
44
|
+
open,
|
|
45
|
+
peek,
|
|
46
|
+
)
|
|
32
47
|
from ._plt_read import TecplotPltReader
|
|
33
48
|
from ._plt_write import TecplotPltWriter
|
|
34
49
|
from ._reader import (
|
|
@@ -41,7 +56,7 @@ from ._reader import (
|
|
|
41
56
|
)
|
|
42
57
|
from ._szl_read import TecplotSzlReader
|
|
43
58
|
from ._szl_write import TecplotSzlWriter
|
|
44
|
-
from ._writer import TecplotWriter
|
|
59
|
+
from ._writer import EmptyZoneWarning, TecplotWriter
|
|
45
60
|
|
|
46
61
|
# Ensure these display as their canonical public name in docs and help(),
|
|
47
62
|
# rather than the private module they're actually defined in.
|
|
@@ -64,6 +79,7 @@ for _cls in (
|
|
|
64
79
|
TecplotSzlWriter,
|
|
65
80
|
TecplotPltWriter,
|
|
66
81
|
TecplotDatWriter,
|
|
82
|
+
EmptyZoneWarning,
|
|
67
83
|
Boolean,
|
|
68
84
|
DataPacking,
|
|
69
85
|
DataType,
|
|
@@ -82,6 +98,15 @@ del _cls
|
|
|
82
98
|
__all__ = [
|
|
83
99
|
"libtecio",
|
|
84
100
|
"open",
|
|
101
|
+
"peek",
|
|
102
|
+
"get_variable_list",
|
|
103
|
+
"get_zone_list",
|
|
104
|
+
"get_num_zones",
|
|
105
|
+
"get_num_variables",
|
|
106
|
+
"get_title",
|
|
107
|
+
"get_file_type",
|
|
108
|
+
"FileSummary",
|
|
109
|
+
"ZoneSummary",
|
|
85
110
|
"cli",
|
|
86
111
|
"AppendWrite",
|
|
87
112
|
"AppendReadWrite",
|
|
@@ -100,6 +125,7 @@ __all__ = [
|
|
|
100
125
|
"TecplotSzlWriter",
|
|
101
126
|
"TecplotPltWriter",
|
|
102
127
|
"TecplotDatWriter",
|
|
128
|
+
"EmptyZoneWarning",
|
|
103
129
|
"Boolean",
|
|
104
130
|
"DataPacking",
|
|
105
131
|
"DataType",
|
tecio/_dat_read.py
CHANGED
|
@@ -14,8 +14,9 @@ and (for data values) PLT.
|
|
|
14
14
|
from __future__ import annotations
|
|
15
15
|
|
|
16
16
|
import contextlib
|
|
17
|
+
import os
|
|
17
18
|
import re
|
|
18
|
-
from typing import Any
|
|
19
|
+
from typing import Any, NamedTuple
|
|
19
20
|
|
|
20
21
|
import numpy as np
|
|
21
22
|
import numpy.typing as npt
|
|
@@ -36,6 +37,7 @@ from ._reader import (
|
|
|
36
37
|
TecplotReader,
|
|
37
38
|
TecplotVariableReader,
|
|
38
39
|
TecplotZoneReader,
|
|
40
|
+
convert_face_connections_to_zero_based,
|
|
39
41
|
)
|
|
40
42
|
|
|
41
43
|
# --------------------------------------------------------------------------------------
|
|
@@ -299,6 +301,93 @@ def _next_token_is_value(text: str, i: int) -> bool:
|
|
|
299
301
|
return not (j < n and text[j] == "=")
|
|
300
302
|
|
|
301
303
|
|
|
304
|
+
def _determine_zone_type(kv: dict[str, str]) -> ZoneType:
|
|
305
|
+
"""Determine a zone's type from its header key-value pairs.
|
|
306
|
+
|
|
307
|
+
Shared by the full parser and the metadata-only scanner so both agree on the same
|
|
308
|
+
modern/legacy keyword rules.
|
|
309
|
+
|
|
310
|
+
Two header dialects are supported:
|
|
311
|
+
* Modern: ``ZONETYPE=FEQuadrilateral, DATAPACKING=POINT``
|
|
312
|
+
* Legacy: ``F=FEPOINT, ET=QUADRILATERAL``
|
|
313
|
+
|
|
314
|
+
The FE-vs-ordered distinction comes from ``ZONETYPE`` (modern) or ``F``
|
|
315
|
+
(legacy). ``ET`` (element type) only ever appears on finite-element zones, so it
|
|
316
|
+
merely names the element *shape* once a zone is already known to be FE. Modern
|
|
317
|
+
keywords win when present.
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
kv: Zone header key-value pairs (leading ``ZONE`` keyword already stripped), as
|
|
321
|
+
returned by :func:`_kv_split`.
|
|
322
|
+
|
|
323
|
+
Returns:
|
|
324
|
+
The zone's type. Unlike the full parser, does not reject FEPOLYGON/FEPOLYHEDRON,
|
|
325
|
+
those just aren't readable for data, the header itself is perfectly legible.
|
|
326
|
+
|
|
327
|
+
Raises:
|
|
328
|
+
ValueError: If a legacy FE header (``F=FEPOINT``/``FEBLOCK``) is
|
|
329
|
+
missing the required ``ET`` keyword.
|
|
330
|
+
|
|
331
|
+
Example:
|
|
332
|
+
>>> _determine_zone_type({"T": '"z"', "I": "5"})
|
|
333
|
+
<ZoneType.ORDERED: ...>
|
|
334
|
+
"""
|
|
335
|
+
legacy_is_fe: bool | None = None
|
|
336
|
+
if "F" in kv:
|
|
337
|
+
legacy_is_fe, _ = _parse_legacy_format(kv["F"])
|
|
338
|
+
|
|
339
|
+
if "ZONETYPE" in kv:
|
|
340
|
+
zt_raw = kv["ZONETYPE"].rstrip(",").strip().lower()
|
|
341
|
+
return _STR_TO_ZONETYPE.get(zt_raw, ZoneType.ORDERED)
|
|
342
|
+
elif legacy_is_fe is False:
|
|
343
|
+
return ZoneType.ORDERED
|
|
344
|
+
elif legacy_is_fe or "ET" in kv:
|
|
345
|
+
if "ET" not in kv:
|
|
346
|
+
raise ValueError(
|
|
347
|
+
"Legacy FE zone header specifies F=FEPOINT/FEBLOCK but is "
|
|
348
|
+
"missing the required ET (element type) keyword."
|
|
349
|
+
)
|
|
350
|
+
return _parse_legacy_element_type(kv["ET"])
|
|
351
|
+
else:
|
|
352
|
+
return ZoneType.ORDERED
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def _collect_zone_header_lines(tokens: _LineBuffer) -> list[str]:
|
|
356
|
+
"""Collect a ZONE block's header lines, stopping before data begins.
|
|
357
|
+
|
|
358
|
+
Shared by the full parser and the metadata-only scanner (:func:`peek_dat_metadata`)
|
|
359
|
+
so both agree on exactly where a zone's header ends and its data starts.
|
|
360
|
+
|
|
361
|
+
Args:
|
|
362
|
+
tokens: Positioned at the ``ZONE`` line itself (not yet consumed).
|
|
363
|
+
|
|
364
|
+
Returns:
|
|
365
|
+
Raw header lines, including the leading ``ZONE`` keyword line.
|
|
366
|
+
|
|
367
|
+
Example:
|
|
368
|
+
>>> lines = _collect_zone_header_lines(tokens)
|
|
369
|
+
"""
|
|
370
|
+
header_lines: list[str] = [tokens.next_stripped()] # ZONE T=...
|
|
371
|
+
while tokens.has_more():
|
|
372
|
+
nxt = tokens.peek_stripped()
|
|
373
|
+
if not nxt:
|
|
374
|
+
tokens.next_stripped()
|
|
375
|
+
continue
|
|
376
|
+
nxt_upper = nxt.lstrip().upper()
|
|
377
|
+
# Stop at a new zone, top-level keyword, or the first data line.
|
|
378
|
+
if nxt_upper.split("=")[0].split()[0] == "ZONE":
|
|
379
|
+
break
|
|
380
|
+
if nxt_upper.startswith(("DATASETAUXDATA", "VARAUXDATA")):
|
|
381
|
+
break
|
|
382
|
+
first_ch = nxt.lstrip()[0] if nxt.lstrip() else ""
|
|
383
|
+
# A data line begins with a numeric token (covers leading-dot values such as
|
|
384
|
+
# ``.5`` and signed values such as ``-1.2e3``).
|
|
385
|
+
if first_ch in "0123456789+-.":
|
|
386
|
+
break
|
|
387
|
+
header_lines.append(tokens.next_stripped())
|
|
388
|
+
return header_lines
|
|
389
|
+
|
|
390
|
+
|
|
302
391
|
def _kv_split(text: str) -> dict[str, str]:
|
|
303
392
|
"""Parse a loose ``KEY=VALUE`` string into an upper-cased-key dict.
|
|
304
393
|
|
|
@@ -608,9 +697,9 @@ def _parse_auxdata_line(line: str) -> tuple[str, str]:
|
|
|
608
697
|
def _apply_varauxdata(line: str, var_auxdata_list: list[dict[str, str]]) -> None:
|
|
609
698
|
"""Parse ``VARAUXDATA 1-based-idx name="value"`` and store in list.
|
|
610
699
|
|
|
611
|
-
*var_auxdata_list* holds plain, mutable dicts, one per variable
|
|
612
|
-
|
|
613
|
-
|
|
700
|
+
*var_auxdata_list* holds plain, mutable dicts, one per variable, 0-based,
|
|
701
|
+
accumulated during parsing. Wrapped into an immutable TecplotDatAuxDataReader
|
|
702
|
+
lazily, only once parsing is complete.
|
|
614
703
|
|
|
615
704
|
Example:
|
|
616
705
|
>>> _apply_varauxdata(line, var_auxdata_list)
|
|
@@ -618,14 +707,14 @@ def _apply_varauxdata(line: str, var_auxdata_list: list[dict[str, str]]) -> None
|
|
|
618
707
|
m = re.match(r"(?i)VARAUXDATA\s+(\d+)\s+", line)
|
|
619
708
|
if not m:
|
|
620
709
|
return
|
|
621
|
-
var_idx = int(m.group(1)) # 1-based
|
|
710
|
+
var_idx = int(m.group(1)) - 1 # 1-based in text, converted to 0-based
|
|
622
711
|
rest = line[m.end() :]
|
|
623
712
|
eq = rest.find("=")
|
|
624
713
|
if eq < 0:
|
|
625
714
|
return
|
|
626
715
|
name = rest[:eq].strip()
|
|
627
716
|
value = _unquote(rest[eq + 1 :].strip())
|
|
628
|
-
if
|
|
717
|
+
if 0 <= var_idx < len(var_auxdata_list):
|
|
629
718
|
var_auxdata_list[var_idx][name] = value
|
|
630
719
|
|
|
631
720
|
|
|
@@ -839,7 +928,7 @@ class TecplotDatVariableReader(TecplotVariableReader):
|
|
|
839
928
|
|
|
840
929
|
@property
|
|
841
930
|
def shared_zone(self) -> int | None:
|
|
842
|
-
"""
|
|
931
|
+
"""0-based source zone index if shared, else None."""
|
|
843
932
|
return self._shared_zone
|
|
844
933
|
|
|
845
934
|
@property
|
|
@@ -897,9 +986,9 @@ class TecplotDatOrderedZoneReader(TecplotOrderedZoneReader):
|
|
|
897
986
|
self,
|
|
898
987
|
zone_index: int,
|
|
899
988
|
title: str,
|
|
900
|
-
I: int, # noqa: E741
|
|
901
|
-
J: int,
|
|
902
|
-
K: int,
|
|
989
|
+
I: int, # noqa: E741 N803
|
|
990
|
+
J: int, # noqa: N803
|
|
991
|
+
K: int, # noqa: N803
|
|
903
992
|
solution_time: float,
|
|
904
993
|
strand_id: int,
|
|
905
994
|
variables: list[TecplotVariableReader],
|
|
@@ -1022,6 +1111,162 @@ class TecplotDatFEZoneReader(TecplotFEZoneReader):
|
|
|
1022
1111
|
return TecplotDatAuxDataReader(self._aux_raw)
|
|
1023
1112
|
|
|
1024
1113
|
|
|
1114
|
+
class _DatZoneInfo(NamedTuple):
|
|
1115
|
+
"""Per-zone metadata from a cheap header-only scan."""
|
|
1116
|
+
|
|
1117
|
+
title: str
|
|
1118
|
+
zone_type: ZoneType
|
|
1119
|
+
num_nodes: int | None
|
|
1120
|
+
num_elements: int | None
|
|
1121
|
+
|
|
1122
|
+
|
|
1123
|
+
class _DatMetadata(NamedTuple):
|
|
1124
|
+
"""Result of :func:`_peek_dat_metadata`."""
|
|
1125
|
+
|
|
1126
|
+
title: str
|
|
1127
|
+
file_type: FileType
|
|
1128
|
+
variable_names: list[str]
|
|
1129
|
+
zones: list[_DatZoneInfo]
|
|
1130
|
+
|
|
1131
|
+
|
|
1132
|
+
_ZONE_LINE_RE = re.compile(r"^[ \t]*ZONE\b", re.IGNORECASE | re.MULTILINE)
|
|
1133
|
+
_ZONE_DATA_START_RE = re.compile(r"^[ \t]*[0-9+\-.]", re.MULTILINE)
|
|
1134
|
+
|
|
1135
|
+
|
|
1136
|
+
def _peek_dat_metadata(path: str | os.PathLike) -> _DatMetadata:
|
|
1137
|
+
"""Scan a DAT file's title, variable names, and per-zone metadata only.
|
|
1138
|
+
|
|
1139
|
+
Unlike opening the file normally, never converts a single data value to a number. A
|
|
1140
|
+
single bulk regex pass across the whole file finds every zone header's starting
|
|
1141
|
+
line; each zone's metadata is then extracted from just the handful of lines between
|
|
1142
|
+
that start and the next one (or EOF), reusing the same header-parsing logic the real
|
|
1143
|
+
parser uses, so a zone header can still span multiple lines correctly. The data and
|
|
1144
|
+
connectivity blocks in between are never read at all. A line-by-line Python scan
|
|
1145
|
+
over the whole file (rather than this bulk approach) was tried first and measured
|
|
1146
|
+
slower than a full parse for large files, string-heavy per-line loops in Python
|
|
1147
|
+
don't beat NumPy's bulk numeric parsing, even when doing less work; a single C-level
|
|
1148
|
+
regex pass over the whole file does.
|
|
1149
|
+
|
|
1150
|
+
A zone's ``num_nodes``/``num_elements`` are ``None`` only for the one case they
|
|
1151
|
+
genuinely can't be determined without reading data: an ordered zone whose header
|
|
1152
|
+
omits I/J/K entirely, relying on the reader to infer the point count from the data
|
|
1153
|
+
itself (see :meth:`TecplotDatReader._infer_ordered_point_count`). Every other case,
|
|
1154
|
+
including FE zones, always states its node/element counts directly in the header
|
|
1155
|
+
text.
|
|
1156
|
+
|
|
1157
|
+
Args:
|
|
1158
|
+
path: Path to a ``.dat``/``.tec`` file.
|
|
1159
|
+
|
|
1160
|
+
Returns:
|
|
1161
|
+
Title, file type, variable names, and per-zone metadata (in file order).
|
|
1162
|
+
|
|
1163
|
+
Raises:
|
|
1164
|
+
FileNotFoundError: If *path* does not exist.
|
|
1165
|
+
|
|
1166
|
+
Example:
|
|
1167
|
+
>>> meta = _peek_dat_metadata("flow.dat")
|
|
1168
|
+
>>> [z.title for z in meta.zones]
|
|
1169
|
+
['FluidVolume', 'WingSurface']
|
|
1170
|
+
"""
|
|
1171
|
+
with open(path, encoding="utf-8", errors="replace") as fh:
|
|
1172
|
+
text = fh.read()
|
|
1173
|
+
|
|
1174
|
+
zone_starts = [m.start() for m in _ZONE_LINE_RE.finditer(text)]
|
|
1175
|
+
|
|
1176
|
+
# File header: everything before the first zone (or the whole file, if it somehow
|
|
1177
|
+
# has none). Mirrors TecplotDatReader._parse_file_header, kept separate rather than
|
|
1178
|
+
# shared with it since that method also populates dataset aux data, which this
|
|
1179
|
+
# function deliberately doesn't scan for. Just a handful of lines regardless of file
|
|
1180
|
+
# size, so no bulk-regex treatment needed here.
|
|
1181
|
+
header_end = zone_starts[0] if zone_starts else len(text)
|
|
1182
|
+
tokens = _LineBuffer(text[:header_end].splitlines(keepends=True))
|
|
1183
|
+
|
|
1184
|
+
title = ""
|
|
1185
|
+
file_type = FileType.FULL
|
|
1186
|
+
variable_names: list[str] = []
|
|
1187
|
+
while tokens.has_more():
|
|
1188
|
+
line = tokens.peek_stripped()
|
|
1189
|
+
if not line:
|
|
1190
|
+
tokens.next_stripped()
|
|
1191
|
+
continue
|
|
1192
|
+
upper_key = (
|
|
1193
|
+
line.split("=")[0].strip().upper() if "=" in line else line.strip().upper()
|
|
1194
|
+
)
|
|
1195
|
+
tokens.next_stripped()
|
|
1196
|
+
|
|
1197
|
+
if upper_key == "TITLE":
|
|
1198
|
+
rhs = line.split("=", 1)[1].strip() if "=" in line else ""
|
|
1199
|
+
title = _unquote(rhs)
|
|
1200
|
+
elif upper_key == "FILETYPE":
|
|
1201
|
+
rhs = line.split("=", 1)[1].strip() if "=" in line else ""
|
|
1202
|
+
file_type = _STR_TO_FILETYPE.get(rhs.strip().lower(), FileType.FULL)
|
|
1203
|
+
elif upper_key == "VARIABLES":
|
|
1204
|
+
rhs = line.split("=", 1)[1].strip() if "=" in line else ""
|
|
1205
|
+
names = _extract_quoted_strings(rhs)
|
|
1206
|
+
while tokens.has_more():
|
|
1207
|
+
nxt = tokens.peek_stripped()
|
|
1208
|
+
if not nxt:
|
|
1209
|
+
tokens.next_stripped()
|
|
1210
|
+
continue
|
|
1211
|
+
if nxt.lstrip().startswith('"'):
|
|
1212
|
+
names.extend(_extract_quoted_strings(tokens.next_stripped()))
|
|
1213
|
+
elif "=" not in nxt.split("#")[0]:
|
|
1214
|
+
tokens.next_stripped()
|
|
1215
|
+
else:
|
|
1216
|
+
break
|
|
1217
|
+
variable_names = names
|
|
1218
|
+
|
|
1219
|
+
# Zones: each zone's header lives in the small window between its start and the next
|
|
1220
|
+
# zone's start (or EOF); _collect_zone_header_lines only ever looks at the first
|
|
1221
|
+
# handful of lines in that window before hitting what looks like data, so this stays
|
|
1222
|
+
# cheap regardless of how much data actually follows.
|
|
1223
|
+
zones: list[_DatZoneInfo] = []
|
|
1224
|
+
for i, start in enumerate(zone_starts):
|
|
1225
|
+
end = zone_starts[i + 1] if i + 1 < len(zone_starts) else len(text)
|
|
1226
|
+
# Bounded search directly on the original string
|
|
1227
|
+
data_match = _ZONE_DATA_START_RE.search(text, start, end)
|
|
1228
|
+
header_end = data_match.start() if data_match else end
|
|
1229
|
+
window_tokens = _LineBuffer(text[start:header_end].splitlines(keepends=True))
|
|
1230
|
+
header_lines = _collect_zone_header_lines(window_tokens)
|
|
1231
|
+
header_text = " ".join(header_lines)
|
|
1232
|
+
m_zone = re.match(r"(?i)^ZONE\s*", header_text)
|
|
1233
|
+
if m_zone:
|
|
1234
|
+
header_text = header_text[m_zone.end() :]
|
|
1235
|
+
kv = _kv_split(header_text)
|
|
1236
|
+
zone_title = _unquote(kv.get("T", ""))
|
|
1237
|
+
|
|
1238
|
+
try:
|
|
1239
|
+
zone_type = _determine_zone_type(kv)
|
|
1240
|
+
except ValueError:
|
|
1241
|
+
# A legacy header F=FEPOINT with no ET reports as unknown
|
|
1242
|
+
zones.append(_DatZoneInfo(zone_title, ZoneType.ORDERED, None, None))
|
|
1243
|
+
continue
|
|
1244
|
+
|
|
1245
|
+
num_nodes: int | None
|
|
1246
|
+
num_elements: int | None
|
|
1247
|
+
if zone_type == ZoneType.ORDERED:
|
|
1248
|
+
if "I" not in kv and "J" not in kv and "K" not in kv:
|
|
1249
|
+
# Point count only knowable by reading data -> report unknown
|
|
1250
|
+
num_nodes = None
|
|
1251
|
+
num_elements = None
|
|
1252
|
+
else:
|
|
1253
|
+
zi = int(kv.get("I", "1") or "1")
|
|
1254
|
+
zj = int(kv.get("J", "1") or "1")
|
|
1255
|
+
zk = int(kv.get("K", "1") or "1")
|
|
1256
|
+
num_nodes = zi * zj * zk
|
|
1257
|
+
num_elements = max(zi - 1, 1) * max(zj - 1, 1) * max(zk - 1, 1)
|
|
1258
|
+
else:
|
|
1259
|
+
# FE zones (including FEPOLYGON/FEPOLYHEDRON, which the full parser can't
|
|
1260
|
+
# read data for, but whose header is just as legible as any other zone's)
|
|
1261
|
+
# always state counts directly.
|
|
1262
|
+
num_nodes = int(kv.get("NODES", kv.get("N", "0")) or "0")
|
|
1263
|
+
num_elements = int(kv.get("ELEMENTS", kv.get("E", "0")) or "0")
|
|
1264
|
+
|
|
1265
|
+
zones.append(_DatZoneInfo(zone_title, zone_type, num_nodes, num_elements))
|
|
1266
|
+
|
|
1267
|
+
return _DatMetadata(title, file_type, variable_names, zones)
|
|
1268
|
+
|
|
1269
|
+
|
|
1025
1270
|
class TecplotDatReader(TecplotReader):
|
|
1026
1271
|
"""Reader for Tecplot ASCII DAT files.
|
|
1027
1272
|
|
|
@@ -1062,9 +1307,9 @@ class TecplotDatReader(TecplotReader):
|
|
|
1062
1307
|
# TecplotDatAuxDataReader lazily, only once fully parsed.
|
|
1063
1308
|
self._dataset_auxdata_raw: dict[str, str] = {}
|
|
1064
1309
|
self._dataset_auxdata: TecplotAuxDataReader | None = None
|
|
1065
|
-
#
|
|
1066
|
-
#
|
|
1067
|
-
self._var_auxdata_raw: list[dict[str, str]] = [
|
|
1310
|
+
# 0-based, one dict per variable; populated once num_vars is known, see
|
|
1311
|
+
# _parse().
|
|
1312
|
+
self._var_auxdata_raw: list[dict[str, str]] = []
|
|
1068
1313
|
# Raw VARAUXDATA lines seen before the first zone, buffered by
|
|
1069
1314
|
# _parse_file_header() and applied once _var_auxdata_raw is allocated with the
|
|
1070
1315
|
# correct length (num_vars isn't known until the header finishes parsing).
|
|
@@ -1133,7 +1378,7 @@ class TecplotDatReader(TecplotReader):
|
|
|
1133
1378
|
self._parse_file_header(tokens)
|
|
1134
1379
|
|
|
1135
1380
|
# Build per-variable aux data slots now that num_vars is known.
|
|
1136
|
-
self._var_auxdata_raw = [{}
|
|
1381
|
+
self._var_auxdata_raw = [{} for _ in range(self.num_vars)]
|
|
1137
1382
|
|
|
1138
1383
|
# Now that _var_auxdata exists, apply any VARAUXDATA lines that appeared before
|
|
1139
1384
|
# the first zone
|
|
@@ -1143,8 +1388,10 @@ class TecplotDatReader(TecplotReader):
|
|
|
1143
1388
|
|
|
1144
1389
|
while tokens.has_more():
|
|
1145
1390
|
line = tokens.peek_stripped()
|
|
1391
|
+
if not line:
|
|
1392
|
+
tokens.next_stripped()
|
|
1393
|
+
continue
|
|
1146
1394
|
upper = line.upper()
|
|
1147
|
-
# if upper.startswith("ZONE"):
|
|
1148
1395
|
if upper.lstrip().split("=")[0].split()[0] == "ZONE":
|
|
1149
1396
|
self._parse_zone(tokens)
|
|
1150
1397
|
elif upper.startswith("DATASETAUXDATA"):
|
|
@@ -1232,7 +1479,9 @@ class TecplotDatReader(TecplotReader):
|
|
|
1232
1479
|
def _infer_ordered_point_count(tokens: _LineBuffer, active_count: int) -> int:
|
|
1233
1480
|
"""Infer an ordered zone's point count when I/J/K are all omitted.
|
|
1234
1481
|
|
|
1235
|
-
|
|
1482
|
+
Some legacy exporters write a bare ``ZONE F=POINT`` header with no dimensions at
|
|
1483
|
+
all, relying on the reader to count data rows itself. Peeks ahead (never
|
|
1484
|
+
consuming) counting numeric tokens up to the next zone/keyword boundary or end
|
|
1236
1485
|
of file, using the same boundary rule :meth:`_parse_zone` already uses to know
|
|
1237
1486
|
where a header ends and data begins, then restores the original position so the
|
|
1238
1487
|
real read (later, driven by the now-known count) starts from the same place.
|
|
@@ -1283,25 +1532,7 @@ class TecplotDatReader(TecplotReader):
|
|
|
1283
1532
|
"""
|
|
1284
1533
|
# -- Collect header lines ------------------------------------------------------
|
|
1285
1534
|
|
|
1286
|
-
header_lines
|
|
1287
|
-
|
|
1288
|
-
while tokens.has_more():
|
|
1289
|
-
nxt = tokens.peek_stripped()
|
|
1290
|
-
if not nxt:
|
|
1291
|
-
tokens.next_stripped()
|
|
1292
|
-
continue
|
|
1293
|
-
nxt_upper = nxt.lstrip().upper()
|
|
1294
|
-
# Stop at a new zone, top-level keyword, or the first data line.
|
|
1295
|
-
if nxt_upper.split("=")[0].split()[0] == "ZONE":
|
|
1296
|
-
break
|
|
1297
|
-
if nxt_upper.startswith(("DATASETAUXDATA", "VARAUXDATA")):
|
|
1298
|
-
break
|
|
1299
|
-
first_ch = nxt.lstrip()[0] if nxt.lstrip() else ""
|
|
1300
|
-
# A data line begins with a numeric token (covers leading-dot values such as
|
|
1301
|
-
# ``.5`` and signed values such as ``-1.2e3``).
|
|
1302
|
-
if first_ch in "0123456789+-.":
|
|
1303
|
-
break
|
|
1304
|
-
header_lines.append(tokens.next_stripped())
|
|
1535
|
+
header_lines = _collect_zone_header_lines(tokens)
|
|
1305
1536
|
|
|
1306
1537
|
header_text = " ".join(header_lines)
|
|
1307
1538
|
|
|
@@ -1320,38 +1551,13 @@ class TecplotDatReader(TecplotReader):
|
|
|
1320
1551
|
|
|
1321
1552
|
# -- Determine zone type and data packing --------------------------------------
|
|
1322
1553
|
#
|
|
1323
|
-
#
|
|
1324
|
-
#
|
|
1325
|
-
# * Legacy: F=FEPOINT, ET=QUADRILATERAL
|
|
1326
|
-
#
|
|
1327
|
-
# The FE-vs-ordered distinction comes from ``ZONETYPE`` (modern) or ``F``
|
|
1328
|
-
# (legacy). ``ET`` (element type) only ever appears on finite-element zones —
|
|
1329
|
-
# ordered/structured data has no elements — so it merely names the element
|
|
1330
|
-
# *shape* once a zone is already known to be FE, and is ignored on ordered
|
|
1331
|
-
# zones. Modern keywords win when present.
|
|
1332
|
-
legacy_is_fe: bool | None = None
|
|
1554
|
+
# See _determine_zone_type for the modern/legacy keyword rules this shares with
|
|
1555
|
+
# the metadata-only scanner (peek_dat_metadata).
|
|
1333
1556
|
legacy_packing: DataPacking | None = None
|
|
1334
1557
|
if "F" in kv:
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
zt_raw = kv["ZONETYPE"].rstrip(",").strip().lower()
|
|
1339
|
-
zone_type = _STR_TO_ZONETYPE.get(zt_raw, ZoneType.ORDERED)
|
|
1340
|
-
elif legacy_is_fe is False:
|
|
1341
|
-
# F=POINT/BLOCK: ordered zone. A stray ET (if any) does not apply.
|
|
1342
|
-
zone_type = ZoneType.ORDERED
|
|
1343
|
-
elif legacy_is_fe or "ET" in kv:
|
|
1344
|
-
# Finite-element zone: F=FEPOINT/FEBLOCK, or an ET keyword with no F (some
|
|
1345
|
-
# exporters omit F). The element shape comes from ET, which is then
|
|
1346
|
-
# required.
|
|
1347
|
-
if "ET" not in kv:
|
|
1348
|
-
raise ValueError(
|
|
1349
|
-
"Legacy FE zone header specifies F=FEPOINT/FEBLOCK but is "
|
|
1350
|
-
"missing the required ET (element type) keyword."
|
|
1351
|
-
)
|
|
1352
|
-
zone_type = _parse_legacy_element_type(kv["ET"])
|
|
1353
|
-
else:
|
|
1354
|
-
zone_type = ZoneType.ORDERED
|
|
1558
|
+
_, legacy_packing = _parse_legacy_format(kv["F"])
|
|
1559
|
+
|
|
1560
|
+
zone_type = _determine_zone_type(kv)
|
|
1355
1561
|
|
|
1356
1562
|
if zone_type in _FE_POLY:
|
|
1357
1563
|
raise ValueError(
|
|
@@ -1372,15 +1578,15 @@ class TecplotDatReader(TecplotReader):
|
|
|
1372
1578
|
|
|
1373
1579
|
if zone_type == ZoneType.ORDERED:
|
|
1374
1580
|
if "I" not in kv and "J" not in kv and "K" not in kv:
|
|
1375
|
-
# Infer
|
|
1581
|
+
# Infer point count
|
|
1376
1582
|
active_count = self.num_vars - len(passive_set) - len(share_map)
|
|
1377
1583
|
num_nodes = self._infer_ordered_point_count(tokens, active_count)
|
|
1378
1584
|
I = num_nodes # noqa E741
|
|
1379
|
-
J = K = 1
|
|
1585
|
+
J = K = 1 # noqa: N806
|
|
1380
1586
|
else:
|
|
1381
|
-
I = int(kv.get("I", "1") or "1") # noqa E741
|
|
1382
|
-
J = int(kv.get("J", "1") or "1")
|
|
1383
|
-
K = int(kv.get("K", "1") or "1")
|
|
1587
|
+
I = int(kv.get("I", "1") or "1") # noqa: E741 N806
|
|
1588
|
+
J = int(kv.get("J", "1") or "1") # noqa: N806
|
|
1589
|
+
K = int(kv.get("K", "1") or "1") # noqa: N806
|
|
1384
1590
|
num_nodes = I * J * K
|
|
1385
1591
|
num_cells = max(I - 1, 1) * max(J - 1, 1) * max(K - 1, 1)
|
|
1386
1592
|
else:
|
|
@@ -1491,12 +1697,15 @@ class TecplotDatReader(TecplotReader):
|
|
|
1491
1697
|
else:
|
|
1492
1698
|
nodes_per_cell = _NODES_PER_ELEM[zone_type]
|
|
1493
1699
|
flat = self._read_int_block(tokens, num_cells * nodes_per_cell)
|
|
1494
|
-
|
|
1700
|
+
# Node numbers are 1-based in ASCII; convert to 0-based indices
|
|
1701
|
+
# that point directly into the corresponding coordinate array.
|
|
1702
|
+
node_map = flat.reshape(num_cells, nodes_per_cell) - 1
|
|
1495
1703
|
|
|
1496
1704
|
# -- Read face-neighbor connections (FE zones only, if declared) ---------------
|
|
1497
1705
|
#
|
|
1498
|
-
# Immediately follows connectivity in the file. Values are
|
|
1499
|
-
#
|
|
1706
|
+
# Immediately follows connectivity in the file. Values are 1-based in ASCII;
|
|
1707
|
+
# cell references, remote-zone references, and the face ordinal are all
|
|
1708
|
+
# converted to 0-based below.
|
|
1500
1709
|
|
|
1501
1710
|
face_connections: npt.NDArray[np.int64] | None = None
|
|
1502
1711
|
one_to_one = (
|
|
@@ -1510,6 +1719,9 @@ class TecplotDatReader(TecplotReader):
|
|
|
1510
1719
|
face_connections = self._read_int_block(
|
|
1511
1720
|
tokens, declared_face_connections * values_per
|
|
1512
1721
|
)
|
|
1722
|
+
face_connections = convert_face_connections_to_zero_based(
|
|
1723
|
+
face_connections, face_neighbor_mode
|
|
1724
|
+
)
|
|
1513
1725
|
elif face_neighbor_mode is not None:
|
|
1514
1726
|
# "Many" modes: ragged, each record's own 4th value (nz) gives the
|
|
1515
1727
|
# count of additional neighbor references that follow it.
|
|
@@ -1525,6 +1737,9 @@ class TecplotDatReader(TecplotReader):
|
|
|
1525
1737
|
face_connections = (
|
|
1526
1738
|
np.concatenate(pieces) if pieces else np.empty(0, dtype=np.int64)
|
|
1527
1739
|
)
|
|
1740
|
+
face_connections = convert_face_connections_to_zero_based(
|
|
1741
|
+
face_connections, face_neighbor_mode
|
|
1742
|
+
)
|
|
1528
1743
|
|
|
1529
1744
|
# -- Build ReadVariable and ReadZone objects -----------------------------------
|
|
1530
1745
|
|
|
@@ -1553,13 +1768,13 @@ class TecplotDatReader(TecplotReader):
|
|
|
1553
1768
|
self._zones[src_zone_1based - 1].variables[var_idx].values
|
|
1554
1769
|
)
|
|
1555
1770
|
|
|
1556
|
-
# This zone's own
|
|
1557
|
-
zone_index = len(self._zones)
|
|
1771
|
+
# This zone's own 0-based index
|
|
1772
|
+
zone_index = len(self._zones)
|
|
1558
1773
|
|
|
1559
1774
|
read_vars: list[TecplotVariableReader] = [
|
|
1560
1775
|
TecplotDatVariableReader(
|
|
1561
1776
|
zone_index=zone_index,
|
|
1562
|
-
var_index=idx
|
|
1777
|
+
var_index=idx,
|
|
1563
1778
|
name=name,
|
|
1564
1779
|
data=_shaped(
|
|
1565
1780
|
var_arrays[idx],
|
|
@@ -1567,7 +1782,7 @@ class TecplotDatReader(TecplotReader):
|
|
|
1567
1782
|
),
|
|
1568
1783
|
value_location=var_locs.get(idx, ValueLocation.NODAL),
|
|
1569
1784
|
is_passive=(idx in passive_set),
|
|
1570
|
-
shared_zone=share_map
|
|
1785
|
+
shared_zone=(share_map[idx] - 1 if idx in share_map else None),
|
|
1571
1786
|
)
|
|
1572
1787
|
for idx, name in enumerate(self._variable_names)
|
|
1573
1788
|
]
|
|
@@ -1586,7 +1801,7 @@ class TecplotDatReader(TecplotReader):
|
|
|
1586
1801
|
auxdata=zone_aux,
|
|
1587
1802
|
node_map=node_map,
|
|
1588
1803
|
datapacking=packing,
|
|
1589
|
-
shared_connectivity=con_share_zone if con_share_zone else None,
|
|
1804
|
+
shared_connectivity=(con_share_zone - 1) if con_share_zone else None,
|
|
1590
1805
|
face_neighbor_mode=face_neighbor_mode,
|
|
1591
1806
|
num_face_connections=declared_face_connections or None,
|
|
1592
1807
|
face_connections=face_connections,
|
|
@@ -1599,9 +1814,9 @@ class TecplotDatReader(TecplotReader):
|
|
|
1599
1814
|
zone_index: int,
|
|
1600
1815
|
title: str,
|
|
1601
1816
|
zone_type: ZoneType,
|
|
1602
|
-
I: int, # noqa: E741
|
|
1603
|
-
J: int,
|
|
1604
|
-
K: int,
|
|
1817
|
+
I: int, # noqa: E741 N803
|
|
1818
|
+
J: int, # noqa: N803
|
|
1819
|
+
K: int, # noqa: N803
|
|
1605
1820
|
solution_time: float,
|
|
1606
1821
|
strand_id: int,
|
|
1607
1822
|
variables: list[TecplotVariableReader],
|