tecio-python 0.2.2__py3-none-any.whl → 0.2.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 +1 -1
- tecio/cli/tec2mat.py +2 -2
- tecio/cli/tecdump.py +1 -1
- tecio/cli/tecfix.py +2 -2
- tecio/cli/tecmerge.py +4 -3
- tecio/cli/teconvert.py +2 -2
- tecio/cli/tecscale.py +1 -1
- tecio/cli/tecslice.py +10 -9
- tecio/cli/tecstats.py +2 -2
- tecio/dat/_read.py +17 -5
- tecio/plt/_read.py +1 -1
- tecio/plugins/paraview/TecplotTecioReader.py +1070 -0
- {tecio_python-0.2.2.dist-info → tecio_python-0.2.3.dist-info}/METADATA +2 -2
- tecio_python-0.2.3.dist-info/RECORD +34 -0
- {tecio_python-0.2.2.dist-info → tecio_python-0.2.3.dist-info}/WHEEL +1 -1
- tecio_python-0.2.2.dist-info/RECORD +0 -33
- {tecio_python-0.2.2.dist-info → tecio_python-0.2.3.dist-info}/entry_points.txt +0 -0
- {tecio_python-0.2.2.dist-info → tecio_python-0.2.3.dist-info}/licenses/LICENSE +0 -0
- {tecio_python-0.2.2.dist-info → tecio_python-0.2.3.dist-info}/licenses/NOTICE +0 -0
- {tecio_python-0.2.2.dist-info → tecio_python-0.2.3.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,1070 @@
|
|
|
1
|
+
"""ParaView reader plugin for Tecplot 360 data files, built on ``tecio``.
|
|
2
|
+
|
|
3
|
+
This plugin closes three gaps in ParaView's built-in Tecplot reader: it cannot read
|
|
4
|
+
``.szplt``, it errors out on some ASCII zone header fields, and does not read auxiliary
|
|
5
|
+
data as VTK :class:`~vtkmodules.vtkCommonDataModel. vtkFieldData`.
|
|
6
|
+
|
|
7
|
+
Output structure
|
|
8
|
+
Each file is read into one ``vtkMultiBlockDataSet``, one block per zone, named after
|
|
9
|
+
the zone's title (or ``"Zone <n>"`` if untitled):
|
|
10
|
+
|
|
11
|
+
* ``ORDERED`` zones become ``vtkStructuredGrid`` blocks.
|
|
12
|
+
* Classic finite-element zones (line, triangle, quadrilateral, tetrahedron, brick)
|
|
13
|
+
become ``vtkUnstructuredGrid`` blocks.
|
|
14
|
+
* ``FEPOLYGON``, ``FEPOLYHEDRON``, and ``FEMIXED`` zones are not yet supported and
|
|
15
|
+
are skipped with a printed warning.
|
|
16
|
+
* A zone whose aux data marks it a non-``Wall`` boundary (see "Zone visibility"
|
|
17
|
+
below) still becomes a block, but its entry in the ``Zones`` property defaults
|
|
18
|
+
to unchecked, matching Tecplot's own default display convention.
|
|
19
|
+
|
|
20
|
+
Auxiliary data has no first-class equivalent in VTK, so it is mapped onto the
|
|
21
|
+
nearest matching :class:`vtkFieldData`, each item as a one-element array named after
|
|
22
|
+
the aux-data key -- numeric (int64/float64) when the value parses cleanly as a
|
|
23
|
+
number, else a :class:`vtkStringArray`. Tecplot itself treats aux data as usable
|
|
24
|
+
scalars in its own equations, and a numeric VTK array is the equivalent for
|
|
25
|
+
ParaView: it works directly in the Python Calculator, Calculator filter, and
|
|
26
|
+
numeric-oriented annotation filters, not just as inert display text.
|
|
27
|
+
|
|
28
|
+
* Dataset-level aux data -> the root ``vtkMultiBlockDataSet``'s field data.
|
|
29
|
+
* Variable-level aux data -> also the root field data, keyed ``"<variable
|
|
30
|
+
name>::<aux key>"`` (there is no per-array metadata slot that ParaView's UI
|
|
31
|
+
surfaces to users).
|
|
32
|
+
* Zone-level aux data -> that zone's own block's field data.
|
|
33
|
+
|
|
34
|
+
Coordinates
|
|
35
|
+
Tecplot data files do not tag which variables are spatial coordinates (that is
|
|
36
|
+
ordinarily a plot/frame setting, not a file-format concept), so this reader resolves
|
|
37
|
+
each axis in priority order:
|
|
38
|
+
|
|
39
|
+
1. The ``XArray``/``YArray``/``ZArray`` properties, if set to a real variable name.
|
|
40
|
+
2. The dataset aux data items ``Common.XVar``/``Common.YVar``/``Common.ZVar``, per
|
|
41
|
+
the Tecplot data format guide -- each holds the 1-based dataset variable number
|
|
42
|
+
to use for that axis, when the file itself specifies one.
|
|
43
|
+
3. A name match against some spelling of ``x``/``y``/``z`` (case-insensitively,
|
|
44
|
+
e.g. ``"X"``, ``"x-coordinate"``).
|
|
45
|
+
4. For X and Y only, positional fallback to the first and second dataset variables,
|
|
46
|
+
since almost every Tecplot CFD file leads with its coordinates. Z has no
|
|
47
|
+
positional fallback beyond the third variable and defaults to all-zero (2-D
|
|
48
|
+
data) when nothing is found by steps 1-3.
|
|
49
|
+
|
|
50
|
+
Vector components
|
|
51
|
+
If the dataset aux data specifies ``Common.UVar``/``Common.VVar``/``Common.WVar``
|
|
52
|
+
(or the ``UArray``/``VArray``/``WArray`` properties are set manually), this reader
|
|
53
|
+
assembles those component variables into one 3-component array per zone so glyphs,
|
|
54
|
+
streamlines, and vector-magnitude coloring work without a separate Calculator
|
|
55
|
+
step. Unlike coordinates, there's no name-based guessing here (solver vector
|
|
56
|
+
naming varies too much to guess reliably), so nothing is built at all unless at
|
|
57
|
+
least one of U/V/W actually resolves; a resolved-but-missing component (2-D flow,
|
|
58
|
+
or passive in a particular zone) is treated as zero rather than blocking the
|
|
59
|
+
other components. The array is named ``"Velocity"`` if the dataset aux item
|
|
60
|
+
``Common.VectorVarsAreVelocity`` is truthy, else ``"Momentum"``.
|
|
61
|
+
|
|
62
|
+
Zone visibility
|
|
63
|
+
Tecplot sets default visiblilty for zones with auxdata that marks them a boundary
|
|
64
|
+
that isn't a wall (``Common.IsBoundaryZone`` true and ``Common.BoundaryCondition``
|
|
65
|
+
not ``"Wall"``).
|
|
66
|
+
|
|
67
|
+
Known limitations (first pass)
|
|
68
|
+
* ``FEPOLYGON`` / ``FEPOLYHEDRON`` / ``FEMIXED`` zones are skipped. Note that
|
|
69
|
+
``tecio``'s ASCII DAT reader currently raises immediately on encountering one of
|
|
70
|
+
these zone types (rather than skipping just that zone), so a ``.dat`` file
|
|
71
|
+
containing one won't load at all through this reader until ``tecio`` supports it.
|
|
72
|
+
* SOLUTION-only files (grid coordinates stored in a separate, unopened GRID file)
|
|
73
|
+
have no coordinate data to build geometry from and are not handled specially; they
|
|
74
|
+
will fail coordinate resolution unless the variables happen to still be present.
|
|
75
|
+
* Face-based connectivity, custom labels, geometry/text annotations, and Tecplot's
|
|
76
|
+
classic-PLT auxiliary record types outside dataset/zone/ variable aux data are not
|
|
77
|
+
handled by ``tecio`` data readers.
|
|
78
|
+
|
|
79
|
+
Requirements
|
|
80
|
+
The ``tecio`` package must be importable from the same Python interpreter ParaView
|
|
81
|
+
uses to run this plugin.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
from __future__ import annotations
|
|
85
|
+
|
|
86
|
+
import math
|
|
87
|
+
import os
|
|
88
|
+
import sys
|
|
89
|
+
import weakref
|
|
90
|
+
from typing import TYPE_CHECKING, Any
|
|
91
|
+
|
|
92
|
+
import numpy as np
|
|
93
|
+
import numpy.typing as npt
|
|
94
|
+
from paraview.util.vtkAlgorithm import smdomain, smhint, smproperty, smproxy
|
|
95
|
+
from vtkmodules.util import numpy_support
|
|
96
|
+
from vtkmodules.util.vtkAlgorithm import VTKPythonAlgorithmBase
|
|
97
|
+
from vtkmodules.vtkCommonCore import vtkDataArraySelection, vtkPoints, vtkStringArray
|
|
98
|
+
from vtkmodules.vtkCommonDataModel import (
|
|
99
|
+
VTK_HEXAHEDRON,
|
|
100
|
+
VTK_LINE,
|
|
101
|
+
VTK_QUAD,
|
|
102
|
+
VTK_TETRA,
|
|
103
|
+
VTK_TRIANGLE,
|
|
104
|
+
vtkCellArray,
|
|
105
|
+
vtkCompositeDataSet,
|
|
106
|
+
vtkFieldData,
|
|
107
|
+
vtkMultiBlockDataSet,
|
|
108
|
+
vtkStructuredGrid,
|
|
109
|
+
vtkUnstructuredGrid,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# Because ParaView's bundled Python (vtkpython) has its own write-protected
|
|
113
|
+
# site-packages tecio cannot directly be installed into the pvpython environment,
|
|
114
|
+
# create a TECIO_PYTHONPATH environment variable to point to the local install
|
|
115
|
+
# (e.g. "/Users/you/tecio-for-paraview").
|
|
116
|
+
_TECIO_FALLBACK_PATH = os.environ.get("TECIO_PYTHONPATH", "")
|
|
117
|
+
if _TECIO_FALLBACK_PATH and _TECIO_FALLBACK_PATH not in sys.path:
|
|
118
|
+
sys.path.insert(0, _TECIO_FALLBACK_PATH)
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
import tecio
|
|
122
|
+
from tecio.libtecio import ValueLocation, ZoneType
|
|
123
|
+
|
|
124
|
+
_TECIO_IMPORT_ERROR: Exception | None = None
|
|
125
|
+
except Exception as exc: # pragma: no cover - environment-dependent # noqa: BLE001
|
|
126
|
+
tecio = None # ty: ignore[invalid-assignment]
|
|
127
|
+
ValueLocation = ZoneType = None # ty: ignore[invalid-assignment]
|
|
128
|
+
_TECIO_IMPORT_ERROR = exc
|
|
129
|
+
|
|
130
|
+
if TYPE_CHECKING:
|
|
131
|
+
from tecio.dat import Read as _DatRead
|
|
132
|
+
from tecio.plt import Read as _PltRead
|
|
133
|
+
from tecio.szl import Read as _SzlRead
|
|
134
|
+
|
|
135
|
+
_TecioReader = _DatRead | _PltRead | _SzlRead
|
|
136
|
+
|
|
137
|
+
# --------------------------------------------------------------------------------------
|
|
138
|
+
# Module-level constants and small helpers
|
|
139
|
+
# --------------------------------------------------------------------------------------
|
|
140
|
+
|
|
141
|
+
_EXTENSIONS = "szplt plt bin dat tec"
|
|
142
|
+
_FILE_DESCRIPTION = "Tecplot 360 Data Files"
|
|
143
|
+
|
|
144
|
+
# Maps a supported classic finite-element ZoneType to its VTK cell type
|
|
145
|
+
_VTK_CELL_TYPE: dict[Any, int] = (
|
|
146
|
+
{
|
|
147
|
+
ZoneType.FELINESEG: VTK_LINE,
|
|
148
|
+
ZoneType.FETRIANGLE: VTK_TRIANGLE,
|
|
149
|
+
ZoneType.FEQUADRILATERAL: VTK_QUAD,
|
|
150
|
+
ZoneType.FETETRAHEDRON: VTK_TETRA,
|
|
151
|
+
ZoneType.FEBRICK: VTK_HEXAHEDRON,
|
|
152
|
+
}
|
|
153
|
+
if tecio is not None
|
|
154
|
+
else {}
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
# Exact-match (case-insensitive, trimmed) spellings of each coordinate axis
|
|
158
|
+
_AXIS_SYNONYMS: dict[str, frozenset[str]] = {
|
|
159
|
+
"x": frozenset({
|
|
160
|
+
"x",
|
|
161
|
+
"x-coordinate",
|
|
162
|
+
"x coordinate",
|
|
163
|
+
"coordinate x",
|
|
164
|
+
"coord-x",
|
|
165
|
+
"coordx",
|
|
166
|
+
"xcoord",
|
|
167
|
+
"x_coord",
|
|
168
|
+
}),
|
|
169
|
+
"y": frozenset({
|
|
170
|
+
"y",
|
|
171
|
+
"y-coordinate",
|
|
172
|
+
"y coordinate",
|
|
173
|
+
"coordinate y",
|
|
174
|
+
"coord-y",
|
|
175
|
+
"coordy",
|
|
176
|
+
"ycoord",
|
|
177
|
+
"y_coord",
|
|
178
|
+
}),
|
|
179
|
+
"z": frozenset({
|
|
180
|
+
"z",
|
|
181
|
+
"z-coordinate",
|
|
182
|
+
"z coordinate",
|
|
183
|
+
"coordinate z",
|
|
184
|
+
"coord-z",
|
|
185
|
+
"coordz",
|
|
186
|
+
"zcoord",
|
|
187
|
+
"z_coord",
|
|
188
|
+
}),
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
# Case-insensitive truthy spellings Tecplot recognizes for Common.IsBoundaryZone
|
|
193
|
+
_BOUNDARY_ZONE_TRUE_VALUES = frozenset({"yes", "y", "true", "t", "on"})
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _is_deactivated_boundary_zone(zone: Any) -> bool:
|
|
197
|
+
"""Return True if *zone* should default to hidden, per Tecplot's own convention.
|
|
198
|
+
|
|
199
|
+
Mirrors Tecplot 360's documented behavior for the ``Common.IsBoundaryZone`` /
|
|
200
|
+
``Common.BoundaryCondition`` zone aux data pair: a zone marked a boundary is
|
|
201
|
+
deactivated (hidden) by default unless its boundary condition is exactly
|
|
202
|
+
``"Wall"``. Only the default *initial* state is affected -- the zone still loads
|
|
203
|
+
as a block either way, and its checkbox in the ``Zones`` property can always be
|
|
204
|
+
re-enabled.
|
|
205
|
+
"""
|
|
206
|
+
aux = zone.auxdata
|
|
207
|
+
is_boundary = aux.get("Common.IsBoundaryZone", "").strip().lower()
|
|
208
|
+
if is_boundary not in _BOUNDARY_ZONE_TRUE_VALUES:
|
|
209
|
+
return False
|
|
210
|
+
return aux.get("Common.BoundaryCondition", "") != "Wall"
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class _UnsupportedZoneError(Exception):
|
|
214
|
+
"""Raised internally when a zone's geometry can't be represented in VTK.
|
|
215
|
+
|
|
216
|
+
Caught by :meth:`TecplotReader.RequestData`, which prints a warning and skips the
|
|
217
|
+
offending zone rather than failing the whole read.
|
|
218
|
+
"""
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _modified_callback(target: VTKPythonAlgorithmBase):
|
|
222
|
+
"""Return a callback that calls ``target.Modified()`` while it's alive.
|
|
223
|
+
|
|
224
|
+
Wires a :class:`vtkDataArraySelection`'s ``ModifiedEvent`` back to the owning reader
|
|
225
|
+
so that toggling an array/zone checkbox in the ParaView UI correctly marks the
|
|
226
|
+
pipeline as needing re-execution. A weak reference avoids keeping the reader alive
|
|
227
|
+
solely because the observer closure refers to it.
|
|
228
|
+
"""
|
|
229
|
+
ref = weakref.ref(target)
|
|
230
|
+
|
|
231
|
+
def _callback(*_args: Any) -> None:
|
|
232
|
+
obj = ref()
|
|
233
|
+
if obj is not None:
|
|
234
|
+
obj.Modified()
|
|
235
|
+
|
|
236
|
+
return _callback
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _autodetect_axis(names: list[str], axis: str) -> str | None:
|
|
240
|
+
"""Return the variable name in *names* that looks like *axis*, or ``None``."""
|
|
241
|
+
synonyms = _AXIS_SYNONYMS[axis]
|
|
242
|
+
for name in names:
|
|
243
|
+
if name.strip().lower() in synonyms:
|
|
244
|
+
return name
|
|
245
|
+
return None
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _resolve_common_var(reader: _TecioReader, key: str) -> str | None:
|
|
249
|
+
"""Resolve a ``Common.<Something>Var`` dataset aux item to a variable name.
|
|
250
|
+
|
|
251
|
+
Per the Tecplot data format guide, items such as ``Common.XVar`` and
|
|
252
|
+
``Common.UVar`` hold a 1-based dataset variable number when present. Returns
|
|
253
|
+
``None`` if *key* is absent from the dataset aux data, isn't a valid integer, or
|
|
254
|
+
is out of range for the dataset -- the caller decides what to fall back to.
|
|
255
|
+
"""
|
|
256
|
+
index = reader.auxdata.as_int(key)
|
|
257
|
+
if index is None:
|
|
258
|
+
return None
|
|
259
|
+
if not 1 <= index <= reader.num_vars:
|
|
260
|
+
print(
|
|
261
|
+
f"[TecplotTecioReader] {key} = {index} is out of range "
|
|
262
|
+
f"[1, {reader.num_vars}]; ignoring."
|
|
263
|
+
)
|
|
264
|
+
return None
|
|
265
|
+
return reader.variables[index - 1]
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _coerce_aux_value(value: str) -> int | float | str:
|
|
269
|
+
"""Best-effort convert an aux-data string to ``int`` or ``float``.
|
|
270
|
+
|
|
271
|
+
Tecplot itself treats aux data as usable scalars in its own equations, so a value
|
|
272
|
+
that parses cleanly as a number is returned as that type; anything else is left as
|
|
273
|
+
the original string. ``int()`` is tried before ``float()`` so an integer-looking
|
|
274
|
+
value (e.g. the variable number in ``Common.XVar``) becomes a integer rather than a
|
|
275
|
+
``1.0``-style float.
|
|
276
|
+
"""
|
|
277
|
+
try:
|
|
278
|
+
return int(value)
|
|
279
|
+
except ValueError:
|
|
280
|
+
pass
|
|
281
|
+
try:
|
|
282
|
+
return float(value)
|
|
283
|
+
except ValueError:
|
|
284
|
+
return value
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _add_aux_field_data(
|
|
288
|
+
field_data: vtkFieldData,
|
|
289
|
+
items: Any,
|
|
290
|
+
key_fn: Any = None,
|
|
291
|
+
) -> None:
|
|
292
|
+
"""Attach each ``(name, value)`` pair in *items* to *field_data*.
|
|
293
|
+
|
|
294
|
+
Each pair becomes a one-element array on *field_data*: a numeric int64/float64
|
|
295
|
+
array (see :func:`_coerce_aux_value`) when the value parses cleanly as a number,
|
|
296
|
+
else a :class:`vtkStringArray`. This is the closest practical VTK equivalent to
|
|
297
|
+
Tecplot's auxiliary data -- there is no first-class "aux data" concept in VTK, but
|
|
298
|
+
field data is visible in ParaView's Information panel and Spreadsheet View, and a
|
|
299
|
+
numeric array additionally works directly with numeric-oriented tools such as the
|
|
300
|
+
Python Calculator and Annotate Attribute Data filter.
|
|
301
|
+
|
|
302
|
+
Args:
|
|
303
|
+
field_data: Target field data (root dataset, block, etc.).
|
|
304
|
+
items: Iterable of ``(key, value)`` string pairs, e.g. a dict's ``.items()``.
|
|
305
|
+
key_fn: Optional ``key -> array name`` transform, e.g. to prefix variable-level
|
|
306
|
+
aux data with the variable name.
|
|
307
|
+
"""
|
|
308
|
+
for key, value in items:
|
|
309
|
+
name = key_fn(key) if key_fn is not None else key
|
|
310
|
+
coerced = _coerce_aux_value(value)
|
|
311
|
+
|
|
312
|
+
if isinstance(coerced, str):
|
|
313
|
+
arr = vtkStringArray()
|
|
314
|
+
arr.SetName(name)
|
|
315
|
+
arr.SetNumberOfValues(1)
|
|
316
|
+
arr.SetValue(0, coerced)
|
|
317
|
+
else:
|
|
318
|
+
dtype = np.int64 if isinstance(coerced, int) else np.float64
|
|
319
|
+
arr = numpy_support.numpy_to_vtk(
|
|
320
|
+
np.array([coerced], dtype=dtype), deep=True
|
|
321
|
+
)
|
|
322
|
+
arr.SetName(name)
|
|
323
|
+
|
|
324
|
+
field_data.AddArray(arr)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
# ======================================================================================
|
|
328
|
+
# Reader
|
|
329
|
+
# ======================================================================================
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
@smproxy.reader(
|
|
333
|
+
name="TecplotTecioReader",
|
|
334
|
+
label="Tecplot 360 Reader (TecIO)",
|
|
335
|
+
extensions=_EXTENSIONS,
|
|
336
|
+
file_description=_FILE_DESCRIPTION,
|
|
337
|
+
)
|
|
338
|
+
class TecplotReader(VTKPythonAlgorithmBase):
|
|
339
|
+
"""Reads Tecplot SZL/PLT/DAT files into a ``vtkMultiBlockDataSet``.
|
|
340
|
+
|
|
341
|
+
One block per zone; see the module docstring for the full mapping, including how
|
|
342
|
+
auxiliary data becomes field data and how coordinate variables are chosen.
|
|
343
|
+
"""
|
|
344
|
+
|
|
345
|
+
def __init__(self) -> None:
|
|
346
|
+
"""Initialize from VTK object."""
|
|
347
|
+
super().__init__(
|
|
348
|
+
nInputPorts=0, nOutputPorts=1, outputType="vtkMultiBlockDataSet"
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
self._filename: str | None = None
|
|
352
|
+
self._reader: _TecioReader | None = None
|
|
353
|
+
self._reader_path: str | None = None
|
|
354
|
+
self._reader_mtime: float | None = None
|
|
355
|
+
|
|
356
|
+
self._x_override: str | None = None
|
|
357
|
+
self._y_override: str | None = None
|
|
358
|
+
self._z_override: str | None = None
|
|
359
|
+
self._u_override: str | None = None
|
|
360
|
+
self._v_override: str | None = None
|
|
361
|
+
self._w_override: str | None = None
|
|
362
|
+
|
|
363
|
+
self._array_selection = vtkDataArraySelection()
|
|
364
|
+
self._array_selection.AddObserver("ModifiedEvent", _modified_callback(self))
|
|
365
|
+
|
|
366
|
+
# Keyed "<1-based zone index>: <title>" -> whether that zone/block loads.
|
|
367
|
+
self._zone_selection = vtkDataArraySelection()
|
|
368
|
+
self._zone_selection.AddObserver("ModifiedEvent", _modified_callback(self))
|
|
369
|
+
self._zone_keys: dict[int, str] = {}
|
|
370
|
+
|
|
371
|
+
def __del__(self) -> None: # pragma: no cover
|
|
372
|
+
"""Best-effort cleanup."""
|
|
373
|
+
try:
|
|
374
|
+
self._close_reader()
|
|
375
|
+
except Exception: # noqa: BLE001 - never raise from __del__
|
|
376
|
+
pass
|
|
377
|
+
|
|
378
|
+
# -- File handle management -------------------------------------------------------
|
|
379
|
+
|
|
380
|
+
def _close_reader(self) -> None:
|
|
381
|
+
"""Release the cached reader handle, if any."""
|
|
382
|
+
if self._reader is not None:
|
|
383
|
+
close = getattr(self._reader, "close", None)
|
|
384
|
+
if callable(close):
|
|
385
|
+
try:
|
|
386
|
+
close()
|
|
387
|
+
except Exception: # noqa: BLE001 - closing must not fail the pipeline
|
|
388
|
+
pass
|
|
389
|
+
self._reader = None
|
|
390
|
+
self._reader_path = None
|
|
391
|
+
self._reader_mtime = None
|
|
392
|
+
|
|
393
|
+
def _get_reader(self) -> _TecioReader | None:
|
|
394
|
+
"""Return a cached, open ``tecio`` reader for the current file.
|
|
395
|
+
|
|
396
|
+
Reopens only when the path or on-disk modification time changes, so the
|
|
397
|
+
frequent, metadata-only pipeline callbacks ParaView issues (array lists, time
|
|
398
|
+
values, ...) don't repeatedly pay the cost of reopening the file.
|
|
399
|
+
|
|
400
|
+
Returns:
|
|
401
|
+
An open ``tecio`` reader, or ``None`` if no file has been set.
|
|
402
|
+
|
|
403
|
+
Raises:
|
|
404
|
+
RuntimeError: If the ``tecio`` package could not be imported.
|
|
405
|
+
FileNotFoundError: If the configured file can't be found/read.
|
|
406
|
+
"""
|
|
407
|
+
if self._filename is None:
|
|
408
|
+
return None
|
|
409
|
+
if tecio is None:
|
|
410
|
+
raise RuntimeError(
|
|
411
|
+
"The 'tecio' package is not importable in ParaView's Python "
|
|
412
|
+
f"environment ({_TECIO_IMPORT_ERROR!r}). Install tecio (and "
|
|
413
|
+
"make sure the TecIO shared library can be located, e.g. via "
|
|
414
|
+
"the TECIO_LIB environment variable) in the Python "
|
|
415
|
+
"interpreter ParaView uses, then reload this plugin."
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
try:
|
|
419
|
+
mtime = os.path.getmtime(self._filename)
|
|
420
|
+
except OSError as exc:
|
|
421
|
+
raise FileNotFoundError(
|
|
422
|
+
f"Cannot read Tecplot file: {self._filename}"
|
|
423
|
+
) from exc
|
|
424
|
+
|
|
425
|
+
if (
|
|
426
|
+
self._reader is not None
|
|
427
|
+
and self._reader_path == self._filename
|
|
428
|
+
and self._reader_mtime == mtime
|
|
429
|
+
):
|
|
430
|
+
return self._reader
|
|
431
|
+
|
|
432
|
+
self._close_reader()
|
|
433
|
+
self._reader = tecio.open(self._filename, "r")
|
|
434
|
+
self._reader_path = self._filename
|
|
435
|
+
self._reader_mtime = mtime
|
|
436
|
+
self._sync_selections(self._reader)
|
|
437
|
+
return self._reader
|
|
438
|
+
|
|
439
|
+
def _sync_selections(self, reader: _TecioReader) -> None:
|
|
440
|
+
"""(Re)populate the array and zone selections for a newly opened file."""
|
|
441
|
+
self._array_selection.RemoveAllArrays()
|
|
442
|
+
for name in reader.variables:
|
|
443
|
+
self._array_selection.AddArray(name)
|
|
444
|
+
|
|
445
|
+
self._zone_selection.RemoveAllArrays()
|
|
446
|
+
self._zone_keys.clear()
|
|
447
|
+
for zone in reader.zone:
|
|
448
|
+
title = zone.title or f"Zone {zone.zone_index}"
|
|
449
|
+
key = f"{zone.zone_index}: {title}"
|
|
450
|
+
self._zone_keys[zone.zone_index] = key
|
|
451
|
+
default_enabled = not _is_deactivated_boundary_zone(zone)
|
|
452
|
+
self._zone_selection.AddArray(key, default_enabled)
|
|
453
|
+
|
|
454
|
+
# -- FileName --------------------------------------------------------------------
|
|
455
|
+
|
|
456
|
+
@smproperty.stringvector(name="FileName")
|
|
457
|
+
@smdomain.filelist()
|
|
458
|
+
@smhint.filechooser(extensions=_EXTENSIONS, file_description=_FILE_DESCRIPTION)
|
|
459
|
+
def SetFileName(self, name: str | None) -> None:
|
|
460
|
+
"""Set the path to the Tecplot file.
|
|
461
|
+
|
|
462
|
+
Accepts ``.szplt``/``.plt``/``.bin``/``.dat``/``.tec``.
|
|
463
|
+
"""
|
|
464
|
+
name = name or None
|
|
465
|
+
if self._filename != name:
|
|
466
|
+
self._filename = name
|
|
467
|
+
self._close_reader()
|
|
468
|
+
self.Modified()
|
|
469
|
+
|
|
470
|
+
# -- Array / zone selection --------------------------------------------------------
|
|
471
|
+
|
|
472
|
+
@smproperty.dataarrayselection(name="Arrays")
|
|
473
|
+
def GetDataArraySelection(self) -> vtkDataArraySelection:
|
|
474
|
+
"""Which dataset variables load as point/cell data arrays."""
|
|
475
|
+
return self._array_selection
|
|
476
|
+
|
|
477
|
+
@smproperty.dataarrayselection(name="Zones")
|
|
478
|
+
def GetZoneSelection(self) -> vtkDataArraySelection:
|
|
479
|
+
"""Which zones load as blocks -- useful to skip zones in large files."""
|
|
480
|
+
return self._zone_selection
|
|
481
|
+
|
|
482
|
+
# -- Coordinate variable overrides ------------------------------------------------
|
|
483
|
+
|
|
484
|
+
@smproperty.stringvector(name="VariableInfo", information_only="1")
|
|
485
|
+
def GetVariableInfo(self) -> list[str]:
|
|
486
|
+
"""Choices offered by the X/Y/Z array dropdowns.
|
|
487
|
+
|
|
488
|
+
Eg ``"(auto)"`` plus every variable.
|
|
489
|
+
"""
|
|
490
|
+
try:
|
|
491
|
+
reader = self._get_reader()
|
|
492
|
+
except Exception: # noqa: BLE001 - domain refresh must not raise into the UI
|
|
493
|
+
reader = None
|
|
494
|
+
names = list(reader.variables) if reader is not None else []
|
|
495
|
+
return ["(auto)", *names]
|
|
496
|
+
|
|
497
|
+
def _set_axis_override(self, axis: str, value: str) -> None:
|
|
498
|
+
resolved = None if value in ("", "(auto)") else value
|
|
499
|
+
attr = f"_{axis}_override"
|
|
500
|
+
if getattr(self, attr) != resolved:
|
|
501
|
+
setattr(self, attr, resolved)
|
|
502
|
+
self.Modified()
|
|
503
|
+
|
|
504
|
+
@smproperty.stringvector(
|
|
505
|
+
name="XArray", number_of_elements="1", default_values=["(auto)"]
|
|
506
|
+
)
|
|
507
|
+
@smdomain.xml("""
|
|
508
|
+
<StringListDomain name="list">
|
|
509
|
+
<RequiredProperties>
|
|
510
|
+
<Property name="VariableInfo" function="StringInfo"/>
|
|
511
|
+
</RequiredProperties>
|
|
512
|
+
</StringListDomain>
|
|
513
|
+
""")
|
|
514
|
+
def SetXArray(self, value: str) -> None:
|
|
515
|
+
"""Override auto-detection of the X-coordinate variable."""
|
|
516
|
+
self._set_axis_override("x", value)
|
|
517
|
+
|
|
518
|
+
@smproperty.stringvector(
|
|
519
|
+
name="YArray", number_of_elements="1", default_values=["(auto)"]
|
|
520
|
+
)
|
|
521
|
+
@smdomain.xml("""
|
|
522
|
+
<StringListDomain name="list">
|
|
523
|
+
<RequiredProperties>
|
|
524
|
+
<Property name="VariableInfo" function="StringInfo"/>
|
|
525
|
+
</RequiredProperties>
|
|
526
|
+
</StringListDomain>
|
|
527
|
+
""")
|
|
528
|
+
def SetYArray(self, value: str) -> None:
|
|
529
|
+
"""Override auto-detection of the Y-coordinate variable."""
|
|
530
|
+
self._set_axis_override("y", value)
|
|
531
|
+
|
|
532
|
+
@smproperty.stringvector(
|
|
533
|
+
name="ZArray", number_of_elements="1", default_values=["(auto)"]
|
|
534
|
+
)
|
|
535
|
+
@smdomain.xml("""
|
|
536
|
+
<StringListDomain name="list">
|
|
537
|
+
<RequiredProperties>
|
|
538
|
+
<Property name="VariableInfo" function="StringInfo"/>
|
|
539
|
+
</RequiredProperties>
|
|
540
|
+
</StringListDomain>
|
|
541
|
+
""")
|
|
542
|
+
def SetZArray(self, value: str) -> None:
|
|
543
|
+
"""Override auto-detection of the Z-coordinate variable (else 2-D: Z=0)."""
|
|
544
|
+
self._set_axis_override("z", value)
|
|
545
|
+
|
|
546
|
+
@smproperty.stringvector(
|
|
547
|
+
name="UArray", number_of_elements="1", default_values=["(auto)"]
|
|
548
|
+
)
|
|
549
|
+
@smdomain.xml("""
|
|
550
|
+
<StringListDomain name="list">
|
|
551
|
+
<RequiredProperties>
|
|
552
|
+
<Property name="VariableInfo" function="StringInfo"/>
|
|
553
|
+
</RequiredProperties>
|
|
554
|
+
</StringListDomain>
|
|
555
|
+
""")
|
|
556
|
+
def SetUArray(self, value: str) -> None:
|
|
557
|
+
"""Override the vector-array U component (else ``Common.UVar``, if set)."""
|
|
558
|
+
self._set_axis_override("u", value)
|
|
559
|
+
|
|
560
|
+
@smproperty.stringvector(
|
|
561
|
+
name="VArray", number_of_elements="1", default_values=["(auto)"]
|
|
562
|
+
)
|
|
563
|
+
@smdomain.xml("""
|
|
564
|
+
<StringListDomain name="list">
|
|
565
|
+
<RequiredProperties>
|
|
566
|
+
<Property name="VariableInfo" function="StringInfo"/>
|
|
567
|
+
</RequiredProperties>
|
|
568
|
+
</StringListDomain>
|
|
569
|
+
""")
|
|
570
|
+
def SetVArray(self, value: str) -> None:
|
|
571
|
+
"""Override the vector-array V component (else ``Common.VVar``, if set)."""
|
|
572
|
+
self._set_axis_override("v", value)
|
|
573
|
+
|
|
574
|
+
@smproperty.stringvector(
|
|
575
|
+
name="WArray", number_of_elements="1", default_values=["(auto)"]
|
|
576
|
+
)
|
|
577
|
+
@smdomain.xml("""
|
|
578
|
+
<StringListDomain name="list">
|
|
579
|
+
<RequiredProperties>
|
|
580
|
+
<Property name="VariableInfo" function="StringInfo"/>
|
|
581
|
+
</RequiredProperties>
|
|
582
|
+
</StringListDomain>
|
|
583
|
+
""")
|
|
584
|
+
def SetWArray(self, value: str) -> None:
|
|
585
|
+
"""Override the vector-array W component (else ``Common.WVar``, if set)."""
|
|
586
|
+
self._set_axis_override("w", value)
|
|
587
|
+
|
|
588
|
+
def _resolve_vector_component_names(
|
|
589
|
+
self, reader: _TecioReader
|
|
590
|
+
) -> tuple[str | None, str | None, str | None]:
|
|
591
|
+
"""Pick the dataset variables to use as the U, V, W vector components.
|
|
592
|
+
|
|
593
|
+
Unlike coordinates, there's no name-based or positional fallback here. Tecplot
|
|
594
|
+
vector-variable names (velocity or momentum components) vary too much across
|
|
595
|
+
solvers to guess reliably. A component resolves only via a manual override
|
|
596
|
+
(``UArray``/``VArray``/``WArray``) or the file's own
|
|
597
|
+
``Common.UVar``/``Common.VVar``/``Common.WVar`` dataset aux data; anything left
|
|
598
|
+
unresolved stays ``None`` and is treated as zero when the vector is assembled,
|
|
599
|
+
so a 2-D (U, V only) flow still gets a valid, glyph-able vector.
|
|
600
|
+
"""
|
|
601
|
+
names = list(reader.variables)
|
|
602
|
+
|
|
603
|
+
def override(axis: str) -> str | None:
|
|
604
|
+
value = getattr(self, f"_{axis}_override")
|
|
605
|
+
return value if value in names else None
|
|
606
|
+
|
|
607
|
+
u = override("u") or _resolve_common_var(reader, "Common.UVar")
|
|
608
|
+
v = override("v") or _resolve_common_var(reader, "Common.VVar")
|
|
609
|
+
w = override("w") or _resolve_common_var(reader, "Common.WVar")
|
|
610
|
+
return u, v, w
|
|
611
|
+
|
|
612
|
+
@staticmethod
|
|
613
|
+
def _resolve_vector_array_name(reader: _TecioReader) -> str:
|
|
614
|
+
"""Return "Velocity" or "Momentum" per Common.VectorVarsAreVelocity.
|
|
615
|
+
|
|
616
|
+
Per the Tecplot data format guide: if that dataset aux item is truthy, the
|
|
617
|
+
U/V/W vector is velocity; otherwise Tecplot's own documented default is to
|
|
618
|
+
assume momentum, which this mirrors.
|
|
619
|
+
"""
|
|
620
|
+
is_velocity = reader.auxdata.as_bool("Common.VectorVarsAreVelocity")
|
|
621
|
+
return "Velocity" if is_velocity else "Momentum"
|
|
622
|
+
|
|
623
|
+
def _resolve_coordinate_names(
|
|
624
|
+
self, reader: _TecioReader
|
|
625
|
+
) -> tuple[str | None, str | None, str | None]:
|
|
626
|
+
"""Pick the dataset variables to use as X, Y, and Z point coordinates.
|
|
627
|
+
|
|
628
|
+
Priority order: manual override, then the ``Common.XVar``/``YVar``/``ZVar``
|
|
629
|
+
dataset aux items if the file specifies them, then a name match (see
|
|
630
|
+
:func:`_autodetect_axis`), then -- for X and Y only -- positional fallback to
|
|
631
|
+
the first and second dataset variables, since almost every Tecplot CFD file
|
|
632
|
+
leads with its coordinates. Z has no positional fallback beyond the third
|
|
633
|
+
variable and defaults to all-zero (2-D data) when nothing is found.
|
|
634
|
+
"""
|
|
635
|
+
names = list(reader.variables)
|
|
636
|
+
|
|
637
|
+
def override(axis: str) -> str | None:
|
|
638
|
+
value = getattr(self, f"_{axis}_override")
|
|
639
|
+
return value if value in names else None
|
|
640
|
+
|
|
641
|
+
x = (
|
|
642
|
+
override("x")
|
|
643
|
+
or _resolve_common_var(reader, "Common.XVar")
|
|
644
|
+
or _autodetect_axis(names, "x")
|
|
645
|
+
)
|
|
646
|
+
y = (
|
|
647
|
+
override("y")
|
|
648
|
+
or _resolve_common_var(reader, "Common.YVar")
|
|
649
|
+
or _autodetect_axis(names, "y")
|
|
650
|
+
)
|
|
651
|
+
z = (
|
|
652
|
+
override("z")
|
|
653
|
+
or _resolve_common_var(reader, "Common.ZVar")
|
|
654
|
+
or _autodetect_axis(names, "z")
|
|
655
|
+
)
|
|
656
|
+
|
|
657
|
+
if x is None and names:
|
|
658
|
+
x = names[0]
|
|
659
|
+
if y is None and len(names) > 1:
|
|
660
|
+
y = names[1]
|
|
661
|
+
if z is None and len(names) > 2 and names[2] not in (x, y):
|
|
662
|
+
z = names[2]
|
|
663
|
+
|
|
664
|
+
return x, y, z
|
|
665
|
+
|
|
666
|
+
# -- Time -----------------------------------------------------------------------
|
|
667
|
+
|
|
668
|
+
def _compute_timesteps(self, reader: _TecioReader) -> list[float]:
|
|
669
|
+
"""Sorted, distinct solution times across all non-static zones.
|
|
670
|
+
|
|
671
|
+
Zones with ``strand_id == 0`` are the standard Tecplot convention for
|
|
672
|
+
static/always-present geometry, so they're excluded from the time axis and
|
|
673
|
+
instead included at every requested time (see :meth:`RequestData`).
|
|
674
|
+
"""
|
|
675
|
+
times = {zone.solution_time for zone in reader.zone if zone.strand_id != 0}
|
|
676
|
+
return sorted(times)
|
|
677
|
+
|
|
678
|
+
@smproperty.doublevector(
|
|
679
|
+
name="TimestepValues", information_only="1", si_class="vtkSITimeStepsProperty"
|
|
680
|
+
)
|
|
681
|
+
def GetTimestepValues(self) -> list[float]:
|
|
682
|
+
"""Publish the dataset's discrete solution times to ParaView's time UI."""
|
|
683
|
+
try:
|
|
684
|
+
reader = self._get_reader()
|
|
685
|
+
except Exception: # noqa: BLE001 - domain refresh must not raise into the UI
|
|
686
|
+
return []
|
|
687
|
+
return self._compute_timesteps(reader) if reader is not None else []
|
|
688
|
+
|
|
689
|
+
def _get_update_time(self, out_info: Any) -> float | None:
|
|
690
|
+
"""Resolve the pipeline's requested time to one of the dataset's own times."""
|
|
691
|
+
reader = self._reader
|
|
692
|
+
if reader is None:
|
|
693
|
+
return None
|
|
694
|
+
timesteps = self._compute_timesteps(reader)
|
|
695
|
+
if not timesteps:
|
|
696
|
+
return None
|
|
697
|
+
|
|
698
|
+
executive = self.GetExecutive()
|
|
699
|
+
if out_info.Has(executive.UPDATE_TIME_STEP()):
|
|
700
|
+
requested = out_info.Get(executive.UPDATE_TIME_STEP())
|
|
701
|
+
result = timesteps[0]
|
|
702
|
+
for t in timesteps:
|
|
703
|
+
if t <= requested + 1e-9:
|
|
704
|
+
result = t
|
|
705
|
+
else:
|
|
706
|
+
break
|
|
707
|
+
return result
|
|
708
|
+
return timesteps[0]
|
|
709
|
+
|
|
710
|
+
# -- Pipeline: RequestInformation --------------------------------------------------
|
|
711
|
+
|
|
712
|
+
def RequestInformation(self, request, inInfoVec, outInfoVec) -> int:
|
|
713
|
+
"""Publish the dataset's time steps, if any, before ``RequestData`` runs."""
|
|
714
|
+
try:
|
|
715
|
+
reader = self._get_reader()
|
|
716
|
+
except Exception as exc: # noqa: BLE001 - report and produce empty output
|
|
717
|
+
print(f"[TecplotTecioReader] {exc}")
|
|
718
|
+
return 1
|
|
719
|
+
if reader is None:
|
|
720
|
+
return 1
|
|
721
|
+
|
|
722
|
+
executive = self.GetExecutive()
|
|
723
|
+
out_info = outInfoVec.GetInformationObject(0)
|
|
724
|
+
out_info.Remove(executive.TIME_STEPS())
|
|
725
|
+
out_info.Remove(executive.TIME_RANGE())
|
|
726
|
+
|
|
727
|
+
timesteps = self._compute_timesteps(reader)
|
|
728
|
+
if timesteps:
|
|
729
|
+
for t in timesteps:
|
|
730
|
+
out_info.Append(executive.TIME_STEPS(), t)
|
|
731
|
+
out_info.Append(executive.TIME_RANGE(), timesteps[0])
|
|
732
|
+
out_info.Append(executive.TIME_RANGE(), timesteps[-1])
|
|
733
|
+
return 1
|
|
734
|
+
|
|
735
|
+
# -- Pipeline: RequestData ---------------------------------------------------------
|
|
736
|
+
|
|
737
|
+
def RequestData(self, request, inInfoVec, outInfoVec) -> int:
|
|
738
|
+
"""Build the ``vtkMultiBlockDataSet`` output for the requested time step."""
|
|
739
|
+
output = vtkMultiBlockDataSet.GetData(outInfoVec, 0)
|
|
740
|
+
|
|
741
|
+
try:
|
|
742
|
+
reader = self._get_reader()
|
|
743
|
+
except Exception as exc: # noqa: BLE001 - report and produce empty output
|
|
744
|
+
print(f"[TecplotTecioReader] {exc}")
|
|
745
|
+
return 1
|
|
746
|
+
if reader is None:
|
|
747
|
+
return 1
|
|
748
|
+
|
|
749
|
+
requested_time = self._get_update_time(outInfoVec.GetInformationObject(0))
|
|
750
|
+
x_name, y_name, z_name = self._resolve_coordinate_names(reader)
|
|
751
|
+
u_name, v_name, w_name = self._resolve_vector_component_names(reader)
|
|
752
|
+
vector_array_name = self._resolve_vector_array_name(reader)
|
|
753
|
+
|
|
754
|
+
_add_aux_field_data(output.GetFieldData(), reader.auxdata.items())
|
|
755
|
+
self._add_variable_aux_field_data(output.GetFieldData(), reader)
|
|
756
|
+
|
|
757
|
+
block_index = 0
|
|
758
|
+
for zone in reader.zone:
|
|
759
|
+
key = self._zone_keys.get(zone.zone_index)
|
|
760
|
+
if key is not None and not self._zone_selection.ArrayIsEnabled(key):
|
|
761
|
+
continue
|
|
762
|
+
if (
|
|
763
|
+
requested_time is not None
|
|
764
|
+
and zone.strand_id != 0
|
|
765
|
+
and not math.isclose(zone.solution_time, requested_time, abs_tol=1e-9)
|
|
766
|
+
):
|
|
767
|
+
continue
|
|
768
|
+
|
|
769
|
+
try:
|
|
770
|
+
dataset = self._build_zone_dataset(
|
|
771
|
+
zone,
|
|
772
|
+
x_name,
|
|
773
|
+
y_name,
|
|
774
|
+
z_name,
|
|
775
|
+
u_name,
|
|
776
|
+
v_name,
|
|
777
|
+
w_name,
|
|
778
|
+
vector_array_name,
|
|
779
|
+
)
|
|
780
|
+
except _UnsupportedZoneError as exc:
|
|
781
|
+
print(
|
|
782
|
+
f"[TecplotTecioReader] Skipping zone {zone.zone_index} "
|
|
783
|
+
f"({zone.title!r}): {exc}"
|
|
784
|
+
)
|
|
785
|
+
continue
|
|
786
|
+
except Exception as exc: # noqa: BLE001 - one bad zone shouldn't fail the read
|
|
787
|
+
print(
|
|
788
|
+
f"[TecplotTecioReader] Error reading zone {zone.zone_index} "
|
|
789
|
+
f"({zone.title!r}), skipping: {exc}"
|
|
790
|
+
)
|
|
791
|
+
continue
|
|
792
|
+
|
|
793
|
+
output.SetBlock(block_index, dataset)
|
|
794
|
+
name = zone.title or f"Zone {zone.zone_index}"
|
|
795
|
+
output.GetMetaData(block_index).Set(vtkCompositeDataSet.NAME(), name)
|
|
796
|
+
block_index += 1
|
|
797
|
+
|
|
798
|
+
if requested_time is not None:
|
|
799
|
+
output.GetInformation().Set(output.DATA_TIME_STEP(), requested_time)
|
|
800
|
+
return 1
|
|
801
|
+
|
|
802
|
+
def _add_variable_aux_field_data(
|
|
803
|
+
self, field_data: vtkFieldData, reader: _TecioReader
|
|
804
|
+
) -> None:
|
|
805
|
+
"""Attach variable-level aux data to *field_data*, prefixed by variable name."""
|
|
806
|
+
names = reader.variables
|
|
807
|
+
for i in range(1, reader.num_vars + 1):
|
|
808
|
+
aux = reader.get_var_auxdata(i)
|
|
809
|
+
if not len(aux):
|
|
810
|
+
continue
|
|
811
|
+
var_name = names[i - 1]
|
|
812
|
+
_add_aux_field_data(
|
|
813
|
+
field_data, aux.items(), key_fn=lambda k, v=var_name: f"{v}::{k}"
|
|
814
|
+
)
|
|
815
|
+
|
|
816
|
+
# -- Zone -> VTK dataset conversion ------------------------------------------------
|
|
817
|
+
|
|
818
|
+
def _build_zone_dataset(
|
|
819
|
+
self,
|
|
820
|
+
zone: Any,
|
|
821
|
+
x_name: str | None,
|
|
822
|
+
y_name: str | None,
|
|
823
|
+
z_name: str | None,
|
|
824
|
+
u_name: str | None,
|
|
825
|
+
v_name: str | None,
|
|
826
|
+
w_name: str | None,
|
|
827
|
+
vector_array_name: str,
|
|
828
|
+
) -> vtkStructuredGrid | vtkUnstructuredGrid:
|
|
829
|
+
"""Convert one Tecplot zone into a VTK type.
|
|
830
|
+
|
|
831
|
+
Structured/Unstructured correspond to``vtkStructuredGrid``/
|
|
832
|
+
``vtkUnstructuredGrid`` respectively.
|
|
833
|
+
"""
|
|
834
|
+
if zone.zone_type == ZoneType.ORDERED:
|
|
835
|
+
dataset: vtkStructuredGrid | vtkUnstructuredGrid = (
|
|
836
|
+
self._build_structured_grid(zone, x_name, y_name, z_name)
|
|
837
|
+
)
|
|
838
|
+
else:
|
|
839
|
+
cell_type = _VTK_CELL_TYPE.get(zone.zone_type)
|
|
840
|
+
if cell_type is None:
|
|
841
|
+
raise _UnsupportedZoneError(
|
|
842
|
+
f"zone type {zone.zone_type.name} is not yet supported by this "
|
|
843
|
+
"reader (only ORDERED and classic FE zones -- line, triangle, "
|
|
844
|
+
"quadrilateral, tetrahedron, brick -- are converted; "
|
|
845
|
+
"polygon/polyhedron/mixed zones are not)"
|
|
846
|
+
)
|
|
847
|
+
dataset = self._build_unstructured_grid(
|
|
848
|
+
zone, cell_type, x_name, y_name, z_name
|
|
849
|
+
)
|
|
850
|
+
|
|
851
|
+
_add_aux_field_data(dataset.GetFieldData(), zone.auxdata.items())
|
|
852
|
+
self._add_variable_arrays(dataset, zone)
|
|
853
|
+
self._add_vector_array(dataset, zone, vector_array_name, u_name, v_name, w_name)
|
|
854
|
+
return dataset
|
|
855
|
+
|
|
856
|
+
def _build_structured_grid(
|
|
857
|
+
self, zone: Any, x_name: str | None, y_name: str | None, z_name: str | None
|
|
858
|
+
) -> vtkStructuredGrid:
|
|
859
|
+
"""Build a ``vtkStructuredGrid`` for an ORDERED zone."""
|
|
860
|
+
ni, nj, nk = zone.dimensions
|
|
861
|
+
shape = (ni, nj, nk)
|
|
862
|
+
x = self._fetch_ordered_coordinate(zone, x_name, shape)
|
|
863
|
+
y = self._fetch_ordered_coordinate(zone, y_name, shape)
|
|
864
|
+
z = self._fetch_ordered_coordinate(zone, z_name, shape)
|
|
865
|
+
|
|
866
|
+
# Tecplot's IJK arrays come back Fortran-ordered (I fastest); raveling the same
|
|
867
|
+
# way lines the flat point list up with VTK's own I-fastest point ordering, so
|
|
868
|
+
# no per-point reindexing is needed.
|
|
869
|
+
points = np.empty((ni * nj * nk, 3), dtype=np.float64)
|
|
870
|
+
points[:, 0] = x.ravel(order="F")
|
|
871
|
+
points[:, 1] = y.ravel(order="F")
|
|
872
|
+
points[:, 2] = z.ravel(order="F")
|
|
873
|
+
|
|
874
|
+
vtk_points = vtkPoints()
|
|
875
|
+
vtk_points.SetData(numpy_support.numpy_to_vtk(points, deep=True))
|
|
876
|
+
|
|
877
|
+
grid = vtkStructuredGrid()
|
|
878
|
+
grid.SetDimensions(ni, nj, nk)
|
|
879
|
+
grid.SetPoints(vtk_points)
|
|
880
|
+
return grid
|
|
881
|
+
|
|
882
|
+
@staticmethod
|
|
883
|
+
def _fetch_ordered_coordinate(
|
|
884
|
+
zone: Any, name: str | None, shape: tuple[int, int, int]
|
|
885
|
+
) -> npt.NDArray[np.float64]:
|
|
886
|
+
"""Return an ``(I, J, K)`` nodal coordinate array for *name*, or zeros."""
|
|
887
|
+
if name is None:
|
|
888
|
+
return np.zeros(shape, dtype=np.float64)
|
|
889
|
+
values = zone.get_array(name)
|
|
890
|
+
if values is None:
|
|
891
|
+
raise _UnsupportedZoneError(
|
|
892
|
+
f"coordinate variable '{name}' has no data in this zone (it is passive)"
|
|
893
|
+
)
|
|
894
|
+
if values.shape != shape:
|
|
895
|
+
raise _UnsupportedZoneError(
|
|
896
|
+
f"coordinate variable '{name}' has shape {values.shape}, expected "
|
|
897
|
+
f"nodal shape {shape} -- is it cell-centered?"
|
|
898
|
+
)
|
|
899
|
+
return values.astype(np.float64, copy=False)
|
|
900
|
+
|
|
901
|
+
def _build_unstructured_grid(
|
|
902
|
+
self,
|
|
903
|
+
zone: Any,
|
|
904
|
+
cell_type: int,
|
|
905
|
+
x_name: str | None,
|
|
906
|
+
y_name: str | None,
|
|
907
|
+
z_name: str | None,
|
|
908
|
+
) -> vtkUnstructuredGrid:
|
|
909
|
+
"""Build a ``vtkUnstructuredGrid`` for a classic finite-element zone."""
|
|
910
|
+
n_nodes = zone.num_nodes
|
|
911
|
+
x = self._fetch_flat_coordinate(zone, x_name, n_nodes)
|
|
912
|
+
y = self._fetch_flat_coordinate(zone, y_name, n_nodes)
|
|
913
|
+
z = self._fetch_flat_coordinate(zone, z_name, n_nodes)
|
|
914
|
+
|
|
915
|
+
points = np.column_stack((x, y, z)).astype(np.float64, copy=False)
|
|
916
|
+
vtk_points = vtkPoints()
|
|
917
|
+
vtk_points.SetData(numpy_support.numpy_to_vtk(points, deep=True))
|
|
918
|
+
|
|
919
|
+
node_map = zone.node_map
|
|
920
|
+
if node_map is None or node_map.size == 0:
|
|
921
|
+
raise _UnsupportedZoneError("zone has no connectivity (empty node map)")
|
|
922
|
+
|
|
923
|
+
n_cells, nodes_per_cell = node_map.shape
|
|
924
|
+
# tecio node maps are 1-based (Tecplot convention); VTK point ids are 0-based.
|
|
925
|
+
connectivity = (node_map.astype(np.int64, copy=False) - 1).ravel()
|
|
926
|
+
offsets = np.arange(
|
|
927
|
+
0, (n_cells + 1) * nodes_per_cell, nodes_per_cell, dtype=np.int64
|
|
928
|
+
)
|
|
929
|
+
|
|
930
|
+
cell_array = vtkCellArray()
|
|
931
|
+
cell_array.SetData(
|
|
932
|
+
numpy_support.numpy_to_vtkIdTypeArray(offsets, deep=True),
|
|
933
|
+
numpy_support.numpy_to_vtkIdTypeArray(connectivity, deep=True),
|
|
934
|
+
)
|
|
935
|
+
|
|
936
|
+
grid = vtkUnstructuredGrid()
|
|
937
|
+
grid.SetPoints(vtk_points)
|
|
938
|
+
grid.SetCells(cell_type, cell_array)
|
|
939
|
+
return grid
|
|
940
|
+
|
|
941
|
+
@staticmethod
|
|
942
|
+
def _fetch_flat_coordinate(
|
|
943
|
+
zone: Any, name: str | None, n_nodes: int
|
|
944
|
+
) -> npt.NDArray[np.float64]:
|
|
945
|
+
"""Return a flat, length-``n_nodes`` coordinate array for *name*, or zeros."""
|
|
946
|
+
if name is None:
|
|
947
|
+
return np.zeros(n_nodes, dtype=np.float64)
|
|
948
|
+
values = zone.get_array(name)
|
|
949
|
+
if values is None:
|
|
950
|
+
raise _UnsupportedZoneError(
|
|
951
|
+
f"coordinate variable '{name}' has no data in this zone (it is passive)"
|
|
952
|
+
)
|
|
953
|
+
if values.size != n_nodes:
|
|
954
|
+
raise _UnsupportedZoneError(
|
|
955
|
+
f"coordinate variable '{name}' has {values.size} values, expected "
|
|
956
|
+
f"{n_nodes} nodal values"
|
|
957
|
+
)
|
|
958
|
+
return values.astype(np.float64, copy=False)
|
|
959
|
+
|
|
960
|
+
def _add_variable_arrays(
|
|
961
|
+
self, dataset: vtkStructuredGrid | vtkUnstructuredGrid, zone: Any
|
|
962
|
+
) -> None:
|
|
963
|
+
"""Attach every selected, active variable to point or cell data.
|
|
964
|
+
|
|
965
|
+
Node-located (``NODAL``) variables become point data; cell-located
|
|
966
|
+
(``CELL_CENTERED``) variables become cell data. A variable whose array length
|
|
967
|
+
doesn't match the dataset's point/cell count is skipped rather than raising, so
|
|
968
|
+
one bad variable doesn't take an otherwise-good zone down with it.
|
|
969
|
+
"""
|
|
970
|
+
n_points = dataset.GetNumberOfPoints()
|
|
971
|
+
n_cells = dataset.GetNumberOfCells()
|
|
972
|
+
|
|
973
|
+
for var in zone.variable:
|
|
974
|
+
if not self._array_selection.ArrayIsEnabled(var.name):
|
|
975
|
+
continue
|
|
976
|
+
values = var.values
|
|
977
|
+
if values is None: # passive, or genuinely no data
|
|
978
|
+
continue
|
|
979
|
+
|
|
980
|
+
flat = values.ravel(order="F") if values.ndim == 3 else np.ravel(values)
|
|
981
|
+
vtk_array = numpy_support.numpy_to_vtk(
|
|
982
|
+
np.ascontiguousarray(flat), deep=True
|
|
983
|
+
)
|
|
984
|
+
vtk_array.SetName(var.name)
|
|
985
|
+
|
|
986
|
+
if var.value_location == ValueLocation.NODAL:
|
|
987
|
+
if flat.size != n_points:
|
|
988
|
+
continue
|
|
989
|
+
dataset.GetPointData().AddArray(vtk_array)
|
|
990
|
+
else:
|
|
991
|
+
if flat.size != n_cells:
|
|
992
|
+
continue
|
|
993
|
+
dataset.GetCellData().AddArray(vtk_array)
|
|
994
|
+
|
|
995
|
+
def _add_vector_array(
|
|
996
|
+
self,
|
|
997
|
+
dataset: vtkStructuredGrid | vtkUnstructuredGrid,
|
|
998
|
+
zone: Any,
|
|
999
|
+
array_name: str,
|
|
1000
|
+
u_name: str | None,
|
|
1001
|
+
v_name: str | None,
|
|
1002
|
+
w_name: str | None,
|
|
1003
|
+
) -> None:
|
|
1004
|
+
"""Assemble the U/V/W components into one 3-component vector array.
|
|
1005
|
+
|
|
1006
|
+
Skipped entirely if none of U/V/W resolved for the dataset. A component that did
|
|
1007
|
+
resolve for the dataset but is passive or absent in *this specific* zone is
|
|
1008
|
+
treated as zero for that zone, same as an axis that never resolved at all. All
|
|
1009
|
+
contributing components must share one value location (``NODAL`` or
|
|
1010
|
+
``CELL_CENTERED``) and array length; a mismatch skips vector assembly for this
|
|
1011
|
+
zone with a printed warning, since the rest of the zone is still good.
|
|
1012
|
+
"""
|
|
1013
|
+
if u_name is None and v_name is None and w_name is None:
|
|
1014
|
+
return
|
|
1015
|
+
|
|
1016
|
+
location: ValueLocation | None = None
|
|
1017
|
+
flat_components: list[npt.NDArray[np.float64] | None] = []
|
|
1018
|
+
for name in (u_name, v_name, w_name):
|
|
1019
|
+
if name is None:
|
|
1020
|
+
flat_components.append(None)
|
|
1021
|
+
continue
|
|
1022
|
+
var = zone.variable[name]
|
|
1023
|
+
values = var.values
|
|
1024
|
+
if values is None: # passive, or absent in this specific zone
|
|
1025
|
+
flat_components.append(None)
|
|
1026
|
+
continue
|
|
1027
|
+
|
|
1028
|
+
flat = values.ravel(order="F") if values.ndim == 3 else np.ravel(values)
|
|
1029
|
+
if location is None:
|
|
1030
|
+
location = var.value_location
|
|
1031
|
+
elif var.value_location != location:
|
|
1032
|
+
print(
|
|
1033
|
+
f"[TecplotTecioReader] Zone {zone.zone_index} ({zone.title!r}): "
|
|
1034
|
+
f"'{name}' has a different value location than the other "
|
|
1035
|
+
"vector components; skipping the vector for this zone."
|
|
1036
|
+
)
|
|
1037
|
+
return
|
|
1038
|
+
flat_components.append(flat)
|
|
1039
|
+
|
|
1040
|
+
if location is None:
|
|
1041
|
+
return # every resolved component was passive/absent in this zone
|
|
1042
|
+
|
|
1043
|
+
n_tuples = (
|
|
1044
|
+
dataset.GetNumberOfPoints()
|
|
1045
|
+
if location == ValueLocation.NODAL
|
|
1046
|
+
else dataset.GetNumberOfCells()
|
|
1047
|
+
)
|
|
1048
|
+
zeros = np.zeros(n_tuples, dtype=np.float64)
|
|
1049
|
+
columns = []
|
|
1050
|
+
for flat in flat_components:
|
|
1051
|
+
if flat is None:
|
|
1052
|
+
columns.append(zeros)
|
|
1053
|
+
elif flat.size != n_tuples:
|
|
1054
|
+
print(
|
|
1055
|
+
f"[TecplotTecioReader] Zone {zone.zone_index} ({zone.title!r}): "
|
|
1056
|
+
f"a vector component has {flat.size} values, expected "
|
|
1057
|
+
f"{n_tuples}; skipping the vector for this zone."
|
|
1058
|
+
)
|
|
1059
|
+
return
|
|
1060
|
+
else:
|
|
1061
|
+
columns.append(flat.astype(np.float64, copy=False))
|
|
1062
|
+
|
|
1063
|
+
vector = np.ascontiguousarray(np.column_stack(columns))
|
|
1064
|
+
vtk_array = numpy_support.numpy_to_vtk(vector, deep=True)
|
|
1065
|
+
vtk_array.SetName(array_name)
|
|
1066
|
+
|
|
1067
|
+
if location == ValueLocation.NODAL:
|
|
1068
|
+
dataset.GetPointData().AddArray(vtk_array)
|
|
1069
|
+
else:
|
|
1070
|
+
dataset.GetCellData().AddArray(vtk_array)
|