tecio-python 0.3.2__py3-none-any.whl → 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. tecio/__init__.py +29 -3
  2. tecio/_dat_read.py +296 -81
  3. tecio/_dat_write.py +206 -146
  4. tecio/_io.py +338 -73
  5. tecio/_meta.py +6 -6
  6. tecio/_plt_legacy.py +400 -0
  7. tecio/_plt_read.py +180 -41
  8. tecio/_plt_write.py +195 -125
  9. tecio/_reader.py +82 -37
  10. tecio/_szl_read.py +68 -61
  11. tecio/_szl_write.py +249 -188
  12. tecio/_writer.py +380 -134
  13. tecio/cli/tec2mat.py +21 -11
  14. tecio/cli/tecaux.py +77 -93
  15. tecio/cli/tecdump.py +1 -1
  16. tecio/cli/tecextract.py +68 -86
  17. tecio/cli/tecfix.py +45 -64
  18. tecio/cli/tecmerge.py +343 -66
  19. tecio/cli/teconvert.py +22 -37
  20. tecio/cli/tecscale.py +185 -109
  21. tecio/cli/tecslice.py +75 -100
  22. tecio/cli/tecstats.py +41 -41
  23. tecio_python-0.4.0.data/data/share/plugins/matlab/contourfe.m +111 -0
  24. tecio_python-0.4.0.data/data/share/plugins/matlab/tecmat2struct.m +137 -0
  25. {tecio_python-0.3.2.data/data/share/tecio → tecio_python-0.4.0.data/data/share}/plugins/paraview/TecplotTecioReader.py +9 -0
  26. tecio_python-0.4.0.data/data/share/plugins/paraview/TecplotTecioWriter.py +651 -0
  27. {tecio_python-0.3.2.dist-info → tecio_python-0.4.0.dist-info}/METADATA +1 -1
  28. tecio_python-0.4.0.dist-info/RECORD +38 -0
  29. {tecio_python-0.3.2.dist-info → tecio_python-0.4.0.dist-info}/WHEEL +1 -1
  30. tecio_python-0.3.2.dist-info/RECORD +0 -34
  31. {tecio_python-0.3.2.dist-info → tecio_python-0.4.0.dist-info}/entry_points.txt +0 -0
  32. {tecio_python-0.3.2.dist-info → tecio_python-0.4.0.dist-info}/licenses/LICENSE +0 -0
  33. {tecio_python-0.3.2.dist-info → tecio_python-0.4.0.dist-info}/licenses/NOTICE +0 -0
  34. {tecio_python-0.3.2.dist-info → tecio_python-0.4.0.dist-info}/top_level.txt +0 -0
