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/_io.py CHANGED
@@ -29,27 +29,32 @@ Note:
29
29
  approach because PLT and SZL are sequential binary formats with no in-place editing
30
30
  capability.
31
31
 
32
+ Note:
33
+ Legacy PLT files (v75, v107, v111) are read-only, so ``"a"`` and ``"a+"`` raise
34
+ :exc:`ValueError` for them.
35
+
32
36
  Note:
33
37
  FEPOLYGON and FEPOLYHEDRON zones are not copied during append operations because the
34
38
  ``Write`` API does not yet expose a poly-zone writer. A :exc:`NotImplementedError`
35
39
  is raised if such zones are encountered in the source file.
36
40
  """
37
41
 
42
+ # ruff: noqa: A001 N806
43
+
38
44
  from __future__ import annotations
39
45
 
40
46
  import os
41
47
  import tempfile
42
48
  from pathlib import Path
43
- from typing import Any, Literal, overload
49
+ from typing import Any, Literal, NamedTuple, overload
44
50
 
45
- import numpy as np
46
51
  import numpy.typing as npt
47
52
 
48
53
  from ._constants import FileType, ValueLocation, ZoneType
49
54
  from ._containers import ZoneList
50
- from ._dat_read import TecplotDatReader
55
+ from ._dat_read import TecplotDatReader, _peek_dat_metadata
51
56
  from ._dat_write import TecplotDatWriter
52
- from ._plt_read import TecplotPltReader
57
+ from ._plt_read import TecplotPltReader, _is_legacy_plt
53
58
  from ._plt_write import TecplotPltWriter
54
59
  from ._reader import (
55
60
  TecplotAuxDataReader,
@@ -88,6 +93,20 @@ _HANDLERS: dict[str, dict[str, Any]] = {
88
93
  "a": None,
89
94
  "a+": None,
90
95
  },
96
+ ".bbin": {
97
+ "r": TecplotPltReader,
98
+ "w": TecplotPltWriter,
99
+ "x": None,
100
+ "a": None,
101
+ "a+": None,
102
+ },
103
+ ".bbin8": {
104
+ "r": TecplotPltReader,
105
+ "w": TecplotPltWriter,
106
+ "x": None,
107
+ "a": None,
108
+ "a+": None,
109
+ },
91
110
  ".dat": {
92
111
  "r": TecplotDatReader,
93
112
  "w": TecplotDatWriter,
@@ -104,6 +123,9 @@ _HANDLERS: dict[str, dict[str, Any]] = {
104
123
  },
105
124
  }
106
125
 
126
+ # Extensions handled by the PLT reader, which may hold a legacy (read-only) version.
127
+ _PLT_EXTS: frozenset[str] = frozenset({".plt", ".bin", ".bbin", ".bbin8"})
128
+
107
129
  # FE zone types that the Write API supports for zone copying.
108
130
  _FE_SIMPLE: frozenset[ZoneType] = frozenset({
109
131
  ZoneType.FELINESEG,
@@ -156,62 +178,43 @@ def _copy_zones(reader: TecplotReader, writer: TecplotWriter) -> None:
156
178
  "copying is not supported by the Write API yet."
157
179
  )
158
180
 
159
- # Collect per-variable metadata and data arrays.
181
+ # Collect per-variable metadata and data arrays
160
182
  data: list[npt.NDArray] = []
161
183
  value_locations: list[ValueLocation] = []
162
- passive_vars: list[bool] = []
163
- var_sharing: list[int] = []
184
+ passive_vars: set[str] = set()
185
+ var_sharing: dict[str, int] = {}
164
186
 
165
187
  for var in zone.variables:
166
- passive_vars.append(var.is_passive())
167
- sv = var.shared_zone # None, or the 1-based source-zone index shared from
168
- # Write API expects 0 = no sharing, positive = 1-based zone source.
169
- var_sharing.append((sv) if sv is not None else 0)
170
- value_locations.append(var.value_location)
171
-
172
- if var.is_passive() or sv is not None:
173
- # No array needed; append a placeholder so indices stay aligned.
174
- data.append(np.array([], dtype=np.float32))
188
+ sv = var.shared_zone # None, or the 0-based source-zone index shared from
189
+ if var.is_passive():
190
+ passive_vars.add(var.name)
191
+ elif sv is not None:
192
+ var_sharing[var.name] = sv
175
193
  else:
176
194
  data.append(var.values) # ty: ignore[invalid-argument-type]
177
-
178
- # Strip placeholder arrays — Write infers active variables from passive_vars /
179
- # var_sharing.
180
- active_data = [
181
- arr
182
- for arr, is_p, sv in zip(data, passive_vars, var_sharing, strict=False)
183
- if not is_p and sv == 0
184
- ]
185
- active_locs = [
186
- loc
187
- for loc, is_p, sv in zip(
188
- value_locations, passive_vars, var_sharing, strict=False
189
- )
190
- if not is_p and sv == 0
191
- ]
192
-
193
- common_kw: dict[str, Any] = dict(
194
- title=zone.title,
195
- value_locations=active_locs,
196
- passive_vars=passive_vars,
197
- var_sharing=var_sharing,
198
- solution_time=zone.solution_time,
199
- strand_id=zone.strand_id,
200
- aux=dict(zone.auxdata.items()) or None,
201
- )
195
+ value_locations.append(var.value_location)
196
+
197
+ common_kw: dict[str, Any] = {
198
+ "title": zone.title,
199
+ "value_locations": value_locations,
200
+ "passive_vars": passive_vars,
201
+ "var_sharing": var_sharing,
202
+ "solution_time": zone.solution_time,
203
+ "strand_id": zone.strand_id,
204
+ "aux": dict(zone.auxdata.items()) or None,
205
+ }
202
206
 
203
207
  if isinstance(zone, TecplotOrderedZoneReader):
204
- writer.write_ordered_zone(data=active_data, **common_kw)
208
+ writer.write_ordered_zone(data=data, **common_kw)
205
209
  elif isinstance(zone, TecplotFEZoneReader):
206
210
  # Forward connectivity sharing: when shared, pass ``con_sharing`` and omit
207
211
  # the node map so the writer derives the node/cell counts from the source
208
212
  # zone; otherwise write this zone's own node map.
209
- cv = zone.shared_connectivity # None, or 1-based source-zone index
210
- con_sharing = cv if cv is not None else 0
213
+ con_sharing = zone.shared_connectivity # None, or 0-based source-zone index
211
214
  writer.write_fe_zone(
212
215
  zone_type=zone.zone_type,
213
- data=active_data,
214
- node_map=None if con_sharing else zone.node_map,
216
+ data=data,
217
+ node_map=None if con_sharing is not None else zone.node_map,
215
218
  con_sharing=con_sharing,
216
219
  **common_kw,
217
220
  )
@@ -301,14 +304,15 @@ class AppendWrite:
301
304
  value_locations (Sequence[ValueLocation] | None): Per-variable
302
305
  :class:`~tecio.libtecio.ValueLocation` for active variables. Defaults
303
306
  to all :attr:`~tecio.libtecio.ValueLocation.NODAL`.
304
- passive_vars (Sequence[bool | int] | None): Per-variable passive flag for
305
- the **full** variable list (length = total variables in the file, not
306
- just active ones). Defaults to all ``False``.
307
- var_sharing (Sequence[int] | None): Per-variable share-from zone index
308
- (1-based, counting all zones including those copied from the original
309
- file). ``0`` = no sharing. Defaults to all ``0``. Sharing grid
310
- coordinates from zone 1 is a common pattern for transient data to avoid
311
- duplicating large arrays.
307
+ passive_vars (list | tuple | set | None): Variables to mark passive: a list,
308
+ tuple, or set of variable names, or of 0-based indices. Defaults to all
309
+ active.
310
+ var_sharing (dict[str | int, int] | None): Variables to share from another
311
+ zone: a ``{variable: source zone}`` mapping (name or 0-based index to
312
+ 0-based zone index, counting all zones including those copied from the
313
+ original file). Defaults to no sharing. Sharing grid coordinates from
314
+ zone 0 is a common pattern for transient data to avoid duplicating large
315
+ arrays.
312
316
  solution_time (float): Solution time for transient data. Defaults to
313
317
  ``0.0``.
314
318
  strand_id (int): Strand ID grouping related time steps. Zones with the same
@@ -317,13 +321,11 @@ class AppendWrite:
317
321
  pairs. Defaults to ``None``.
318
322
 
319
323
  Example:
320
- Append a time step, sharing the grid from zone 1:
324
+ Append a time step, sharing the grid from zone 0:
321
325
 
322
- >>> n = len(tec.variables) # e.g. ["x", "y", "pressure"]
323
326
  >>> tec.write_ordered_zone(
324
327
  ... data=[p_new], # only the non-shared variable
325
- ... passive_vars=[False] * n,
326
- ... var_sharing=[1, 1, 0], # x and y shared from zone 1
328
+ ... var_sharing={"x": 0, "y": 0},
327
329
  ... solution_time=5.0,
328
330
  ... strand_id=1,
329
331
  ... )
@@ -350,19 +352,22 @@ class AppendWrite:
350
352
  must have length ``num_nodes``; CELL_CENTERED arrays must have length
351
353
  ``num_cells``. Both are inferred from ``node_map``.
352
354
  node_map (array-like | None): Integer array of shape ``(num_cells,
353
- nodes_per_cell)`` with **1-based** node indices. ``num_nodes =
354
- node_map.max()``. The 32- or 64-bit write path is chosen automatically.
355
+ nodes_per_cell)`` with 0-based node indices. ``num_nodes =
356
+ node_map.max() + 1``. The 32- or 64-bit write path is chosen
357
+ automatically.
355
358
  title (str | None): Zone title. Defaults to ``"FE_Zone_{n}"``.
356
359
  value_locations (Sequence[ValueLocation] | None): Per-variable value
357
360
  location for active variables. Defaults to all
358
361
  :attr:`~tecio.libtecio.ValueLocation.NODAL`.
359
- passive_vars (Sequence[bool | int] | None): Per-variable passive flags for
360
- the full variable list. Defaults to all ``False``.
361
- var_sharing (Sequence[int] | None): Per-variable share-from zone index
362
- (1-based). ``0`` = no sharing. Defaults to all ``0``.
363
- con_sharing (int): Zone index to share connectivity from (1-based). ``0`` =
364
- write connectivity directly. Cannot be used with GLOBAL face-neighbor
365
- modes. Defaults to ``0``.
362
+ passive_vars (list | tuple | set | None): Variables to mark passive: a list,
363
+ tuple, or set of variable names, or of 0-based indices. Defaults to all
364
+ active.
365
+ var_sharing (dict[str | int, int] | None): Variables to share from another
366
+ zone: a ``{variable: source zone}`` mapping (name or 0-based index to
367
+ 0-based zone index). Defaults to no sharing.
368
+ con_sharing (int | None): Zone index to share connectivity from (0-based).
369
+ ``None`` = write connectivity directly. Cannot be used with GLOBAL
370
+ face-neighbor modes. Defaults to ``None``.
366
371
  face_neighbors (array-like | None): Face-neighbor connectivity. When
367
372
  provided, ``num_face_cons`` is set automatically.
368
373
  face_nbr_mode (FaceNeighborMode | None): Face-neighbor mode. Defaults to
@@ -402,7 +407,7 @@ class AppendWrite:
402
407
 
403
408
  @property
404
409
  def current_zone(self) -> int:
405
- """Index of the most recently written zone (1-based)."""
410
+ """0-based index of the most recently written zone, -1 before any write."""
406
411
  return self._writer.current_zone
407
412
 
408
413
  # -- Close and finalise ------------------------------------------------------------
@@ -506,16 +511,16 @@ class AppendReadWrite(AppendWrite):
506
511
  return self._reader.num_auxdata_items
507
512
 
508
513
  @property
509
- def var_auxdata(self) -> list[TecplotAuxDataReader | None]:
514
+ def var_auxdata(self) -> list[TecplotAuxDataReader]:
510
515
  """Per-variable auxiliary data list from the *original* file."""
511
516
  return self._reader.var_auxdata
512
517
 
513
518
  def get_var_auxdata(self, var_index: int) -> TecplotAuxDataReader:
514
- """Return variable aux data for *var_index* (1-based)."""
519
+ """Return variable aux data for *var_index* (0-based)."""
515
520
  return self._reader.get_var_auxdata(var_index)
516
521
 
517
522
  def get_zone_auxdata(self, zone_index: int) -> TecplotAuxDataReader:
518
- """Return zone aux data for *zone_index* (1-based)."""
523
+ """Return zone aux data for *zone_index* (0-based)."""
519
524
  return self._reader.get_zone_auxdata(zone_index)
520
525
 
521
526
 
@@ -546,6 +551,8 @@ def _open_append(
546
551
 
547
552
  Raises:
548
553
  FileNotFoundError: If *path* does not exist (nothing to append to).
554
+ ValueError: If *path* is a legacy PLT file (v75, v107, or v111), which is
555
+ read-only.
549
556
  """
