tecio-python 0.3.2__tar.gz → 0.3.3__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.
- {tecio_python-0.3.2/tecio_python.egg-info → tecio_python-0.3.3}/PKG-INFO +1 -1
- tecio_python-0.3.3/plugins/matlab/fecplot.m +111 -0
- {tecio_python-0.3.2 → tecio_python-0.3.3}/plugins/paraview/TecplotTecioReader.py +9 -0
- {tecio_python-0.3.2 → tecio_python-0.3.3}/pyproject.toml +3 -2
- {tecio_python-0.3.2 → tecio_python-0.3.3}/tecio/__init__.py +24 -2
- {tecio_python-0.3.2 → tecio_python-0.3.3}/tecio/_dat_read.py +258 -53
- {tecio_python-0.3.2 → tecio_python-0.3.3}/tecio/_io.py +251 -2
- {tecio_python-0.3.2 → tecio_python-0.3.3}/tecio/cli/tecmerge.py +278 -8
- {tecio_python-0.3.2 → tecio_python-0.3.3/tecio_python.egg-info}/PKG-INFO +1 -1
- {tecio_python-0.3.2 → tecio_python-0.3.3}/tecio_python.egg-info/SOURCES.txt +1 -0
- {tecio_python-0.3.2 → tecio_python-0.3.3}/tests/test_io.py +178 -0
- {tecio_python-0.3.2 → tecio_python-0.3.3}/LICENSE +0 -0
- {tecio_python-0.3.2 → tecio_python-0.3.3}/NOTICE +0 -0
- {tecio_python-0.3.2 → tecio_python-0.3.3}/README.md +0 -0
- {tecio_python-0.3.2 → tecio_python-0.3.3}/setup.cfg +0 -0
- {tecio_python-0.3.2 → tecio_python-0.3.3}/tecio/_constants.py +0 -0
- {tecio_python-0.3.2 → tecio_python-0.3.3}/tecio/_containers.py +0 -0
- {tecio_python-0.3.2 → tecio_python-0.3.3}/tecio/_dat_write.py +0 -0
- {tecio_python-0.3.2 → tecio_python-0.3.3}/tecio/_meta.py +0 -0
- {tecio_python-0.3.2 → tecio_python-0.3.3}/tecio/_plt_read.py +0 -0
- {tecio_python-0.3.2 → tecio_python-0.3.3}/tecio/_plt_write.py +0 -0
- {tecio_python-0.3.2 → tecio_python-0.3.3}/tecio/_reader.py +0 -0
- {tecio_python-0.3.2 → tecio_python-0.3.3}/tecio/_szl_read.py +0 -0
- {tecio_python-0.3.2 → tecio_python-0.3.3}/tecio/_szl_write.py +0 -0
- {tecio_python-0.3.2 → tecio_python-0.3.3}/tecio/_utils.py +0 -0
- {tecio_python-0.3.2 → tecio_python-0.3.3}/tecio/_writer.py +0 -0
- {tecio_python-0.3.2 → tecio_python-0.3.3}/tecio/cli/__init__.py +0 -0
- {tecio_python-0.3.2 → tecio_python-0.3.3}/tecio/cli/tec2mat.py +0 -0
- {tecio_python-0.3.2 → tecio_python-0.3.3}/tecio/cli/tecaux.py +0 -0
- {tecio_python-0.3.2 → tecio_python-0.3.3}/tecio/cli/tecdump.py +0 -0
- {tecio_python-0.3.2 → tecio_python-0.3.3}/tecio/cli/tecextract.py +0 -0
- {tecio_python-0.3.2 → tecio_python-0.3.3}/tecio/cli/tecfix.py +0 -0
- {tecio_python-0.3.2 → tecio_python-0.3.3}/tecio/cli/teconvert.py +0 -0
- {tecio_python-0.3.2 → tecio_python-0.3.3}/tecio/cli/tecscale.py +0 -0
- {tecio_python-0.3.2 → tecio_python-0.3.3}/tecio/cli/tecslice.py +0 -0
- {tecio_python-0.3.2 → tecio_python-0.3.3}/tecio/cli/tecstats.py +0 -0
- {tecio_python-0.3.2 → tecio_python-0.3.3}/tecio/libtecio.py +0 -0
- {tecio_python-0.3.2 → tecio_python-0.3.3}/tecio_python.egg-info/dependency_links.txt +0 -0
- {tecio_python-0.3.2 → tecio_python-0.3.3}/tecio_python.egg-info/entry_points.txt +0 -0
- {tecio_python-0.3.2 → tecio_python-0.3.3}/tecio_python.egg-info/requires.txt +0 -0
- {tecio_python-0.3.2 → tecio_python-0.3.3}/tecio_python.egg-info/top_level.txt +0 -0
- {tecio_python-0.3.2 → tecio_python-0.3.3}/tests/test_cli.py +0 -0
- {tecio_python-0.3.2 → tecio_python-0.3.3}/tests/test_libtecio.py +0 -0
- {tecio_python-0.3.2 → tecio_python-0.3.3}/tests/test_read.py +0 -0
- {tecio_python-0.3.2 → tecio_python-0.3.3}/tests/test_write.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tecio-python
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.3
|
|
4
4
|
Summary: Python interface for reading and writing Tecplot data files
|
|
5
5
|
Project-URL: Homepage, https://github.com/meersman/tecio
|
|
6
6
|
Project-URL: Documentation, https://meersman.github.io/tecio/
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
function f = fecplot(dim, x, y, z, c, node_map, varargin)
|
|
2
|
+
% Filled patch contour for unstructured 2D/3D surface zones
|
|
3
|
+
%
|
|
4
|
+
% Call:
|
|
5
|
+
% h = feplot(dim, x, y, z, c, node_map, ...)
|
|
6
|
+
%
|
|
7
|
+
% Args:
|
|
8
|
+
% dim: 2 or 3
|
|
9
|
+
% x,y[,z]: nodal coordinate column vectors (Nx1)
|
|
10
|
+
% c: contour values; either nodal (Nx1) or cell-centered (Mcells x 1)
|
|
11
|
+
% node_map: M x K integer matrix of node indices per cell (K=3 or 4)
|
|
12
|
+
% varargin: name-value pairs forwarded to patch (e.g. 'EdgeColor','none')
|
|
13
|
+
%
|
|
14
|
+
% Returns:
|
|
15
|
+
% f: handle to the patch object
|
|
16
|
+
%
|
|
17
|
+
% Notes:
|
|
18
|
+
% - If c is nodal, uses 'FaceVertexCData' with 'FaceColor'='interp'
|
|
19
|
+
% - If c is cell-centered, uses 'CData' per-face with 'FaceColor'='flat'
|
|
20
|
+
% - For dim==3, z must be provided and plotting uses 3D patch (viewable with lighting)
|
|
21
|
+
% - node_map may contain NaNs for unequal-sided cells; those faces are handled
|
|
22
|
+
|
|
23
|
+
% Validate dim
|
|
24
|
+
if ~ismember(dim, [2,3])
|
|
25
|
+
error('dim must be 2 or 3');
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
% Ensure column vectors
|
|
29
|
+
x = x(:); y = y(:);
|
|
30
|
+
if dim == 3
|
|
31
|
+
if nargin < 4 || isempty(z)
|
|
32
|
+
error('z must be provided for dim==3');
|
|
33
|
+
end
|
|
34
|
+
z = z(:);
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
% Determine number of nodes and faces
|
|
38
|
+
nNodes = numel(x);
|
|
39
|
+
[nFaces, ~] = size(node_map);
|
|
40
|
+
|
|
41
|
+
% Validate node_map indices
|
|
42
|
+
if any(node_map(:) > nNodes) || any(node_map(:) < 0 & ~isnan(node_map(:)))
|
|
43
|
+
error('node_map contains invalid node indices.');
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
% Prepare vertices matrix
|
|
47
|
+
if dim == 2
|
|
48
|
+
verts = [x, y];
|
|
49
|
+
else
|
|
50
|
+
verts = [x, y, z];
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
% Determine if c is nodal or cell-centered
|
|
54
|
+
c = c(:);
|
|
55
|
+
isNodal = (numel(c) == nNodes);
|
|
56
|
+
isCell = (numel(c) == nFaces);
|
|
57
|
+
if ~(isNodal || isCell)
|
|
58
|
+
error('Length of c must match number of nodes (nodal) or number of faces (cell-centered).');
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
% Clean node_map: replace zeros -> NaN (if any), ensure double
|
|
62
|
+
node_map = double(node_map);
|
|
63
|
+
node_map(node_map==0) = NaN;
|
|
64
|
+
|
|
65
|
+
% If node_map contains NaNs for some cells, patch accepts NaN-separated faces.
|
|
66
|
+
% Create faces in appropriate format: patch accepts face vertex indices matrix.
|
|
67
|
+
faces = node_map;
|
|
68
|
+
|
|
69
|
+
% Build patch arguments
|
|
70
|
+
pArgs = varargin;
|
|
71
|
+
|
|
72
|
+
if isNodal
|
|
73
|
+
% Nodal data: use FaceVertexCData with interpolation
|
|
74
|
+
% Create patch with vertices and faces, supply FaceVertexCData
|
|
75
|
+
% Set FaceColor to 'interp' and EdgeColor as provided or 'none' by default
|
|
76
|
+
if ~any(strcmpi('FaceColor',pArgs))
|
|
77
|
+
pArgs = [{'FaceColor','interp'}, pArgs];
|
|
78
|
+
end
|
|
79
|
+
if ~any(strcmpi('EdgeColor',pArgs))
|
|
80
|
+
pArgs = [{'EdgeColor','none'}, pArgs];
|
|
81
|
+
end
|
|
82
|
+
% Create patch
|
|
83
|
+
f = patch('Vertices', verts, 'Faces', faces, 'FaceVertexCData', c, pArgs{:});
|
|
84
|
+
|
|
85
|
+
else
|
|
86
|
+
% Cell-centered: supply per-face CData and use flat coloring
|
|
87
|
+
if ~any(strcmpi('FaceColor',pArgs))
|
|
88
|
+
pArgs = [{'FaceColor','flat'}, pArgs];
|
|
89
|
+
end
|
|
90
|
+
if ~any(strcmpi('EdgeColor',pArgs))
|
|
91
|
+
pArgs = [{'EdgeColor','none'}, pArgs];
|
|
92
|
+
end
|
|
93
|
+
% patch expects CData as Mx1 or Mx3 color; provide as Mx1
|
|
94
|
+
faceCData = c;
|
|
95
|
+
% Create patch
|
|
96
|
+
f = patch('Vertices', verts, 'Faces', faces, 'CData', faceCData, pArgs{:});
|
|
97
|
+
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
% Set colormap and colorbar behavior consistent with contourf
|
|
101
|
+
if ~any(strcmpi('EdgeColor',pArgs))
|
|
102
|
+
set(f,'EdgeColor','none');
|
|
103
|
+
end
|
|
104
|
+
axis equal
|
|
105
|
+
if dim == 3
|
|
106
|
+
view(3)
|
|
107
|
+
camlight headlight
|
|
108
|
+
lighting gouraud
|
|
109
|
+
end
|
|
110
|
+
colorbar
|
|
111
|
+
end
|
|
@@ -170,6 +170,9 @@ _AXIS_SYNONYMS: dict[str, frozenset[str]] = {
|
|
|
170
170
|
"coordx",
|
|
171
171
|
"xcoord",
|
|
172
172
|
"x_coord",
|
|
173
|
+
"xgrid",
|
|
174
|
+
"x-grid",
|
|
175
|
+
"x_grid",
|
|
173
176
|
}),
|
|
174
177
|
"y": frozenset({
|
|
175
178
|
"y",
|
|
@@ -180,6 +183,9 @@ _AXIS_SYNONYMS: dict[str, frozenset[str]] = {
|
|
|
180
183
|
"coordy",
|
|
181
184
|
"ycoord",
|
|
182
185
|
"y_coord",
|
|
186
|
+
"ygrid",
|
|
187
|
+
"y-grid",
|
|
188
|
+
"y_grid",
|
|
183
189
|
}),
|
|
184
190
|
"z": frozenset({
|
|
185
191
|
"z",
|
|
@@ -190,6 +196,9 @@ _AXIS_SYNONYMS: dict[str, frozenset[str]] = {
|
|
|
190
196
|
"coordz",
|
|
191
197
|
"zcoord",
|
|
192
198
|
"z_coord",
|
|
199
|
+
"zgrid",
|
|
200
|
+
"z-grid",
|
|
201
|
+
"z_grid",
|
|
193
202
|
}),
|
|
194
203
|
}
|
|
195
204
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "tecio-python"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.3"
|
|
8
8
|
description = "Python interface for reading and writing Tecplot data files"
|
|
9
9
|
|
|
10
10
|
readme = "README.md"
|
|
@@ -70,7 +70,8 @@ include = ["tecio*"]
|
|
|
70
70
|
exclude = ["archive*", "test*"]
|
|
71
71
|
|
|
72
72
|
[tool.setuptools.data-files]
|
|
73
|
-
"share/
|
|
73
|
+
"share/plugins/paraview" = ["plugins/paraview/*"]
|
|
74
|
+
"share/plugins/matlab" = ["plugins/matlab/*"]
|
|
74
75
|
|
|
75
76
|
[tool.ruff]
|
|
76
77
|
line-length = 88
|
|
@@ -9,7 +9,7 @@ from importlib import metadata
|
|
|
9
9
|
try:
|
|
10
10
|
__version__ = metadata.version("tecio")
|
|
11
11
|
except metadata.PackageNotFoundError:
|
|
12
|
-
__version__ = "0.3.
|
|
12
|
+
__version__ = "0.3.3"
|
|
13
13
|
|
|
14
14
|
from . import cli, libtecio
|
|
15
15
|
from ._constants import (
|
|
@@ -28,7 +28,20 @@ from ._constants import (
|
|
|
28
28
|
from ._containers import VariableList, ZoneList
|
|
29
29
|
from ._dat_read import TecplotDatReader
|
|
30
30
|
from ._dat_write import TecplotDatWriter
|
|
31
|
-
from ._io import
|
|
31
|
+
from ._io import (
|
|
32
|
+
AppendReadWrite,
|
|
33
|
+
AppendWrite,
|
|
34
|
+
FileSummary,
|
|
35
|
+
ZoneSummary,
|
|
36
|
+
get_file_type,
|
|
37
|
+
get_num_variables,
|
|
38
|
+
get_num_zones,
|
|
39
|
+
get_title,
|
|
40
|
+
get_variable_list,
|
|
41
|
+
get_zone_list,
|
|
42
|
+
open,
|
|
43
|
+
peek,
|
|
44
|
+
)
|
|
32
45
|
from ._plt_read import TecplotPltReader
|
|
33
46
|
from ._plt_write import TecplotPltWriter
|
|
34
47
|
from ._reader import (
|
|
@@ -82,6 +95,15 @@ del _cls
|
|
|
82
95
|
__all__ = [
|
|
83
96
|
"libtecio",
|
|
84
97
|
"open",
|
|
98
|
+
"peek",
|
|
99
|
+
"get_variable_list",
|
|
100
|
+
"get_zone_list",
|
|
101
|
+
"get_num_zones",
|
|
102
|
+
"get_num_variables",
|
|
103
|
+
"get_title",
|
|
104
|
+
"get_file_type",
|
|
105
|
+
"FileSummary",
|
|
106
|
+
"ZoneSummary",
|
|
85
107
|
"cli",
|
|
86
108
|
"AppendWrite",
|
|
87
109
|
"AppendReadWrite",
|
|
@@ -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
|
|
@@ -299,6 +300,93 @@ def _next_token_is_value(text: str, i: int) -> bool:
|
|
|
299
300
|
return not (j < n and text[j] == "=")
|
|
300
301
|
|
|
301
302
|
|
|
303
|
+
def _determine_zone_type(kv: dict[str, str]) -> ZoneType:
|
|
304
|
+
"""Determine a zone's type from its header key-value pairs.
|
|
305
|
+
|
|
306
|
+
Shared by the full parser and the metadata-only scanner so both agree on the same
|
|
307
|
+
modern/legacy keyword rules.
|
|
308
|
+
|
|
309
|
+
Two header dialects are supported:
|
|
310
|
+
* Modern: ``ZONETYPE=FEQuadrilateral, DATAPACKING=POINT``
|
|
311
|
+
* Legacy: ``F=FEPOINT, ET=QUADRILATERAL``
|
|
312
|
+
|
|
313
|
+
The FE-vs-ordered distinction comes from ``ZONETYPE`` (modern) or ``F``
|
|
314
|
+
(legacy). ``ET`` (element type) only ever appears on finite-element zones, so it
|
|
315
|
+
merely names the element *shape* once a zone is already known to be FE. Modern
|
|
316
|
+
keywords win when present.
|
|
317
|
+
|
|
318
|
+
Args:
|
|
319
|
+
kv: Zone header key-value pairs (leading ``ZONE`` keyword already stripped), as
|
|
320
|
+
returned by :func:`_kv_split`.
|
|
321
|
+
|
|
322
|
+
Returns:
|
|
323
|
+
The zone's type. Unlike the full parser, does not reject FEPOLYGON/FEPOLYHEDRON,
|
|
324
|
+
those just aren't readable for data, the header itself is perfectly legible.
|
|
325
|
+
|
|
326
|
+
Raises:
|
|
327
|
+
ValueError: If a legacy FE header (``F=FEPOINT``/``FEBLOCK``) is
|
|
328
|
+
missing the required ``ET`` keyword.
|
|
329
|
+
|
|
330
|
+
Example:
|
|
331
|
+
>>> _determine_zone_type({"T": '"z"', "I": "5"})
|
|
332
|
+
<ZoneType.ORDERED: ...>
|
|
333
|
+
"""
|
|
334
|
+
legacy_is_fe: bool | None = None
|
|
335
|
+
if "F" in kv:
|
|
336
|
+
legacy_is_fe, _ = _parse_legacy_format(kv["F"])
|
|
337
|
+
|
|
338
|
+
if "ZONETYPE" in kv:
|
|
339
|
+
zt_raw = kv["ZONETYPE"].rstrip(",").strip().lower()
|
|
340
|
+
return _STR_TO_ZONETYPE.get(zt_raw, ZoneType.ORDERED)
|
|
341
|
+
elif legacy_is_fe is False:
|
|
342
|
+
return ZoneType.ORDERED
|
|
343
|
+
elif legacy_is_fe or "ET" in kv:
|
|
344
|
+
if "ET" not in kv:
|
|
345
|
+
raise ValueError(
|
|
346
|
+
"Legacy FE zone header specifies F=FEPOINT/FEBLOCK but is "
|
|
347
|
+
"missing the required ET (element type) keyword."
|
|
348
|
+
)
|
|
349
|
+
return _parse_legacy_element_type(kv["ET"])
|
|
350
|
+
else:
|
|
351
|
+
return ZoneType.ORDERED
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def _collect_zone_header_lines(tokens: _LineBuffer) -> list[str]:
|
|
355
|
+
"""Collect a ZONE block's header lines, stopping before data begins.
|
|
356
|
+
|
|
357
|
+
Shared by the full parser and the metadata-only scanner (:func:`peek_dat_metadata`)
|
|
358
|
+
so both agree on exactly where a zone's header ends and its data starts.
|
|
359
|
+
|
|
360
|
+
Args:
|
|
361
|
+
tokens: Positioned at the ``ZONE`` line itself (not yet consumed).
|
|
362
|
+
|
|
363
|
+
Returns:
|
|
364
|
+
Raw header lines, including the leading ``ZONE`` keyword line.
|
|
365
|
+
|
|
366
|
+
Example:
|
|
367
|
+
>>> lines = _collect_zone_header_lines(tokens)
|
|
368
|
+
"""
|
|
369
|
+
header_lines: list[str] = [tokens.next_stripped()] # ZONE T=...
|
|
370
|
+
while tokens.has_more():
|
|
371
|
+
nxt = tokens.peek_stripped()
|
|
372
|
+
if not nxt:
|
|
373
|
+
tokens.next_stripped()
|
|
374
|
+
continue
|
|
375
|
+
nxt_upper = nxt.lstrip().upper()
|
|
376
|
+
# Stop at a new zone, top-level keyword, or the first data line.
|
|
377
|
+
if nxt_upper.split("=")[0].split()[0] == "ZONE":
|
|
378
|
+
break
|
|
379
|
+
if nxt_upper.startswith(("DATASETAUXDATA", "VARAUXDATA")):
|
|
380
|
+
break
|
|
381
|
+
first_ch = nxt.lstrip()[0] if nxt.lstrip() else ""
|
|
382
|
+
# A data line begins with a numeric token (covers leading-dot values such as
|
|
383
|
+
# ``.5`` and signed values such as ``-1.2e3``).
|
|
384
|
+
if first_ch in "0123456789+-.":
|
|
385
|
+
break
|
|
386
|
+
header_lines.append(tokens.next_stripped())
|
|
387
|
+
return header_lines
|
|
388
|
+
|
|
389
|
+
|
|
302
390
|
def _kv_split(text: str) -> dict[str, str]:
|
|
303
391
|
"""Parse a loose ``KEY=VALUE`` string into an upper-cased-key dict.
|
|
304
392
|
|
|
@@ -1022,6 +1110,162 @@ class TecplotDatFEZoneReader(TecplotFEZoneReader):
|
|
|
1022
1110
|
return TecplotDatAuxDataReader(self._aux_raw)
|
|
1023
1111
|
|
|
1024
1112
|
|
|
1113
|
+
class _DatZoneInfo(NamedTuple):
|
|
1114
|
+
"""Per-zone metadata from a cheap header-only scan."""
|
|
1115
|
+
|
|
1116
|
+
title: str
|
|
1117
|
+
zone_type: ZoneType
|
|
1118
|
+
num_nodes: int | None
|
|
1119
|
+
num_elements: int | None
|
|
1120
|
+
|
|
1121
|
+
|
|
1122
|
+
class _DatMetadata(NamedTuple):
|
|
1123
|
+
"""Result of :func:`_peek_dat_metadata`."""
|
|
1124
|
+
|
|
1125
|
+
title: str
|
|
1126
|
+
file_type: FileType
|
|
1127
|
+
variable_names: list[str]
|
|
1128
|
+
zones: list[_DatZoneInfo]
|
|
1129
|
+
|
|
1130
|
+
|
|
1131
|
+
_ZONE_LINE_RE = re.compile(r"^[ \t]*ZONE\b", re.IGNORECASE | re.MULTILINE)
|
|
1132
|
+
_ZONE_DATA_START_RE = re.compile(r"^[ \t]*[0-9+\-.]", re.MULTILINE)
|
|
1133
|
+
|
|
1134
|
+
|
|
1135
|
+
def _peek_dat_metadata(path: str | os.PathLike) -> _DatMetadata:
|
|
1136
|
+
"""Scan a DAT file's title, variable names, and per-zone metadata only.
|
|
1137
|
+
|
|
1138
|
+
Unlike opening the file normally, never converts a single data value to a number. A
|
|
1139
|
+
single bulk regex pass across the whole file finds every zone header's starting
|
|
1140
|
+
line; each zone's metadata is then extracted from just the handful of lines between
|
|
1141
|
+
that start and the next one (or EOF), reusing the same header-parsing logic the real
|
|
1142
|
+
parser uses, so a zone header can still span multiple lines correctly. The data and
|
|
1143
|
+
connectivity blocks in between are never read at all. A line-by-line Python scan
|
|
1144
|
+
over the whole file (rather than this bulk approach) was tried first and measured
|
|
1145
|
+
slower than a full parse for large files, string-heavy per-line loops in Python
|
|
1146
|
+
don't beat NumPy's bulk numeric parsing, even when doing less work; a single C-level
|
|
1147
|
+
regex pass over the whole file does.
|
|
1148
|
+
|
|
1149
|
+
A zone's ``num_nodes``/``num_elements`` are ``None`` only for the one case they
|
|
1150
|
+
genuinely can't be determined without reading data: an ordered zone whose header
|
|
1151
|
+
omits I/J/K entirely, relying on the reader to infer the point count from the data
|
|
1152
|
+
itself (see :meth:`TecplotDatReader._infer_ordered_point_count`). Every other case,
|
|
1153
|
+
including FE zones, always states its node/element counts directly in the header
|
|
1154
|
+
text.
|
|
1155
|
+
|
|
1156
|
+
Args:
|
|
1157
|
+
path: Path to a ``.dat``/``.tec`` file.
|
|
1158
|
+
|
|
1159
|
+
Returns:
|
|
1160
|
+
Title, file type, variable names, and per-zone metadata (in file order).
|
|
1161
|
+
|
|
1162
|
+
Raises:
|
|
1163
|
+
FileNotFoundError: If *path* does not exist.
|
|
1164
|
+
|
|
1165
|
+
Example:
|
|
1166
|
+
>>> meta = _peek_dat_metadata("flow.dat")
|
|
1167
|
+
>>> [z.title for z in meta.zones]
|
|
1168
|
+
['FluidVolume', 'WingSurface']
|
|
1169
|
+
"""
|
|
1170
|
+
with open(path, encoding="utf-8", errors="replace") as fh:
|
|
1171
|
+
text = fh.read()
|
|
1172
|
+
|
|
1173
|
+
zone_starts = [m.start() for m in _ZONE_LINE_RE.finditer(text)]
|
|
1174
|
+
|
|
1175
|
+
# File header: everything before the first zone (or the whole file, if it somehow
|
|
1176
|
+
# has none). Mirrors TecplotDatReader._parse_file_header, kept separate rather than
|
|
1177
|
+
# shared with it since that method also populates dataset aux data, which this
|
|
1178
|
+
# function deliberately doesn't scan for. Just a handful of lines regardless of file
|
|
1179
|
+
# size, so no bulk-regex treatment needed here.
|
|
1180
|
+
header_end = zone_starts[0] if zone_starts else len(text)
|
|
1181
|
+
tokens = _LineBuffer(text[:header_end].splitlines(keepends=True))
|
|
1182
|
+
|
|
1183
|
+
title = ""
|
|
1184
|
+
file_type = FileType.FULL
|
|
1185
|
+
variable_names: list[str] = []
|
|
1186
|
+
while tokens.has_more():
|
|
1187
|
+
line = tokens.peek_stripped()
|
|
1188
|
+
if not line:
|
|
1189
|
+
tokens.next_stripped()
|
|
1190
|
+
continue
|
|
1191
|
+
upper_key = (
|
|
1192
|
+
line.split("=")[0].strip().upper() if "=" in line else line.strip().upper()
|
|
1193
|
+
)
|
|
1194
|
+
tokens.next_stripped()
|
|
1195
|
+
|
|
1196
|
+
if upper_key == "TITLE":
|
|
1197
|
+
rhs = line.split("=", 1)[1].strip() if "=" in line else ""
|
|
1198
|
+
title = _unquote(rhs)
|
|
1199
|
+
elif upper_key == "FILETYPE":
|
|
1200
|
+
rhs = line.split("=", 1)[1].strip() if "=" in line else ""
|
|
1201
|
+
file_type = _STR_TO_FILETYPE.get(rhs.strip().lower(), FileType.FULL)
|
|
1202
|
+
elif upper_key == "VARIABLES":
|
|
1203
|
+
rhs = line.split("=", 1)[1].strip() if "=" in line else ""
|
|
1204
|
+
names = _extract_quoted_strings(rhs)
|
|
1205
|
+
while tokens.has_more():
|
|
1206
|
+
nxt = tokens.peek_stripped()
|
|
1207
|
+
if not nxt:
|
|
1208
|
+
tokens.next_stripped()
|
|
1209
|
+
continue
|
|
1210
|
+
if nxt.lstrip().startswith('"'):
|
|
1211
|
+
names.extend(_extract_quoted_strings(tokens.next_stripped()))
|
|
1212
|
+
elif "=" not in nxt.split("#")[0]:
|
|
1213
|
+
tokens.next_stripped()
|
|
1214
|
+
else:
|
|
1215
|
+
break
|
|
1216
|
+
variable_names = names
|
|
1217
|
+
|
|
1218
|
+
# Zones: each zone's header lives in the small window between its start and the next
|
|
1219
|
+
# zone's start (or EOF); _collect_zone_header_lines only ever looks at the first
|
|
1220
|
+
# handful of lines in that window before hitting what looks like data, so this stays
|
|
1221
|
+
# cheap regardless of how much data actually follows.
|
|
1222
|
+
zones: list[_DatZoneInfo] = []
|
|
1223
|
+
for i, start in enumerate(zone_starts):
|
|
1224
|
+
end = zone_starts[i + 1] if i + 1 < len(zone_starts) else len(text)
|
|
1225
|
+
# Bounded search directly on the original string
|
|
1226
|
+
data_match = _ZONE_DATA_START_RE.search(text, start, end)
|
|
1227
|
+
header_end = data_match.start() if data_match else end
|
|
1228
|
+
window_tokens = _LineBuffer(text[start:header_end].splitlines(keepends=True))
|
|
1229
|
+
header_lines = _collect_zone_header_lines(window_tokens)
|
|
1230
|
+
header_text = " ".join(header_lines)
|
|
1231
|
+
m_zone = re.match(r"(?i)^ZONE\s*", header_text)
|
|
1232
|
+
if m_zone:
|
|
1233
|
+
header_text = header_text[m_zone.end() :]
|
|
1234
|
+
kv = _kv_split(header_text)
|
|
1235
|
+
zone_title = _unquote(kv.get("T", ""))
|
|
1236
|
+
|
|
1237
|
+
try:
|
|
1238
|
+
zone_type = _determine_zone_type(kv)
|
|
1239
|
+
except ValueError:
|
|
1240
|
+
# A legacy header F=FEPOINT with no ET reports as unknown
|
|
1241
|
+
zones.append(_DatZoneInfo(zone_title, ZoneType.ORDERED, None, None))
|
|
1242
|
+
continue
|
|
1243
|
+
|
|
1244
|
+
num_nodes: int | None
|
|
1245
|
+
num_elements: int | None
|
|
1246
|
+
if zone_type == ZoneType.ORDERED:
|
|
1247
|
+
if "I" not in kv and "J" not in kv and "K" not in kv:
|
|
1248
|
+
# Point count only knowable by reading data -> report unknown
|
|
1249
|
+
num_nodes = None
|
|
1250
|
+
num_elements = None
|
|
1251
|
+
else:
|
|
1252
|
+
zi = int(kv.get("I", "1") or "1")
|
|
1253
|
+
zj = int(kv.get("J", "1") or "1")
|
|
1254
|
+
zk = int(kv.get("K", "1") or "1")
|
|
1255
|
+
num_nodes = zi * zj * zk
|
|
1256
|
+
num_elements = max(zi - 1, 1) * max(zj - 1, 1) * max(zk - 1, 1)
|
|
1257
|
+
else:
|
|
1258
|
+
# FE zones (including FEPOLYGON/FEPOLYHEDRON, which the full parser can't
|
|
1259
|
+
# read data for, but whose header is just as legible as any other zone's)
|
|
1260
|
+
# always state counts directly.
|
|
1261
|
+
num_nodes = int(kv.get("NODES", kv.get("N", "0")) or "0")
|
|
1262
|
+
num_elements = int(kv.get("ELEMENTS", kv.get("E", "0")) or "0")
|
|
1263
|
+
|
|
1264
|
+
zones.append(_DatZoneInfo(zone_title, zone_type, num_nodes, num_elements))
|
|
1265
|
+
|
|
1266
|
+
return _DatMetadata(title, file_type, variable_names, zones)
|
|
1267
|
+
|
|
1268
|
+
|
|
1025
1269
|
class TecplotDatReader(TecplotReader):
|
|
1026
1270
|
"""Reader for Tecplot ASCII DAT files.
|
|
1027
1271
|
|
|
@@ -1143,8 +1387,10 @@ class TecplotDatReader(TecplotReader):
|
|
|
1143
1387
|
|
|
1144
1388
|
while tokens.has_more():
|
|
1145
1389
|
line = tokens.peek_stripped()
|
|
1390
|
+
if not line:
|
|
1391
|
+
tokens.next_stripped()
|
|
1392
|
+
continue
|
|
1146
1393
|
upper = line.upper()
|
|
1147
|
-
# if upper.startswith("ZONE"):
|
|
1148
1394
|
if upper.lstrip().split("=")[0].split()[0] == "ZONE":
|
|
1149
1395
|
self._parse_zone(tokens)
|
|
1150
1396
|
elif upper.startswith("DATASETAUXDATA"):
|
|
@@ -1232,7 +1478,9 @@ class TecplotDatReader(TecplotReader):
|
|
|
1232
1478
|
def _infer_ordered_point_count(tokens: _LineBuffer, active_count: int) -> int:
|
|
1233
1479
|
"""Infer an ordered zone's point count when I/J/K are all omitted.
|
|
1234
1480
|
|
|
1235
|
-
|
|
1481
|
+
Some legacy exporters write a bare ``ZONE F=POINT`` header with no dimensions at
|
|
1482
|
+
all, relying on the reader to count data rows itself. Peeks ahead (never
|
|
1483
|
+
consuming) counting numeric tokens up to the next zone/keyword boundary or end
|
|
1236
1484
|
of file, using the same boundary rule :meth:`_parse_zone` already uses to know
|
|
1237
1485
|
where a header ends and data begins, then restores the original position so the
|
|
1238
1486
|
real read (later, driven by the now-known count) starts from the same place.
|
|
@@ -1283,25 +1531,7 @@ class TecplotDatReader(TecplotReader):
|
|
|
1283
1531
|
"""
|
|
1284
1532
|
# -- Collect header lines ------------------------------------------------------
|
|
1285
1533
|
|
|
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())
|
|
1534
|
+
header_lines = _collect_zone_header_lines(tokens)
|
|
1305
1535
|
|
|
1306
1536
|
header_text = " ".join(header_lines)
|
|
1307
1537
|
|
|
@@ -1320,38 +1550,13 @@ class TecplotDatReader(TecplotReader):
|
|
|
1320
1550
|
|
|
1321
1551
|
# -- Determine zone type and data packing --------------------------------------
|
|
1322
1552
|
#
|
|
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
|
|
1553
|
+
# See _determine_zone_type for the modern/legacy keyword rules this shares with
|
|
1554
|
+
# the metadata-only scanner (peek_dat_metadata).
|
|
1333
1555
|
legacy_packing: DataPacking | None = None
|
|
1334
1556
|
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
|
|
1557
|
+
_, legacy_packing = _parse_legacy_format(kv["F"])
|
|
1558
|
+
|
|
1559
|
+
zone_type = _determine_zone_type(kv)
|
|
1355
1560
|
|
|
1356
1561
|
if zone_type in _FE_POLY:
|
|
1357
1562
|
raise ValueError(
|
|
@@ -1372,7 +1577,7 @@ class TecplotDatReader(TecplotReader):
|
|
|
1372
1577
|
|
|
1373
1578
|
if zone_type == ZoneType.ORDERED:
|
|
1374
1579
|
if "I" not in kv and "J" not in kv and "K" not in kv:
|
|
1375
|
-
# Infer
|
|
1580
|
+
# Infer point count
|
|
1376
1581
|
active_count = self.num_vars - len(passive_set) - len(share_map)
|
|
1377
1582
|
num_nodes = self._infer_ordered_point_count(tokens, active_count)
|
|
1378
1583
|
I = num_nodes # noqa E741
|