tecio/_plt_legacy.py ADDED
@@ -0,0 +1,400 @@
1
+ r"""Read the legacy Tecplot PLT v7.5 binary format (magic ``#!TDV75 ``).
2
+
3
+ v7.5 is a legacy Tecplot binary format that is still used by a handful of CFD solvers
4
+ (eg CFD++). It has no StrandID, SolutionTime, value location (cell-centered data),
5
+ face-neighbor, or aux-data support at all. Its zone
6
+ header uses a single "Format" field that packs together what later versions split into
7
+ two independent things: POINT vs. BLOCK data packing, and ORDERED vs. FE zone
8
+ topology. For FE zones, the element type (triangle/quad/tet/brick) is packed into the
9
+ zone's KMax field.
10
+
11
+ This module implements v7.5 header and data-section parsing against an archived data
12
+ formatting guide provided by Tecplot support, and tested against v7.5 solution files
13
+ written by CFD++ where the data arrays coiuld be cross-checked against the same dataset
14
+ re-exported through a v11.2 PLT writer using the Tecplot GUI:
15
+
16
+ Notes:
17
+ * **Format field.** ``0`` = ORDERED, BLOCK packing. ``1`` = ORDERED, POINT
18
+ packing. ``2`` = FE, BLOCK packing (FEBLOCK). ``3`` = FE, POINT packing
19
+ (FEPOINT). POINT packing means all variables for node 0, then node 1, etc. See
20
+ ``_ZoneMeta.is_point_format`` and
21
+ ``TecplotPltVariableReader._get_point_format_values`` in :mod:`._plt_read`.
22
+ * **KMax as element type.** For FE zones (Format 2 or 3), IMax = number of points,
23
+ JMax = number of elements, and KMax is the element type: ``0`` = triangles, ``1``
24
+ = quadrilaterals, ``2`` = tetrahedrons, ``3`` = bricks. For ORDERED zones (Format
25
+ 0 or 1), IMax/JMax/KMax are the literal ordered dimensions (same as later
26
+ versions).
27
+ * **Connectivity is 1-based.**
28
+ * **Variable sharing ("repeat vars").** A zone header may declare that some
29
+ of its variables are "repeated" rather than stored. I do not have a v7.5 sample that
30
+ exercises this, so two details are inferred rather than confirmed: that the
31
+ repeated variable numbers are 0-based, and that the source is always the
32
+ immediately preceding zone (by analogy with the connectivity "repeat adjacency
33
+ list from previous zone" flag below, which the spec does state explicitly). Treat
34
+ this path as unverified against real bytes.
35
+ * **Connectivity sharing.** A zone's data section carries an explicit INT32 boolean,
36
+ "repeat adjacency list from previous zone": 1 means this zone's connectivity is
37
+ identical to the previous zone's and nothing further is stored on disk; 0 means an
38
+ ordinary INT32 * L * JMax connectivity block follows, L given by element type
39
+ (3/4/4/8 for triangle/quad/tet/brick).
40
+
41
+ Not supported (no such field exists in v7.5):
42
+ * Passive variables.
43
+ * Cell-centered data.
44
+ * StrandID / SolutionTime.
45
+ * Face-neighbor connections of any kind.
46
+ * Dataset, zone, or variable auxiliary data.
47
+
48
+ Format reference:
49
+ A Tecplot provided binary format description for v7.5, plus the cross checks
50
+ described above.
51
+ """
52
+
53
+ from __future__ import annotations
54
+
55
+ import os
56
+ from typing import TYPE_CHECKING
57
+
58
+ import numpy as np
59
+
60
+ from ._constants import ValueLocation, ZoneType
61
+ from ._plt_read import (
62
+ _DATATYPE_TO_NUMPY,
63
+ _INT_TO_DATATYPE,
64
+ _NODES_PER_ELEMENT,
65
+ PltReadError,
66
+ _read_float32,
67
+ _read_int32,
68
+ _read_string,
69
+ _skip_string,
70
+ _ZoneMeta,
71
+ )
72
+
73
+ if TYPE_CHECKING:
74
+ import io
75
+
76
+ from ._plt_read import _PltParser
77
+
78
+ # --------------------------------------------------------------------------------------
79
+ # Constants
80
+ # --------------------------------------------------------------------------------------
81
+
82
+ _MARKER_ZONE: float = 299.0
83
+ _MARKER_GEOM: float = 399.0
84
+ _MARKER_TEXT: float = 499.0
85
+ _MARKER_CUSTOM_LABEL: float = 599.0
86
+ _MARKER_EOHMARKER: float = 357.0
87
+
88
+ # Format field: (is_fe, is_point_packing).
89
+ _FORMAT_IS_FE: dict[int, bool] = {0: False, 1: False, 2: True, 3: True}
90
+ _FORMAT_IS_POINT: dict[int, bool] = {0: False, 1: True, 2: False, 3: True}
91
+
92
+ # KMax element type, FE zones only.
93
+ _ELEMENT_TYPE: dict[int, ZoneType] = {
94
+ 0: ZoneType.FETRIANGLE,
95
+ 1: ZoneType.FEQUADRILATERAL,
96
+ 2: ZoneType.FETETRAHEDRON,
97
+ 3: ZoneType.FEBRICK,
98
+ 4: ZoneType.FELINESEG,
99
+ }
100
+
101
+
102
+ # --------------------------------------------------------------------------------------
103
+ # Header section
104
+ # --------------------------------------------------------------------------------------
105
+
106
+
107
+ def parse_v75_header(parser: _PltParser, fp: io.BufferedIOBase) -> None:
108
+ """Parse a v75 header (magic and endian probe already consumed).
109
+
110
+ Populates ``parser.title``, ``parser.var_names``, and ``parser.zones``
111
+ directly, matching :meth:`_PltParser._parse_header`'s contract for every
112
+ other version.
113
+
114
+ Raises:
115
+ PltReadError: On an unrecognised header marker. v75 has no aux-data or
116
+ UserRec markers (they either postdate v75 or the format
117
+ description gives no discoverable marker for them), so anything
118
+ other than Zone/Geometry/Text/CustomLabel/EOHMARKER is treated as
119
+ a malformed file rather than silently skipped.
120
+ """
121
+ byte_order = parser.byte_order
122
+
123
+ parser.title = _read_string(fp, byte_order)
124
+ num_vars = _read_int32(fp, byte_order)
125
+ parser.var_names = [_read_string(fp, byte_order) for _ in range(num_vars)]
126
+
127
+ while True:
128
+ marker = _read_float32(fp, byte_order)
129
+
130
+ if abs(marker - _MARKER_EOHMARKER) < 0.5:
131
+ return
132
+ elif abs(marker - _MARKER_ZONE) < 0.5:
133
+ _parse_v75_zone_header(parser, fp)
134
+ elif abs(marker - _MARKER_GEOM) < 0.5: # pragma: no cover
135
+ # No real or hand-authored v75 sample has a geometry record;
136
+ # see _skip_v75_geometry's own docstring.
137
+ _skip_v75_geometry(fp, byte_order)
138
+ elif abs(marker - _MARKER_TEXT) < 0.5: # pragma: no cover
139
+ # No real or hand-authored v75 sample has a text record; see
140
+ # _skip_v75_text's own docstring.
141
+ _skip_v75_text(fp, byte_order)
142
+ elif abs(marker - _MARKER_CUSTOM_LABEL) < 0.5:
143
+ parser._skip_custom_label(fp)
144
+ else:
145
+ raise PltReadError(
146
+ f"Unexpected v75 header marker {marker:.1f} at file offset "
147
+ f"{fp.tell() - 4:#010x}."
148
+ )
149
+
150
+
151
+ def _parse_v75_zone_header(parser: _PltParser, fp: io.BufferedIOBase) -> None:
152
+ """Parse one v75 zone header record and append it to ``parser.zones``."""
153
+ byte_order = parser.byte_order
154
+ meta = _ZoneMeta()
155
+
156
+ meta.title = _read_string(fp, byte_order)
157
+ fmt = _read_int32(fp, byte_order)
158
+ _read_int32(fp, byte_order) # Zone Color, unused (-1 = let Tecplot decide)
159
+ i_dim = _read_int32(fp, byte_order)
160
+ j_dim = _read_int32(fp, byte_order)
161
+ k_dim = _read_int32(fp, byte_order)
162
+
163
+ is_fe = _FORMAT_IS_FE.get(fmt)
164
+ if is_fe is None:
165
+ raise PltReadError(
166
+ f"Zone {meta.title!r}: unrecognised v75 Format value {fmt} "
167
+ "(expected 0=BLOCK, 1=POINT, 2=FEBLOCK, or 3=FEPOINT)."
168
+ )
169
+ meta.is_point_format = _FORMAT_IS_POINT[fmt]
170
+
171
+ if is_fe:
172
+ element_type = _ELEMENT_TYPE.get(k_dim)
173
+ if element_type is None:
174
+ raise PltReadError(
175
+ f"Zone {meta.title!r}: unrecognised v75 FE element type {k_dim} "
176
+ "(expected 0=triangles, 1=quadrilaterals, 2=tetrahedrons, "
177
+ "3=bricks, or 4=line segments). Pyramids and prisms are stored "
178
+ "as bricks with repeated nodes."
179
+ )
180
+ meta.zone_type = element_type
181
+ meta.num_nodes = i_dim
182
+ meta.num_elements = j_dim
183
+ meta.connectivity_is_one_based = True
184
+ else:
185
+ meta.zone_type = ZoneType.ORDERED
186
+ meta.i_max, meta.j_max, meta.k_max = i_dim, j_dim, k_dim
187
+
188
+ num_vars = len(parser.var_names)
189
+ meta.value_locations = [ValueLocation.NODAL] * num_vars
190
+
191
+ parser.zones.append(meta)
192
+
193
+
194
+ def _skip_v75_geometry(
195
+ fp: io.BufferedIOBase, byte_order: str
196
+ ) -> None: # pragma: no cover
197
+ """Skip a v75 geometry record (marker already consumed).
198
+
199
+ Unlike the current-format geometry record, v75 has no DrawOrder field
200
+ (right after Scope) or Clipping field (at the very end); both are absent
201
+ from the format description. No real v75 sample exercises this record;
202
+ implemented directly from the format description. Not hand-authored
203
+ either: unlike the ORDERED/BLOCK/sharing cases in
204
+ ``tests/test_read_legacy_synthetic.py``, a geometry record has no
205
+ bearing on reading array data, so the cost of hand-building one
206
+ correctly isn't worth it for a record this module only has to skip
207
+ over, never interpret.
208
+ """
209
+ _read_int32(fp, byte_order) # CoordSys
210
+ _read_int32(fp, byte_order) # Scope
211
+ fp.read(24) # X, Y, Z starting location (3 x FLOAT64)
212
+ _read_int32(fp, byte_order) # Zone (0 = all)
213
+ _read_int32(fp, byte_order) # Color
214
+ _read_int32(fp, byte_order) # FillColor
215
+ _read_int32(fp, byte_order) # IsFilled
216
+ geom_type = _read_int32(fp, byte_order)
217
+ _read_int32(fp, byte_order) # LinePattern
218
+ fp.read(16) # PatternLength + LineThickness (2 x FLOAT64)
219
+ _read_int32(fp, byte_order) # NumEllipsePts
220
+ _read_int32(fp, byte_order) # ArrowheadStyle
221
+ _read_int32(fp, byte_order) # ArrowheadAttachment
222
+ fp.read(16) # ArrowheadSize + ArrowheadAngle (2 x FLOAT64)
223
+ _skip_string(fp, byte_order) # MacroFunctionCommand
224
+ gtype = _read_int32(fp, byte_order) # PolylineFieldDataType (1=float, 2=double)
225
+ gtype_bytes = 4 if gtype == 1 else 8
226
+
227
+ if geom_type in (0, 4): # Line, 3DLine
228
+ num_blocks = 3 if geom_type == 4 else 2 # 3DLine also has a Z-block
229
+ num_polylines = _read_int32(fp, byte_order)
230
+ for _ in range(num_polylines):
231
+ num_pts = _read_int32(fp, byte_order)
232
+ fp.read(num_pts * gtype_bytes * num_blocks)
233
+ elif geom_type == 1: # Rectangle
234
+ fp.read(16) # X, Y offset for far corner (2 x FLOAT64, always double)
235
+ elif geom_type == 2: # Circle
236
+ fp.read(8) # Radius (FLOAT64)
237
+ elif geom_type == 3: # Ellipse
238
+ fp.read(16) # X, Y radii (2 x FLOAT64)
239
+
240
+
241
+ def _skip_v75_text(fp: io.BufferedIOBase, byte_order: str) -> None: # pragma: no cover
242
+ """Skip a v75 text record (marker already consumed).
243
+
244
+ Unlike the current-format text record, v75's starting location is X, Y
245
+ only (no Z), and there is no Clipping field before the text string. No
246
+ real v75 sample exercises this record; implemented directly from the
247
+ format description. See ``_skip_v75_geometry``'s docstring for why this
248
+ isn't hand-authored either.
249
+ """
250
+ _read_int32(fp, byte_order) # CoordSys
251
+ _read_int32(fp, byte_order) # Scope
252
+ fp.read(16) # X, Y starting location (2 x FLOAT64)
253
+ _read_int32(fp, byte_order) # FontType
254
+ _read_int32(fp, byte_order) # CharHeightUnits
255
+ fp.read(8) # Height (FLOAT64)
256
+ _read_int32(fp, byte_order) # TextBoxType
257
+ fp.read(16) # Margin + MarginLinewidth (2 x FLOAT64)
258
+ _read_int32(fp, byte_order) # TextBoxOutlineColor
259
+ _read_int32(fp, byte_order) # TextBoxFillColor
260
+ fp.read(16) # Angle + LineSpacing (2 x FLOAT64)
261
+ _read_int32(fp, byte_order) # TextAnchor
262
+ _read_int32(fp, byte_order) # Zone (0 = all)
263
+ _read_int32(fp, byte_order) # Color
264
+ _skip_string(fp, byte_order) # MacroFunctionCommand
265
+ _skip_string(fp, byte_order) # The text itself
266
+
267
+
268
+ # --------------------------------------------------------------------------------------
269
+ # Data section
270
+ # --------------------------------------------------------------------------------------
271
+
272
+
273
+ def parse_v75_data_section(parser: _PltParser, fp: io.BufferedIOBase) -> None:
274
+ """Parse the v75 data section (EOHMARKER already consumed).
275
+
276
+ Iterates zones in declaration order, matching :meth:`_PltParser`'s
277
+ contract for every other version.
278
+ """
279
+ byte_order = parser.byte_order
280
+ num_vars = len(parser.var_names)
281
+
282
+ for zone_idx, meta in enumerate(parser.zones):
283
+ marker = _read_float32(fp, byte_order)
284
+ if abs(marker - _MARKER_ZONE) >= 0.5:
285
+ raise PltReadError(
286
+ f"v75 data section zone {zone_idx}: expected zone marker 299.0, "
287
+ f"got {marker:.2f}."
288
+ )
289
+
290
+ n_repeat = _read_int32(fp, byte_order)
291
+ # See module docstring: numbers are 1-based, source is the previous zone.
292
+ repeat_var_nums = [_read_int32(fp, byte_order) for _ in range(n_repeat)]
293
+ raw_dtypes = [_read_int32(fp, byte_order) for _ in range(num_vars)]
294
+ meta.var_data_types = [_INT_TO_DATATYPE[d] for d in raw_dtypes]
295
+ meta.is_passive = [False] * num_vars
296
+ meta.shared_zone = [-1] * num_vars
297
+ if repeat_var_nums and zone_idx == 0:
298
+ raise PltReadError(
299
+ "v75 data section zone 0: repeated variables declared, but there "
300
+ "is no previous zone to repeat from."
301
+ )
302
+ for var_num in repeat_var_nums:
303
+ if not 1 <= var_num <= num_vars:
304
+ raise PltReadError(
305
+ f"v75 data section zone {zone_idx}: repeated variable number "
306
+ f"{var_num} out of range 1..{num_vars}."
307
+ )
308
+ meta.shared_zone[var_num - 1] = zone_idx - 1
309
+
310
+ active_vars = [v for v in range(num_vars) if meta.shared_zone[v] < 0]
311
+
312
+ if meta.zone_type == ZoneType.ORDERED:
313
+ num_rows = max(meta.i_max, 1) * max(meta.j_max, 1) * max(meta.k_max, 1)
314
+ else:
315
+ num_rows = meta.num_nodes
316
+
317
+ if meta.is_point_format:
318
+ _record_point_format_offsets(fp, meta, active_vars, num_rows, byte_order)
319
+ else:
320
+ _record_block_format_offsets(fp, meta, active_vars, num_rows)
321
+
322
+ if meta.zone_type != ZoneType.ORDERED:
323
+ _record_v75_connectivity(fp, meta, byte_order, zone_idx)
324
+
325
+
326
+ def _record_point_format_offsets(
327
+ fp: io.BufferedIOBase,
328
+ meta: _ZoneMeta,
329
+ active_vars: list[int],
330
+ num_rows: int,
331
+ byte_order: str,
332
+ ) -> None:
333
+ """Record the interleaved row layout for a POINT/FEPOINT zone.
334
+
335
+ Builds the structured dtype covering every active (non-repeated)
336
+ variable, in file order, with byte order baked into each field so reads
337
+ never need a separate byte-swap pass. Does not read the row block itself,
338
+ :class:`~._plt_read.TecplotPltVariableReader` reads it lazily (and caches
339
+ the whole block on first full read, see its ``_get_point_format_values``).
340
+ """
341
+ fields: list[tuple[str, str]] = []
342
+ field_by_var: dict[int, str] = {}
343
+ for var_idx in active_vars:
344
+ dtype_str, itemsize = _DATATYPE_TO_NUMPY[meta.var_data_types[var_idx].value]
345
+ field_name = f"v{var_idx}"
346
+ fields.append((field_name, f"{byte_order}{dtype_str}"))
347
+ field_by_var[var_idx] = field_name
348
+
349
+ row_dtype = np.dtype(fields)
350
+ meta.point_row_dtype = row_dtype
351
+ meta.point_data_offset = fp.tell()
352
+ meta.point_num_rows = num_rows
353
+ meta.point_field_by_var = field_by_var
354
+
355
+ for var_idx in active_vars:
356
+ dtype_str, _ = _DATATYPE_TO_NUMPY[meta.var_data_types[var_idx].value]
357
+ meta.var_offsets[var_idx] = (meta.point_data_offset, num_rows, dtype_str)
358
+
359
+ fp.seek(row_dtype.itemsize * num_rows, os.SEEK_CUR)
360
+
361
+
362
+ def _record_block_format_offsets(
363
+ fp: io.BufferedIOBase,
364
+ meta: _ZoneMeta,
365
+ active_vars: list[int],
366
+ num_rows: int,
367
+ ) -> None:
368
+ """Record contiguous per-variable block offsets for a BLOCK/FEBLOCK zone.
369
+
370
+ Same on-disk layout as every later version, seeks past each variable's
371
+ block without reading it.
372
+ """
373
+ for var_idx in active_vars:
374
+ dtype_str, itemsize = _DATATYPE_TO_NUMPY[meta.var_data_types[var_idx].value]
375
+ meta.var_offsets[var_idx] = (fp.tell(), num_rows, dtype_str)
376
+ fp.seek(num_rows * itemsize, os.SEEK_CUR)
377
+
378
+
379
+ def _record_v75_connectivity(
380
+ fp: io.BufferedIOBase,
381
+ meta: _ZoneMeta,
382
+ byte_order: str,
383
+ zone_idx: int,
384
+ ) -> None:
385
+ """Record the connectivity block (or shared-from-previous-zone marker).
386
+
387
+ The "repeat adjacency list from previous zone" boolean is read
388
+ unconditionally for every FE zone; connectivity data only follows when
389
+ it's 0.
390
+ """
391
+ repeat_flag = _read_int32(fp, byte_order)
392
+ if repeat_flag:
393
+ meta.connectivity_shared_zone = zone_idx - 1
394
+ return
395
+
396
+ nodes_per_cell = _NODES_PER_ELEMENT[meta.zone_type]
397
+ count = meta.num_elements * nodes_per_cell
398
+ meta.connectivity_offset = fp.tell()
399
+ meta.connectivity_count = count
400
+ fp.seek(count * 4, os.SEEK_CUR)