550
557
  src = Path(path)
551
558
  if not src.exists():
@@ -553,6 +560,11 @@ def _open_append(
553
560
  f"Cannot append to '{src}': file does not exist. "
554
561
  "Use mode='w' to create a new file."
555
562
  )
563
+ if ext in _PLT_EXTS and _is_legacy_plt(src):
564
+ raise ValueError(
565
+ f"Cannot append to '{src}': legacy PLT versions (v75, v107, v111) are "
566
+ "read-only. Open with mode='r', or convert to a current format first."
567
+ )
556
568
 
557
569
  ReadCls = _reader_for_ext(ext)
558
570
  WriteCls = _writer_for_ext(ext)
@@ -690,7 +702,7 @@ def open(
690
702
  path (str | os.PathLike): File path. Extension determines the format handler:
691
703
 
692
704
  - ``.szplt`` → :mod:`tecio.szl`
693
- - ``.plt`` / ``.bin`` → :mod:`tecio.plt`
705
+ - ``.plt`` / ``.bin`` / ``.bbin`` / ``.bbin8`` → :mod:`tecio.plt`
694
706
  - ``.dat`` / ``.tec`` → :mod:`tecio.dat`
695
707
 
696
708
  mode (str): Access mode — mirrors Python's built-in :func:`open`:
@@ -706,6 +718,9 @@ def open(
706
718
  - ``'a+'`` -- Same as ``'a'`` but the returned handle also exposes the full
707
719
  read API populated from the original file.
708
720
 
721
+ Legacy PLT files (v75, v107, v111) can only be opened with ``'r'``;
722
+ ``'a'`` and ``'a+'`` raise :exc:`ValueError` for them.
723
+
709
724
  **kwargs: Forwarded to the underlying handler constructor. The accepted keyword
710
725
  arguments differ by mode — see the tables below.
711
726
 
@@ -755,7 +770,8 @@ def open(
755
770
  - ``'a+'`` → :class:`~tecio.AppendReadWrite`
756
771
 
757
772
  Raises:
758
- ValueError: Unsupported file extension or unrecognised mode.
773
+ ValueError: Unsupported file extension, unrecognised mode, or ``'a'``/``'a+'``
774
+ on a legacy PLT file (v75, v107, v111).
759
775
  FileNotFoundError: ``'r'``, ``'a'``, or ``'a+'`` on a missing file.
760
776
  FileExistsError: ``'x'`` on an existing file.
761
777
  NotImplementedError: Append mode on a file containing FEPOLYGON or FEPOLYHEDRON
@@ -811,3 +827,252 @@ def open(
811
827
  raise ValueError(
812
828
  f"Unrecognised mode '{mode}'. Supported modes: 'r', 'w', 'x', 'a', 'a+'"
813
829
  )
830
+
831
+
832
+ # --------------------------------------------------------------------------------------
833
+ # Lightweight metadata queries
834
+ # --------------------------------------------------------------------------------------
835
+
836
+
837
+ def _validated_ext(path: str | os.PathLike) -> str:
838
+ """Return *path*'s lowercased extension, or raise if unsupported.
839
+
840
+ Example:
841
+ >>> _validated_ext("flow.szplt")
842
+ '.szplt'
843
+ """
844
+ ext = Path(path).suffix.lower()
845
+ if ext not in _HANDLERS:
846
+ raise ValueError(
847
+ f"Unsupported file extension: '{ext}'. Supported: {sorted(_HANDLERS)}"
848
+ )
849
+ return ext
850
+
851
+
852
+ class ZoneSummary(NamedTuple):
853
+ """Per-zone metadata from a lightweight :func:`peek` query.
854
+
855
+ ``num_nodes``/``num_elements`` are ``None`` only for a DAT ordered zone whose header
856
+ omits I/J/K entirely (rare; relies on the reader inferring the point count from the
857
+ data itself, which a lightweight query never does). Every other case, and every
858
+ SZL/PLT zone, always has both.
859
+ """
860
+
861
+ title: str
862
+ zone_type: ZoneType
863
+ num_nodes: int | None
864
+ num_elements: int | None
865
+
866
+
867
+ class FileSummary(NamedTuple):
868
+ """Whole-file metadata from :func:`peek`, with no variable data touched."""
869
+
870
+ title: str
871
+ file_type: FileType
872
+ variable_names: list[str]
873
+ zones: list[ZoneSummary]
874
+
875
+
876
+ def get_variable_list(path: str | os.PathLike) -> list[str]:
877
+ """Return a file's variable names without reading any variable data.
878
+
879
+ For SZL and PLT this is already what a normal :func:`open` does, both formats
880
+ resolve dataset-level metadata (including variable names) up front without touching
881
+ data arrays, so this is a thin convenience wrapper for those two. DAT is different:
882
+ opening it normally always reads and numerically converts every value in the file,
883
+ so this function instead scans just the file's header text, several orders of
884
+ magnitude cheaper for a file with large data blocks.
885
+
886
+ Args:
887
+ path: Path to a ``.szplt``, ``.plt``/``.bin``, or ``.dat``/``.tec`` file.
888
+
889
+ Returns:
890
+ Variable names, in file order.
891
+
892
+ Raises:
893
+ FileNotFoundError: If *path* does not exist.
894
+ ValueError: If the file extension is not recognized.
895
+
896
+ Example:
897
+ >>> get_variable_list("flow.szplt")
898
+ ['x', 'y', 'z', 'pressure']
899
+ """
900
+ ext = _validated_ext(path)
901
+ if ext in (".dat", ".tec"):
902
+ return _peek_dat_metadata(path).variable_names
903
+ with _HANDLERS[ext]["r"](str(path)) as r:
904
+ return list(r.variables)
905
+
906
+
907
+ def get_zone_list(path: str | os.PathLike) -> list[str]:
908
+ """Return a file's zone titles without reading any variable data.
909
+
910
+ Same rationale as :func:`get_variable_list`: already cheap for SZL and PLT
911
+ (constructing a zone reader only resolves scalar metadata, never variable data),
912
+ so this is a thin wrapper for those; for DAT, uses a dedicated scan that skips
913
+ every data and connectivity block entirely.
914
+
915
+ Args:
916
+ path: Path to a ``.szplt``, ``.plt``/``.bin``, or ``.dat``/``.tec`` file.
917
+
918
+ Returns:
919
+ Zone titles, in file order. An unnamed zone contributes an empty string, not
920
+ a placeholder like ``"Zone 1"``.
921
+
922
+ Raises:
923
+ FileNotFoundError: If *path* does not exist.
924
+ ValueError: If the file extension is not recognized.
925
+
926
+ Example:
927
+ >>> get_zone_list("flow.szplt")
928
+ ['FluidVolume', 'WingSurface']
929
+ """
930
+ ext = _validated_ext(path)
931
+ if ext in (".dat", ".tec"):
932
+ return [z.title for z in _peek_dat_metadata(path).zones]
933
+ with _HANDLERS[ext]["r"](str(path)) as r:
934
+ return [zone.title for zone in r.zones]
935
+
936
+
937
+ def get_num_zones(path: str | os.PathLike) -> int:
938
+ """Return a file's zone count without reading any variable data.
939
+
940
+ Cheaper still than :func:`get_zone_list`: for SZL, a single scalar C-library
941
+ call with no zone objects constructed at all; for PLT and DAT, a count against
942
+ already-parsed/scanned header metadata.
943
+
944
+ Args:
945
+ path: Path to a ``.szplt``, ``.plt``/``.bin``, or ``.dat``/``.tec`` file.
946
+
947
+ Returns:
948
+ Number of zones in the file.
949
+
950
+ Raises:
951
+ FileNotFoundError: If *path* does not exist.
952
+ ValueError: If the file extension is not recognized.
953
+
954
+ Example:
955
+ >>> get_num_zones("flow.szplt")
956
+ 2
957
+ """
958
+ ext = _validated_ext(path)
959
+ if ext in (".dat", ".tec"):
960
+ return len(_peek_dat_metadata(path).zones)
961
+ with _HANDLERS[ext]["r"](str(path)) as r:
962
+ return r.num_zones
963
+
964
+
965
+ def get_num_variables(path: str | os.PathLike) -> int:
966
+ """Return a file's variable count without reading any variable data.
967
+
968
+ Cheaper still than :func:`get_variable_list`: for SZL, a single scalar
969
+ C-library call.
970
+
971
+ Args:
972
+ path: Path to a ``.szplt``, ``.plt``/``.bin``, or ``.dat``/``.tec`` file.
973
+
974
+ Returns:
975
+ Number of variables in the dataset.
976
+
977
+ Raises:
978
+ FileNotFoundError: If *path* does not exist.
979
+ ValueError: If the file extension is not recognized.
980
+
981
+ Example:
982
+ >>> get_num_variables("flow.szplt")
983
+ 4
984
+ """
985
+ ext = _validated_ext(path)
986
+ if ext in (".dat", ".tec"):
987
+ return len(_peek_dat_metadata(path).variable_names)
988
+ with _HANDLERS[ext]["r"](str(path)) as r:
989
+ return r.num_vars
990
+
991
+
992
+ def get_title(path: str | os.PathLike) -> str:
993
+ """Return a file's dataset title without reading any variable data.
994
+
995
+ Args:
996
+ path: Path to a ``.szplt``, ``.plt``/``.bin``, or ``.dat``/``.tec`` file.
997
+
998
+ Returns:
999
+ Dataset title string.
1000
+
1001
+ Raises:
1002
+ FileNotFoundError: If *path* does not exist.
1003
+ ValueError: If the file extension is not recognized.
1004
+
1005
+ Example:
1006
+ >>> get_title("flow.szplt")
1007
+ 'Onera M6 wing'
1008
+ """
1009
+ ext = _validated_ext(path)
1010
+ if ext in (".dat", ".tec"):
1011
+ return _peek_dat_metadata(path).title
1012
+ with _HANDLERS[ext]["r"](str(path)) as r:
1013
+ return r.title
1014
+
1015
+
1016
+ def get_file_type(path: str | os.PathLike) -> FileType:
1017
+ """Return a file's :class:`~tecio.FileType` without reading any variable data.
1018
+
1019
+ Args:
1020
+ path: Path to a ``.szplt``, ``.plt``/``.bin``, or ``.dat``/``.tec`` file.
1021
+
1022
+ Returns:
1023
+ The file type (``FULL``, ``GRID``, or ``SOLUTION``).
1024
+
1025
+ Raises:
1026
+ FileNotFoundError: If *path* does not exist.
1027
+ ValueError: If the file extension is not recognized.
1028
+
1029
+ Example:
1030
+ >>> get_file_type("flow.szplt")
1031
+ <FileType.FULL: 0>
1032
+ """
1033
+ ext = _validated_ext(path)
1034
+ if ext in (".dat", ".tec"):
1035
+ return _peek_dat_metadata(path).file_type
1036
+ with _HANDLERS[ext]["r"](str(path)) as r:
1037
+ return r.file_type
1038
+
1039
+
1040
+ def peek(path: str | os.PathLike) -> FileSummary:
1041
+ """Return a file's title, file type, variable names, and per-zone summaries.
1042
+
1043
+ A single combined query covering everything :func:`get_variable_list`,
1044
+ :func:`get_zone_list`, :func:`get_title`, and :func:`get_file_type` return, plus
1045
+ each zone's type and size. For DAT, calling several of those functions
1046
+ separately on the same file means scanning it that many times over, this
1047
+ function scans once no matter how much of the summary is needed.
1048
+
1049
+ Args:
1050
+ path: Path to a ``.szplt``, ``.plt``/``.bin``, or ``.dat``/``.tec`` file.
1051
+
1052
+ Returns:
1053
+ Whole-file and per-zone metadata. See :class:`FileSummary` and
1054
+ :class:`ZoneSummary`.
1055
+
1056
+ Raises:
1057
+ FileNotFoundError: If *path* does not exist.
1058
+ ValueError: If the file extension is not recognized.
1059
+
1060
+ Example:
1061
+ >>> summary = peek("flow.szplt")
1062
+ >>> [z.title for z in summary.zones]
1063
+ ['FluidVolume', 'WingSurface']
1064
+ """
1065
+ ext = _validated_ext(path)
1066
+ if ext in (".dat", ".tec"):
1067
+ meta = _peek_dat_metadata(path)
1068
+ zones = [
1069
+ ZoneSummary(z.title, z.zone_type, z.num_nodes, z.num_elements)
1070
+ for z in meta.zones
1071
+ ]
1072
+ return FileSummary(meta.title, meta.file_type, meta.variable_names, zones)
1073
+ with _HANDLERS[ext]["r"](str(path)) as r:
1074
+ zones = [
1075
+ ZoneSummary(zone.title, zone.zone_type, zone.num_nodes, zone.num_elements)
1076
+ for zone in r.zones
1077
+ ]
1078
+ return FileSummary(r.title, r.file_type, list(r.variables), zones)
tecio/_meta.py CHANGED
@@ -56,7 +56,7 @@ class ZoneMeta:
56
56
  variable-type arrays of a ``TECZNE142`` zone header.
57
57
 
58
58
  Attributes:
59
- index: 1-based zone index returned by the C library.
59
+ index: 0-based index of this zone within the dataset.
60
60
  title: Zone title.
61
61
  zone_type: The zone's :class:`~tecio.libtecio.ZoneType`.
62
62
  solution_time: Solution time (``0.0`` for static zones).
@@ -72,8 +72,8 @@ class ZoneMeta:
72
72
  else ``None`` if it has none.
73
73
  value_locations: Per-variable value location, full dataset length.
74
74
  passive_vars: Per-variable passive flags, full dataset length.
75
- shared_vars: Per-variable share-from zone index (1-based; ``0`` for not
76
- shared), full dataset length.
75
+ shared_vars: Per-variable share-from zone index, or ``None`` for not
76
+ shared, full dataset length.
77
77
  data_types: Per-variable data type, full dataset length.
78
78
 
79
79
  """
@@ -93,7 +93,7 @@ class ZoneMeta:
93
93
  # Per-variable descriptors (length == dataset variable count).
94
94
  value_locations: tuple[ValueLocation, ...] = ()
95
95
  passive_vars: tuple[bool, ...] = ()
96
- shared_vars: tuple[int, ...] = ()
96
+ shared_vars: tuple[int | None, ...] = ()
97
97
  data_types: tuple[DataType, ...] = ()
98
98
 
99
99
  @property
@@ -133,7 +133,7 @@ class WriterMeta:
133
133
  header has been written (lazy-open).
134
134
  num_dataset_aux_items: Count of dataset-level aux items written.
135
135
  num_var_aux_items: Total variable-level aux items written.
136
- zones: Mapping of 1-based zone index to :class:`ZoneMeta`,
136
+ zones: Mapping of 0-based zone index to :class:`ZoneMeta`,
137
137
  in write order.
138
138
  """
139
139
 
@@ -173,7 +173,7 @@ class WriterMeta:
173
173
  self.num_var_aux_items += count
174
174
 
175
175
  def record_zone(self, zone: ZoneMeta) -> None:
176
- """Register a fully written zone by its 1-based index."""
176
+ """Register a fully written zone by its 0-based index."""
177
177
  self.zones[zone.index] = zone
178
178
 
179
179
  # -- Retrieval helpers ------------------------------------------------------------