tecio-python 0.3.2__py3-none-any.whl → 0.3.3__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 +24 -2
- tecio/_dat_read.py +258 -53
- tecio/_io.py +251 -2
- tecio/cli/tecmerge.py +278 -8
- tecio_python-0.3.3.data/data/share/plugins/matlab/fecplot.m +111 -0
- {tecio_python-0.3.2.data/data/share/tecio → tecio_python-0.3.3.data/data/share}/plugins/paraview/TecplotTecioReader.py +9 -0
- {tecio_python-0.3.2.dist-info → tecio_python-0.3.3.dist-info}/METADATA +1 -1
- {tecio_python-0.3.2.dist-info → tecio_python-0.3.3.dist-info}/RECORD +13 -12
- {tecio_python-0.3.2.dist-info → tecio_python-0.3.3.dist-info}/WHEEL +0 -0
- {tecio_python-0.3.2.dist-info → tecio_python-0.3.3.dist-info}/entry_points.txt +0 -0
- {tecio_python-0.3.2.dist-info → tecio_python-0.3.3.dist-info}/licenses/LICENSE +0 -0
- {tecio_python-0.3.2.dist-info → tecio_python-0.3.3.dist-info}/licenses/NOTICE +0 -0
- {tecio_python-0.3.2.dist-info → tecio_python-0.3.3.dist-info}/top_level.txt +0 -0
tecio/__init__.py
CHANGED
|
@@ -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",
|
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
|
|
@@ -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
|
tecio/_io.py
CHANGED
|
@@ -40,14 +40,14 @@ from __future__ import annotations
|
|
|
40
40
|
import os
|
|
41
41
|
import tempfile
|
|
42
42
|
from pathlib import Path
|
|
43
|
-
from typing import Any, Literal, overload
|
|
43
|
+
from typing import Any, Literal, NamedTuple, overload
|
|
44
44
|
|
|
45
45
|
import numpy as np
|
|
46
46
|
import numpy.typing as npt
|
|
47
47
|
|
|
48
48
|
from ._constants import FileType, ValueLocation, ZoneType
|
|
49
49
|
from ._containers import ZoneList
|
|
50
|
-
from ._dat_read import TecplotDatReader
|
|
50
|
+
from ._dat_read import TecplotDatReader, _peek_dat_metadata
|
|
51
51
|
from ._dat_write import TecplotDatWriter
|
|
52
52
|
from ._plt_read import TecplotPltReader
|
|
53
53
|
from ._plt_write import TecplotPltWriter
|
|
@@ -811,3 +811,252 @@ def open(
|
|
|
811
811
|
raise ValueError(
|
|
812
812
|
f"Unrecognised mode '{mode}'. Supported modes: 'r', 'w', 'x', 'a', 'a+'"
|
|
813
813
|
)
|
|
814
|
+
|
|
815
|
+
|
|
816
|
+
# --------------------------------------------------------------------------------------
|
|
817
|
+
# Lightweight metadata queries
|
|
818
|
+
# --------------------------------------------------------------------------------------
|
|
819
|
+
|
|
820
|
+
|
|
821
|
+
def _validated_ext(path: str | os.PathLike) -> str:
|
|
822
|
+
"""Return *path*'s lowercased extension, or raise if unsupported.
|
|
823
|
+
|
|
824
|
+
Example:
|
|
825
|
+
>>> _validated_ext("flow.szplt")
|
|
826
|
+
'.szplt'
|
|
827
|
+
"""
|
|
828
|
+
ext = Path(path).suffix.lower()
|
|
829
|
+
if ext not in _HANDLERS:
|
|
830
|
+
raise ValueError(
|
|
831
|
+
f"Unsupported file extension: '{ext}'. Supported: {sorted(_HANDLERS)}"
|
|
832
|
+
)
|
|
833
|
+
return ext
|
|
834
|
+
|
|
835
|
+
|
|
836
|
+
class ZoneSummary(NamedTuple):
|
|
837
|
+
"""Per-zone metadata from a lightweight :func:`peek` query.
|
|
838
|
+
|
|
839
|
+
``num_nodes``/``num_elements`` are ``None`` only for a DAT ordered zone whose header
|
|
840
|
+
omits I/J/K entirely (rare; relies on the reader inferring the point count from the
|
|
841
|
+
data itself, which a lightweight query never does). Every other case, and every
|
|
842
|
+
SZL/PLT zone, always has both.
|
|
843
|
+
"""
|
|
844
|
+
|
|
845
|
+
title: str
|
|
846
|
+
zone_type: ZoneType
|
|
847
|
+
num_nodes: int | None
|
|
848
|
+
num_elements: int | None
|
|
849
|
+
|
|
850
|
+
|
|
851
|
+
class FileSummary(NamedTuple):
|
|
852
|
+
"""Whole-file metadata from :func:`peek`, with no variable data touched."""
|
|
853
|
+
|
|
854
|
+
title: str
|
|
855
|
+
file_type: FileType
|
|
856
|
+
variable_names: list[str]
|
|
857
|
+
zones: list[ZoneSummary]
|
|
858
|
+
|
|
859
|
+
|
|
860
|
+
def get_variable_list(path: str | os.PathLike) -> list[str]:
|
|
861
|
+
"""Return a file's variable names without reading any variable data.
|
|
862
|
+
|
|
863
|
+
For SZL and PLT this is already what a normal :func:`open` does, both formats
|
|
864
|
+
resolve dataset-level metadata (including variable names) up front without touching
|
|
865
|
+
data arrays, so this is a thin convenience wrapper for those two. DAT is different:
|
|
866
|
+
opening it normally always reads and numerically converts every value in the file,
|
|
867
|
+
so this function instead scans just the file's header text, several orders of
|
|
868
|
+
magnitude cheaper for a file with large data blocks.
|
|
869
|
+
|
|
870
|
+
Args:
|
|
871
|
+
path: Path to a ``.szplt``, ``.plt``/``.bin``, or ``.dat``/``.tec`` file.
|
|
872
|
+
|
|
873
|
+
Returns:
|
|
874
|
+
Variable names, in file order.
|
|
875
|
+
|
|
876
|
+
Raises:
|
|
877
|
+
FileNotFoundError: If *path* does not exist.
|
|
878
|
+
ValueError: If the file extension is not recognized.
|
|
879
|
+
|
|
880
|
+
Example:
|
|
881
|
+
>>> get_variable_list("flow.szplt")
|
|
882
|
+
['x', 'y', 'z', 'pressure']
|
|
883
|
+
"""
|
|
884
|
+
ext = _validated_ext(path)
|
|
885
|
+
if ext in (".dat", ".tec"):
|
|
886
|
+
return _peek_dat_metadata(path).variable_names
|
|
887
|
+
with _HANDLERS[ext]["r"](str(path)) as r:
|
|
888
|
+
return list(r.variables)
|
|
889
|
+
|
|
890
|
+
|
|
891
|
+
def get_zone_list(path: str | os.PathLike) -> list[str]:
|
|
892
|
+
"""Return a file's zone titles without reading any variable data.
|
|
893
|
+
|
|
894
|
+
Same rationale as :func:`get_variable_list`: already cheap for SZL and PLT
|
|
895
|
+
(constructing a zone reader only resolves scalar metadata, never variable data),
|
|
896
|
+
so this is a thin wrapper for those; for DAT, uses a dedicated scan that skips
|
|
897
|
+
every data and connectivity block entirely.
|
|
898
|
+
|
|
899
|
+
Args:
|
|
900
|
+
path: Path to a ``.szplt``, ``.plt``/``.bin``, or ``.dat``/``.tec`` file.
|
|
901
|
+
|
|
902
|
+
Returns:
|
|
903
|
+
Zone titles, in file order. An unnamed zone contributes an empty string, not
|
|
904
|
+
a placeholder like ``"Zone 1"``.
|
|
905
|
+
|
|
906
|
+
Raises:
|
|
907
|
+
FileNotFoundError: If *path* does not exist.
|
|
908
|
+
ValueError: If the file extension is not recognized.
|
|
909
|
+
|
|
910
|
+
Example:
|
|
911
|
+
>>> get_zone_list("flow.szplt")
|
|
912
|
+
['FluidVolume', 'WingSurface']
|
|
913
|
+
"""
|
|
914
|
+
ext = _validated_ext(path)
|
|
915
|
+
if ext in (".dat", ".tec"):
|
|
916
|
+
return [z.title for z in _peek_dat_metadata(path).zones]
|
|
917
|
+
with _HANDLERS[ext]["r"](str(path)) as r:
|
|
918
|
+
return [zone.title for zone in r.zones]
|
|
919
|
+
|
|
920
|
+
|
|
921
|
+
def get_num_zones(path: str | os.PathLike) -> int:
|
|
922
|
+
"""Return a file's zone count without reading any variable data.
|
|
923
|
+
|
|
924
|
+
Cheaper still than :func:`get_zone_list`: for SZL, a single scalar C-library
|
|
925
|
+
call with no zone objects constructed at all; for PLT and DAT, a count against
|
|
926
|
+
already-parsed/scanned header metadata.
|
|
927
|
+
|
|
928
|
+
Args:
|
|
929
|
+
path: Path to a ``.szplt``, ``.plt``/``.bin``, or ``.dat``/``.tec`` file.
|
|
930
|
+
|
|
931
|
+
Returns:
|
|
932
|
+
Number of zones in the file.
|
|
933
|
+
|
|
934
|
+
Raises:
|
|
935
|
+
FileNotFoundError: If *path* does not exist.
|
|
936
|
+
ValueError: If the file extension is not recognized.
|
|
937
|
+
|
|
938
|
+
Example:
|
|
939
|
+
>>> get_num_zones("flow.szplt")
|
|
940
|
+
2
|
|
941
|
+
"""
|
|
942
|
+
ext = _validated_ext(path)
|
|
943
|
+
if ext in (".dat", ".tec"):
|
|
944
|
+
return len(_peek_dat_metadata(path).zones)
|
|
945
|
+
with _HANDLERS[ext]["r"](str(path)) as r:
|
|
946
|
+
return r.num_zones
|
|
947
|
+
|
|
948
|
+
|
|
949
|
+
def get_num_variables(path: str | os.PathLike) -> int:
|
|
950
|
+
"""Return a file's variable count without reading any variable data.
|
|
951
|
+
|
|
952
|
+
Cheaper still than :func:`get_variable_list`: for SZL, a single scalar
|
|
953
|
+
C-library call.
|
|
954
|
+
|
|
955
|
+
Args:
|
|
956
|
+
path: Path to a ``.szplt``, ``.plt``/``.bin``, or ``.dat``/``.tec`` file.
|
|
957
|
+
|
|
958
|
+
Returns:
|
|
959
|
+
Number of variables in the dataset.
|
|
960
|
+
|
|
961
|
+
Raises:
|
|
962
|
+
FileNotFoundError: If *path* does not exist.
|
|
963
|
+
ValueError: If the file extension is not recognized.
|
|
964
|
+
|
|
965
|
+
Example:
|
|
966
|
+
>>> get_num_variables("flow.szplt")
|
|
967
|
+
4
|
|
968
|
+
"""
|
|
969
|
+
ext = _validated_ext(path)
|
|
970
|
+
if ext in (".dat", ".tec"):
|
|
971
|
+
return len(_peek_dat_metadata(path).variable_names)
|
|
972
|
+
with _HANDLERS[ext]["r"](str(path)) as r:
|
|
973
|
+
return r.num_vars
|
|
974
|
+
|
|
975
|
+
|
|
976
|
+
def get_title(path: str | os.PathLike) -> str:
|
|
977
|
+
"""Return a file's dataset title without reading any variable data.
|
|
978
|
+
|
|
979
|
+
Args:
|
|
980
|
+
path: Path to a ``.szplt``, ``.plt``/``.bin``, or ``.dat``/``.tec`` file.
|
|
981
|
+
|
|
982
|
+
Returns:
|
|
983
|
+
Dataset title string.
|
|
984
|
+
|
|
985
|
+
Raises:
|
|
986
|
+
FileNotFoundError: If *path* does not exist.
|
|
987
|
+
ValueError: If the file extension is not recognized.
|
|
988
|
+
|
|
989
|
+
Example:
|
|
990
|
+
>>> get_title("flow.szplt")
|
|
991
|
+
'Onera M6 wing'
|
|
992
|
+
"""
|
|
993
|
+
ext = _validated_ext(path)
|
|
994
|
+
if ext in (".dat", ".tec"):
|
|
995
|
+
return _peek_dat_metadata(path).title
|
|
996
|
+
with _HANDLERS[ext]["r"](str(path)) as r:
|
|
997
|
+
return r.title
|
|
998
|
+
|
|
999
|
+
|
|
1000
|
+
def get_file_type(path: str | os.PathLike) -> FileType:
|
|
1001
|
+
"""Return a file's :class:`~tecio.FileType` without reading any variable data.
|
|
1002
|
+
|
|
1003
|
+
Args:
|
|
1004
|
+
path: Path to a ``.szplt``, ``.plt``/``.bin``, or ``.dat``/``.tec`` file.
|
|
1005
|
+
|
|
1006
|
+
Returns:
|
|
1007
|
+
The file type (``FULL``, ``GRID``, or ``SOLUTION``).
|
|
1008
|
+
|
|
1009
|
+
Raises:
|
|
1010
|
+
FileNotFoundError: If *path* does not exist.
|
|
1011
|
+
ValueError: If the file extension is not recognized.
|
|
1012
|
+
|
|
1013
|
+
Example:
|
|
1014
|
+
>>> get_file_type("flow.szplt")
|
|
1015
|
+
<FileType.FULL: 0>
|
|
1016
|
+
"""
|
|
1017
|
+
ext = _validated_ext(path)
|
|
1018
|
+
if ext in (".dat", ".tec"):
|
|
1019
|
+
return _peek_dat_metadata(path).file_type
|
|
1020
|
+
with _HANDLERS[ext]["r"](str(path)) as r:
|
|
1021
|
+
return r.file_type
|
|
1022
|
+
|
|
1023
|
+
|
|
1024
|
+
def peek(path: str | os.PathLike) -> FileSummary:
|
|
1025
|
+
"""Return a file's title, file type, variable names, and per-zone summaries.
|
|
1026
|
+
|
|
1027
|
+
A single combined query covering everything :func:`get_variable_list`,
|
|
1028
|
+
:func:`get_zone_list`, :func:`get_title`, and :func:`get_file_type` return, plus
|
|
1029
|
+
each zone's type and size. For DAT, calling several of those functions
|
|
1030
|
+
separately on the same file means scanning it that many times over, this
|
|
1031
|
+
function scans once no matter how much of the summary is needed.
|
|
1032
|
+
|
|
1033
|
+
Args:
|
|
1034
|
+
path: Path to a ``.szplt``, ``.plt``/``.bin``, or ``.dat``/``.tec`` file.
|
|
1035
|
+
|
|
1036
|
+
Returns:
|
|
1037
|
+
Whole-file and per-zone metadata. See :class:`FileSummary` and
|
|
1038
|
+
:class:`ZoneSummary`.
|
|
1039
|
+
|
|
1040
|
+
Raises:
|
|
1041
|
+
FileNotFoundError: If *path* does not exist.
|
|
1042
|
+
ValueError: If the file extension is not recognized.
|
|
1043
|
+
|
|
1044
|
+
Example:
|
|
1045
|
+
>>> summary = peek("flow.szplt")
|
|
1046
|
+
>>> [z.title for z in summary.zones]
|
|
1047
|
+
['FluidVolume', 'WingSurface']
|
|
1048
|
+
"""
|
|
1049
|
+
ext = _validated_ext(path)
|
|
1050
|
+
if ext in (".dat", ".tec"):
|
|
1051
|
+
meta = _peek_dat_metadata(path)
|
|
1052
|
+
zones = [
|
|
1053
|
+
ZoneSummary(z.title, z.zone_type, z.num_nodes, z.num_elements)
|
|
1054
|
+
for z in meta.zones
|
|
1055
|
+
]
|
|
1056
|
+
return FileSummary(meta.title, meta.file_type, meta.variable_names, zones)
|
|
1057
|
+
with _HANDLERS[ext]["r"](str(path)) as r:
|
|
1058
|
+
zones = [
|
|
1059
|
+
ZoneSummary(zone.title, zone.zone_type, zone.num_nodes, zone.num_elements)
|
|
1060
|
+
for zone in r.zones
|
|
1061
|
+
]
|
|
1062
|
+
return FileSummary(r.title, r.file_type, list(r.variables), zones)
|
tecio/cli/tecmerge.py
CHANGED
|
@@ -9,14 +9,18 @@ time-step sequences, solution times can be assigned automatically from a start t
|
|
|
9
9
|
either a fixed interval or an end time; each zone also gets a strand ID matching its
|
|
10
10
|
1-based position within its source file, so the same physical block (e.g. a wing zone
|
|
11
11
|
present in every timestep) shares one strand across the whole sequence and can be
|
|
12
|
-
animated as a single, continuous entity in the Tecplot GUI.
|
|
12
|
+
animated as a single, continuous entity in the Tecplot GUI. Merged zones get
|
|
13
|
+
``SourceFile``/``SourceFileName`` aux data record of the source file and path. Grid data
|
|
14
|
+
can also be detected and shared across zones with matching structure instead of
|
|
15
|
+
duplicated in every merged zone.
|
|
13
16
|
|
|
14
17
|
:Usage:
|
|
15
18
|
|
|
16
19
|
.. code:: bash
|
|
17
20
|
|
|
18
21
|
tecmerge [-h] -o PATH [-f] [--title STRING] [--assign-time-strands]
|
|
19
|
-
[-s VALUE] [-d VALUE | -e VALUE] [--strand ID]
|
|
22
|
+
[-s VALUE] [-d VALUE | -e VALUE] [--strand ID] [--merge-grid [LIST]]
|
|
23
|
+
FILE [FILE ...]
|
|
20
24
|
|
|
21
25
|
:Positional Arguments:
|
|
22
26
|
``FILE [FILE ...]``
|
|
@@ -63,6 +67,26 @@ animated as a single, continuous entity in the Tecplot GUI.
|
|
|
63
67
|
per-zone-position assignment described under ``--assign-time-strands``. Has no
|
|
64
68
|
effect without ``--assign-time-strands``.
|
|
65
69
|
|
|
70
|
+
``--merge-grid [LIST]``
|
|
71
|
+
Detect and share grid data (coordinate variables, and for FE zones,
|
|
72
|
+
connectivity) across zones with matching zone type and dimensions, instead of
|
|
73
|
+
writing it fresh in every merged zone. Zones are compared by a
|
|
74
|
+
``(zone_type, num_nodes, num_elements)`` signature; the first zone with a given
|
|
75
|
+
signature (in file, then zone, order) writes its grid fresh, and every later
|
|
76
|
+
zone with a matching signature, whether from the same file or a different one,
|
|
77
|
+
shares from it instead. This is not a rigorous check that the underlying data
|
|
78
|
+
is actually identical, just that it plausibly could be, a false match (e.g. two
|
|
79
|
+
genuinely different blocks that happen to share dimensions) would silently
|
|
80
|
+
share the wrong grid.
|
|
81
|
+
|
|
82
|
+
Given with no value, grid variables are found automatically by name (``x``,
|
|
83
|
+
``X``, ``x-coordinate``, ``xgrid``, and similar common names for X/Y/Z). Given a
|
|
84
|
+
comma-separated list of 1-based indices or exact names into the union variable
|
|
85
|
+
list instead (e.g. ``--merge-grid x,y,z`` or ``--merge-grid 1,2,3``), only those
|
|
86
|
+
variables are treated as the grid; indices and names cannot be mixed in the same
|
|
87
|
+
list. Solution variables are never affected either way, only variables
|
|
88
|
+
explicitly identified as the grid are ever shared.
|
|
89
|
+
|
|
66
90
|
:Returns:
|
|
67
91
|
A new Tecplot file written to the output path containing all zones from every input
|
|
68
92
|
file. Variables absent from a source file are written as passive. Exit code is ``0``
|
|
@@ -90,6 +114,16 @@ Examples:
|
|
|
90
114
|
$ tecmerge --assign-time-strands -s 0.0 -d 0.1 --strand 1 \\
|
|
91
115
|
"step_*.szplt" -o transient.szplt
|
|
92
116
|
|
|
117
|
+
Merge a multiblock time series, sharing each block's grid across timesteps
|
|
118
|
+
instead of duplicating it::
|
|
119
|
+
|
|
120
|
+
$ tecmerge --assign-time-strands -s 0.0 -d 0.1 --merge-grid \\
|
|
121
|
+
"step_*.szplt" -o transient.szplt
|
|
122
|
+
|
|
123
|
+
Same, but only "x" and "y" are grid variables (a 2-D case with no "z")::
|
|
124
|
+
|
|
125
|
+
$ tecmerge --merge-grid x,y "step_*.szplt" -o transient.szplt
|
|
126
|
+
|
|
93
127
|
Call directly from a Python session::
|
|
94
128
|
|
|
95
129
|
import tecio.cli.tecmerge.main as tecmerge
|
|
@@ -122,6 +156,7 @@ from .. import (
|
|
|
122
156
|
TecplotSzlWriter,
|
|
123
157
|
TecplotWriter,
|
|
124
158
|
TecplotZoneReader,
|
|
159
|
+
ValueLocation,
|
|
125
160
|
ZoneType,
|
|
126
161
|
)
|
|
127
162
|
from .. import open as tecio_open
|
|
@@ -251,6 +286,22 @@ def _parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
|
|
|
251
286
|
),
|
|
252
287
|
)
|
|
253
288
|
|
|
289
|
+
# Grid sharing
|
|
290
|
+
parser.add_argument(
|
|
291
|
+
"--merge-grid",
|
|
292
|
+
nargs="?",
|
|
293
|
+
const="__auto__",
|
|
294
|
+
default=None,
|
|
295
|
+
type=str,
|
|
296
|
+
metavar="LIST",
|
|
297
|
+
help=(
|
|
298
|
+
"Detect and share the grid (coordinate variables, and connectivity for FE "
|
|
299
|
+
"zones) with matching zone type and dimensions. To manually specify grid "
|
|
300
|
+
"coordinates provide a comma-separated list of 1-based indices or exact "
|
|
301
|
+
"variable names (e.g. -merge-grid x,y,z or -merge-grid 1,2,3)."
|
|
302
|
+
),
|
|
303
|
+
)
|
|
304
|
+
|
|
254
305
|
return parser.parse_args(argv)
|
|
255
306
|
|
|
256
307
|
|
|
@@ -258,6 +309,157 @@ def _parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
|
|
|
258
309
|
# Helpers
|
|
259
310
|
# --------------------------------------------------------------------------------------
|
|
260
311
|
|
|
312
|
+
# Guesses of grid variable names automatic grid variable detection for merging
|
|
313
|
+
_AXIS_SYNONYMS: dict[str, frozenset[str]] = {
|
|
314
|
+
"x": frozenset({
|
|
315
|
+
"x",
|
|
316
|
+
"x-coordinate",
|
|
317
|
+
"x coordinate",
|
|
318
|
+
"coordinate x",
|
|
319
|
+
"coord-x",
|
|
320
|
+
"coordx",
|
|
321
|
+
"xcoord",
|
|
322
|
+
"x_coord",
|
|
323
|
+
"xgrid",
|
|
324
|
+
"x-grid",
|
|
325
|
+
"x_grid",
|
|
326
|
+
}),
|
|
327
|
+
"y": frozenset({
|
|
328
|
+
"y",
|
|
329
|
+
"y-coordinate",
|
|
330
|
+
"y coordinate",
|
|
331
|
+
"coordinate y",
|
|
332
|
+
"coord-y",
|
|
333
|
+
"coordy",
|
|
334
|
+
"ycoord",
|
|
335
|
+
"y_coord",
|
|
336
|
+
"ygrid",
|
|
337
|
+
"y-grid",
|
|
338
|
+
"y_grid",
|
|
339
|
+
}),
|
|
340
|
+
"z": frozenset({
|
|
341
|
+
"z",
|
|
342
|
+
"z-coordinate",
|
|
343
|
+
"z coordinate",
|
|
344
|
+
"coordinate z",
|
|
345
|
+
"coord-z",
|
|
346
|
+
"coordz",
|
|
347
|
+
"zcoord",
|
|
348
|
+
"z_coord",
|
|
349
|
+
"zgrid",
|
|
350
|
+
"z-grid",
|
|
351
|
+
"z_grid",
|
|
352
|
+
}),
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _autodetect_grid_variables(names: list[str]) -> list[int]:
|
|
357
|
+
"""Return the 1-based indices in *names* that look like X/Y/Z coordinates.
|
|
358
|
+
|
|
359
|
+
Matched by exact name (case-insensitive, trimmed) against
|
|
360
|
+
:data:`_AXIS_SYNONYMS`, checked in X, Y, Z order; an axis with no match among
|
|
361
|
+
*names* is simply omitted rather than guessed at positionally, unlike the
|
|
362
|
+
ParaView plugin's coordinate resolution, an incorrect guess here would silently
|
|
363
|
+
mishandle real data instead of just looking odd on screen, so this only
|
|
364
|
+
returns indices it's actually confident in.
|
|
365
|
+
|
|
366
|
+
Example:
|
|
367
|
+
>>> _autodetect_grid_variables(["x-grid", "y", "pressure"])
|
|
368
|
+
[1, 2]
|
|
369
|
+
"""
|
|
370
|
+
indices: list[int] = []
|
|
371
|
+
for axis in ("x", "y", "z"):
|
|
372
|
+
synonyms = _AXIS_SYNONYMS[axis]
|
|
373
|
+
for i, name in enumerate(names, start=1):
|
|
374
|
+
if name.strip().lower() in synonyms:
|
|
375
|
+
indices.append(i)
|
|
376
|
+
break
|
|
377
|
+
return indices
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _parse_index_or_name_list(value: str) -> list[int | str]:
|
|
381
|
+
"""Parse a comma-separated string of 1-based integers and/or variable names.
|
|
382
|
+
|
|
383
|
+
Each token is parsed as an integer where possible; anything else is kept as a
|
|
384
|
+
name string, resolved against the union variable list once it's known.
|
|
385
|
+
|
|
386
|
+
Args:
|
|
387
|
+
value: String like ``"1,2,3"`` or ``"x,y,z"``.
|
|
388
|
+
|
|
389
|
+
Returns:
|
|
390
|
+
List of ``int`` (1-based index) and/or ``str`` (variable name) tokens, in
|
|
391
|
+
the order given.
|
|
392
|
+
|
|
393
|
+
Example:
|
|
394
|
+
>>> _parse_index_or_name_list("x,y,z")
|
|
395
|
+
['x', 'y', 'z']
|
|
396
|
+
"""
|
|
397
|
+
tokens: list[int | str] = []
|
|
398
|
+
for raw in value.split(","):
|
|
399
|
+
token = raw.strip()
|
|
400
|
+
try:
|
|
401
|
+
tokens.append(int(token))
|
|
402
|
+
except ValueError:
|
|
403
|
+
tokens.append(token)
|
|
404
|
+
return tokens
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def _resolve_grid_variable_indices(
|
|
408
|
+
explicit: str | None, union_vars: list[str]
|
|
409
|
+
) -> set[int] | None:
|
|
410
|
+
"""Resolve ``--merge-grid``'s value into a set of 1-based union indices.
|
|
411
|
+
|
|
412
|
+
Args:
|
|
413
|
+
explicit: ``None`` if ``--merge-grid`` wasn't given at all (the feature is off);
|
|
414
|
+
the sentinel ``"__auto__"`` if given with no value (detect by name, see
|
|
415
|
+
:func:`_autodetect_grid_variables`); otherwise a comma-separated list of
|
|
416
|
+
indices and/or names, all of one kind, not mixed (matching
|
|
417
|
+
``-v``/``--variables`` elsewhere in this project).
|
|
418
|
+
union_vars: The merge's full union variable list.
|
|
419
|
+
|
|
420
|
+
Returns:
|
|
421
|
+
1-based indices into *union_vars* eligible for grid sharing, or ``None`` if
|
|
422
|
+
``--merge-grid`` wasn't given at all. An empty set (rather than ``None``) means
|
|
423
|
+
the feature is on but nothing was found/specified to share, --merge-grid then
|
|
424
|
+
has no effect.
|
|
425
|
+
|
|
426
|
+
Raises:
|
|
427
|
+
ValueError: If an explicit index is out of range, an explicit name isn't in
|
|
428
|
+
*union_vars*, or indices and names are mixed in the same list.
|
|
429
|
+
"""
|
|
430
|
+
if explicit is None:
|
|
431
|
+
return None
|
|
432
|
+
if explicit == "__auto__":
|
|
433
|
+
return set(_autodetect_grid_variables(union_vars))
|
|
434
|
+
|
|
435
|
+
tokens = _parse_index_or_name_list(explicit)
|
|
436
|
+
has_int = any(isinstance(t, int) for t in tokens)
|
|
437
|
+
has_name = any(isinstance(t, str) for t in tokens)
|
|
438
|
+
if has_int and has_name:
|
|
439
|
+
raise ValueError(
|
|
440
|
+
"--merge-grid indices and names cannot be mixed in the same list; "
|
|
441
|
+
f"use all indices or all names, got: {explicit!r}."
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
resolved: set[int] = set()
|
|
445
|
+
for token in tokens:
|
|
446
|
+
if isinstance(token, int):
|
|
447
|
+
if token < 1 or token > len(union_vars):
|
|
448
|
+
raise ValueError(
|
|
449
|
+
f"--merge-grid variable index {token} out of range "
|
|
450
|
+
f"[1, {len(union_vars)}]."
|
|
451
|
+
)
|
|
452
|
+
resolved.add(token)
|
|
453
|
+
else:
|
|
454
|
+
try:
|
|
455
|
+
resolved.add(union_vars.index(token) + 1)
|
|
456
|
+
except ValueError:
|
|
457
|
+
raise ValueError(
|
|
458
|
+
f"--merge-grid variable name {token!r} not found; available "
|
|
459
|
+
f"names: {', '.join(union_vars)}."
|
|
460
|
+
) from None
|
|
461
|
+
return resolved
|
|
462
|
+
|
|
261
463
|
|
|
262
464
|
def _expand_inputs(patterns: list[str]) -> list[Path]:
|
|
263
465
|
"""Expand a list of file paths / glob patterns to a sorted list of Paths.
|
|
@@ -338,6 +540,9 @@ def _write_zone(
|
|
|
338
540
|
solution_time: float | None,
|
|
339
541
|
strand_id: int | None,
|
|
340
542
|
zone_index_map: dict[int, int],
|
|
543
|
+
source_path: Path,
|
|
544
|
+
grid_var_indices: set[int] | None,
|
|
545
|
+
grid_reference: int | None,
|
|
341
546
|
) -> None:
|
|
342
547
|
"""Write one zone to *writer* using the reconciled variable list.
|
|
343
548
|
|
|
@@ -348,12 +553,24 @@ def _write_zone(
|
|
|
348
553
|
writer: Open writer instance.
|
|
349
554
|
zone: Source zone reader.
|
|
350
555
|
union_vars: Full union variable name list.
|
|
351
|
-
local_index_map: Map from union index -> local 0-based var index
|
|
352
|
-
|
|
556
|
+
local_index_map: Map from union index -> local 0-based var index (``None`` =
|
|
557
|
+
not present in this file).
|
|
353
558
|
solution_time: Override solution time, or ``None`` to keep original.
|
|
354
559
|
strand_id: Override strand ID, or ``None`` to keep original.
|
|
355
560
|
zone_index_map: Map 1-based source zone to 1-based output zone index for
|
|
356
561
|
variable and connectivity sharing.
|
|
562
|
+
source_path: Input file this zone came from, recorded as zone-level aux
|
|
563
|
+
data (``SourceFile``, ``SourceFileName``) on every merged
|
|
564
|
+
zone.
|
|
565
|
+
grid_var_indices: 1-based union indices of the ``--merge-grid`` grid variables,
|
|
566
|
+
or ``None`` if the feature is off.
|
|
567
|
+
grid_reference: 1-based output zone index to share this zone's grid variables
|
|
568
|
+
(and, for FE zones, connectivity) from, if an earlier zone
|
|
569
|
+
with a matching (zone_type, num_nodes, num_elements) signature
|
|
570
|
+
was already written, else ``None`` (this zone's grid is
|
|
571
|
+
written fresh; the caller is responsible for registering its
|
|
572
|
+
signature afterward). Ignored entirely when *grid_var_indices*
|
|
573
|
+
is ``None``.
|
|
357
574
|
"""
|
|
358
575
|
zt = zone.zone_type
|
|
359
576
|
|
|
@@ -362,7 +579,22 @@ def _write_zone(
|
|
|
362
579
|
passive_vars: list[bool] = []
|
|
363
580
|
var_sharing: list[int] = []
|
|
364
581
|
|
|
365
|
-
for local_idx in local_index_map:
|
|
582
|
+
for union_i, local_idx in enumerate(local_index_map, start=1):
|
|
583
|
+
is_grid_var = grid_var_indices is not None and union_i in grid_var_indices
|
|
584
|
+
|
|
585
|
+
if is_grid_var and grid_reference is not None:
|
|
586
|
+
# An earlier zone with the same zone_type/num_nodes/num_elements already
|
|
587
|
+
# wrote this grid so share rather than duplicate
|
|
588
|
+
passive_vars.append(False)
|
|
589
|
+
var_sharing.append(grid_reference)
|
|
590
|
+
if local_idx is not None:
|
|
591
|
+
loc = zone.variables[local_idx].value_location
|
|
592
|
+
else:
|
|
593
|
+
loc = ValueLocation.NODAL
|
|
594
|
+
active_locs.append(loc)
|
|
595
|
+
active_data.append(np.array([], dtype=np.float32))
|
|
596
|
+
continue
|
|
597
|
+
|
|
366
598
|
if local_idx is None:
|
|
367
599
|
# Variable not in this file -- mark passive.
|
|
368
600
|
passive_vars.append(True)
|
|
@@ -410,9 +642,13 @@ def _write_zone(
|
|
|
410
642
|
if not is_p and sv == 0
|
|
411
643
|
]
|
|
412
644
|
|
|
413
|
-
zone_aux: dict[str, str]
|
|
645
|
+
zone_aux: dict[str, str] = {}
|
|
414
646
|
if len(zone.auxdata) > 0:
|
|
415
647
|
zone_aux = dict(zone.auxdata.items())
|
|
648
|
+
# Always added, source provenance is cheap to record and easy to ignore in the
|
|
649
|
+
# Tecplot GUI if unwanted
|
|
650
|
+
zone_aux["SourceFile"] = str(source_path)
|
|
651
|
+
zone_aux["SourceFileName"] = source_path.name
|
|
416
652
|
|
|
417
653
|
s_time = solution_time if solution_time is not None else zone.solution_time
|
|
418
654
|
s_id = strand_id if strand_id is not None else zone.strand_id
|
|
@@ -430,8 +666,14 @@ def _write_zone(
|
|
|
430
666
|
if isinstance(zone, TecplotOrderedZoneReader):
|
|
431
667
|
writer.write_ordered_zone(data=writer_data, **common_kw)
|
|
432
668
|
elif isinstance(zone, TecplotFEZoneReader):
|
|
433
|
-
|
|
434
|
-
|
|
669
|
+
if grid_var_indices is not None and grid_reference is not None:
|
|
670
|
+
# Share connectivity from the same reference zone as the grid variables, a
|
|
671
|
+
# matching (zone_type, num_nodes, num_elements) signature implies the same
|
|
672
|
+
# mesh, node map included.
|
|
673
|
+
con_remapped: int | None = grid_reference
|
|
674
|
+
else:
|
|
675
|
+
con_src = zone.shared_connectivity
|
|
676
|
+
con_remapped = zone_index_map.get(con_src) if con_src is not None else None
|
|
435
677
|
fe_kw = common_kw.copy()
|
|
436
678
|
|
|
437
679
|
# Face-neighbor connections
|
|
@@ -530,6 +772,17 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|
|
530
772
|
|
|
531
773
|
print(f"\nUnion variable list ({n_union}): {union_vars}")
|
|
532
774
|
|
|
775
|
+
try:
|
|
776
|
+
grid_var_indices = _resolve_grid_variable_indices(
|
|
777
|
+
args.merge_grid, union_vars
|
|
778
|
+
)
|
|
779
|
+
except ValueError as exc:
|
|
780
|
+
print(f"Error: {exc}", file=sys.stderr)
|
|
781
|
+
return 1
|
|
782
|
+
if grid_var_indices is not None:
|
|
783
|
+
grid_names = [union_vars[i - 1] for i in sorted(grid_var_indices)]
|
|
784
|
+
print(f"Grid sharing enabled for variables: {grid_names}")
|
|
785
|
+
|
|
533
786
|
# Report any variables that will be passive in some files.
|
|
534
787
|
for fi, (_reader, imap) in enumerate(zip(readers, index_maps, strict=False)):
|
|
535
788
|
missing = [union_vars[ui] for ui, li in enumerate(imap) if li is None]
|
|
@@ -566,6 +819,8 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|
|
566
819
|
writer.add_auxvar_dict(auxvar)
|
|
567
820
|
|
|
568
821
|
total_zones = 0
|
|
822
|
+
# Persists sharing registry across every input file for merge grid option
|
|
823
|
+
signature_registry: dict[tuple[ZoneType, int, int], int] = {}
|
|
569
824
|
for fi, (reader, imap) in enumerate(zip(readers, index_maps, strict=False)):
|
|
570
825
|
sol_time = times[fi] if times is not None else None
|
|
571
826
|
|
|
@@ -595,6 +850,14 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|
|
595
850
|
else:
|
|
596
851
|
s_id = zone_num
|
|
597
852
|
|
|
853
|
+
grid_reference: int | None = None
|
|
854
|
+
signature: tuple[ZoneType, int, int] | None = None
|
|
855
|
+
if grid_var_indices is not None and isinstance(
|
|
856
|
+
zone, (TecplotOrderedZoneReader, TecplotFEZoneReader)
|
|
857
|
+
):
|
|
858
|
+
signature = (zt, zone.num_nodes, zone.num_elements)
|
|
859
|
+
grid_reference = signature_registry.get(signature)
|
|
860
|
+
|
|
598
861
|
_write_zone(
|
|
599
862
|
writer=writer,
|
|
600
863
|
zone=zone,
|
|
@@ -603,8 +866,15 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|
|
603
866
|
solution_time=sol_time,
|
|
604
867
|
strand_id=s_id,
|
|
605
868
|
zone_index_map=zone_index_map,
|
|
869
|
+
source_path=input_paths[fi],
|
|
870
|
+
grid_var_indices=grid_var_indices,
|
|
871
|
+
grid_reference=grid_reference,
|
|
606
872
|
)
|
|
607
873
|
zone_index_map[zone_num] = writer.current_zone
|
|
874
|
+
if signature is not None and grid_reference is None:
|
|
875
|
+
# First zone with this signature should write grid, every later
|
|
876
|
+
# zone with a matching signature shares from it
|
|
877
|
+
signature_registry[signature] = writer.current_zone
|
|
608
878
|
total_zones += 1
|
|
609
879
|
|
|
610
880
|
# Close all readers
|
|
@@ -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
|
|
|
@@ -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/
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
tecio/__init__.py,sha256
|
|
1
|
+
tecio/__init__.py,sha256=-aohTaSV10NJQTIkixETvD1r34zjOwS2vOI72xNXwIc,3211
|
|
2
2
|
tecio/_constants.py,sha256=k6o3FSuE2mCTtJ431Bg4hzkgVW34-ENEY3U3pLz6CN0,7190
|
|
3
3
|
tecio/_containers.py,sha256=PmVUwYfImIHB8uCHtA0AWQEcOHGBRkGlkMRrY8ONGZA,10501
|
|
4
|
-
tecio/_dat_read.py,sha256=
|
|
4
|
+
tecio/_dat_read.py,sha256=Bm17K53DG6ObX7UQA56ghcJQ5UApIJ7Do7yIDIVslvw,81146
|
|
5
5
|
tecio/_dat_write.py,sha256=bB95ziTg9lMG3DdgIg3LKLxtytRM2_qWFp2TRbgSrO4,39994
|
|
6
|
-
tecio/_io.py,sha256=
|
|
6
|
+
tecio/_io.py,sha256=jC3GG5IgvoVXc7s5zoupTdDEqhwtRX840VGfS5Dq7WE,39759
|
|
7
7
|
tecio/_meta.py,sha256=EqOqejk3wjtvB2KSzXZgn6nzDTvIuQqsYsm_agBpr1M,8028
|
|
8
8
|
tecio/_plt_read.py,sha256=3SRWvqbHeIll_zB_TTUUUWibtEaoeoN0x0C0BYm3XY4,56523
|
|
9
9
|
tecio/_plt_write.py,sha256=kq3waOr5TWInNHbT6YdW2HRw5mN5er_IhLLnPLPyPFU,31735
|
|
@@ -19,16 +19,17 @@ tecio/cli/tecaux.py,sha256=1ZCL5Ef_8cfR7TXm8tSxNTAgLZnnyDvIawwnVJhfNYg,36712
|
|
|
19
19
|
tecio/cli/tecdump.py,sha256=pBWYjVMlQNzan5mgfFeLrvRQluRXAGX3JrM0XHSbXmE,12119
|
|
20
20
|
tecio/cli/tecextract.py,sha256=cM70fcwbQE4ziXVFSkhC1ISOlQpXPz-VFI_votqVynM,19435
|
|
21
21
|
tecio/cli/tecfix.py,sha256=c4DGYJ0O48dHW4HppZ2dLVW8HKKtTBsp1MksxYWgiP8,20313
|
|
22
|
-
tecio/cli/tecmerge.py,sha256=
|
|
22
|
+
tecio/cli/tecmerge.py,sha256=NQ1NMhjvus9Pt02AJO_ZjL7i7N35BoLwdJ-IpZn5EF0,33614
|
|
23
23
|
tecio/cli/teconvert.py,sha256=x2Xmcyn9O7IB-ioE_f_TkEWLrJcMpMy03FLO3X58DTY,12865
|
|
24
24
|
tecio/cli/tecscale.py,sha256=-uSdjfa82g7LIfcj-liMyXpsKNAAACDGP5fGa_pxiyE,14810
|
|
25
25
|
tecio/cli/tecslice.py,sha256=n442ixabc7S74FZwLwo24GShvR1DavHR_Sh7Q07W3og,36246
|
|
26
26
|
tecio/cli/tecstats.py,sha256=kLPz41JBsmgDRL_YrVRZSp_qoW2c-bmb5NI2eFD691k,20923
|
|
27
|
-
tecio_python-0.3.
|
|
28
|
-
tecio_python-0.3.
|
|
29
|
-
tecio_python-0.3.
|
|
30
|
-
tecio_python-0.3.
|
|
31
|
-
tecio_python-0.3.
|
|
32
|
-
tecio_python-0.3.
|
|
33
|
-
tecio_python-0.3.
|
|
34
|
-
tecio_python-0.3.
|
|
27
|
+
tecio_python-0.3.3.data/data/share/plugins/matlab/fecplot.m,sha256=YYPjn7FmIVcE5K4a73WkO_ZV_sNJ4l0426-OYHFqkos,3206
|
|
28
|
+
tecio_python-0.3.3.data/data/share/plugins/paraview/TecplotTecioReader.py,sha256=JXttRwxlHXbKuqh5V-GRkiEh7ACm1xjvgr2KY9xIUXA,44181
|
|
29
|
+
tecio_python-0.3.3.dist-info/licenses/LICENSE,sha256=WYmcYJG1QFgu1hfo7qrEkZ3Jhcz8NUWe6XUraZvlIFs,10172
|
|
30
|
+
tecio_python-0.3.3.dist-info/licenses/NOTICE,sha256=bDcDOguk8dYN4FpHJCHuCsDgR2VnP5XlK2C4BzVCtwg,340
|
|
31
|
+
tecio_python-0.3.3.dist-info/METADATA,sha256=xwyDjPGDBbW4ZQ7vD-xdc_JUVk6ZeKrfKak46FVeUIw,5102
|
|
32
|
+
tecio_python-0.3.3.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
33
|
+
tecio_python-0.3.3.dist-info/entry_points.txt,sha256=TH2UnRF700KO0GjeRFwkBBtFKN1PyP2tgrc11OsVNAc,362
|
|
34
|
+
tecio_python-0.3.3.dist-info/top_level.txt,sha256=ayCfhWQNUimgwyaTSyamJ3LbgjloZRWl2vlcZCjYWmQ,6
|
|
35
|
+
tecio_python-0.3.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|