tecio-python 0.3.1__tar.gz → 0.3.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. {tecio_python-0.3.1/tecio_python.egg-info → tecio_python-0.3.2}/PKG-INFO +8 -6
  2. {tecio_python-0.3.1 → tecio_python-0.3.2}/README.md +7 -5
  3. {tecio_python-0.3.1/tecio → tecio_python-0.3.2}/plugins/paraview/TecplotTecioReader.py +5 -5
  4. {tecio_python-0.3.1 → tecio_python-0.3.2}/pyproject.toml +5 -2
  5. {tecio_python-0.3.1 → tecio_python-0.3.2}/tecio/__init__.py +1 -1
  6. {tecio_python-0.3.1 → tecio_python-0.3.2}/tecio/_containers.py +21 -21
  7. {tecio_python-0.3.1 → tecio_python-0.3.2}/tecio/_dat_read.py +181 -17
  8. {tecio_python-0.3.1 → tecio_python-0.3.2}/tecio/_dat_write.py +149 -270
  9. {tecio_python-0.3.1 → tecio_python-0.3.2}/tecio/_io.py +24 -22
  10. {tecio_python-0.3.1 → tecio_python-0.3.2}/tecio/_meta.py +28 -15
  11. {tecio_python-0.3.1 → tecio_python-0.3.2}/tecio/_plt_read.py +171 -17
  12. {tecio_python-0.3.1 → tecio_python-0.3.2}/tecio/_plt_write.py +79 -282
  13. {tecio_python-0.3.1 → tecio_python-0.3.2}/tecio/_reader.py +230 -11
  14. {tecio_python-0.3.1 → tecio_python-0.3.2}/tecio/_szl_read.py +37 -4
  15. {tecio_python-0.3.1 → tecio_python-0.3.2}/tecio/_szl_write.py +129 -341
  16. {tecio_python-0.3.1 → tecio_python-0.3.2}/tecio/_utils.py +3 -1
  17. tecio_python-0.3.2/tecio/_writer.py +953 -0
  18. {tecio_python-0.3.1 → tecio_python-0.3.2}/tecio/cli/tec2mat.py +20 -4
  19. {tecio_python-0.3.1 → tecio_python-0.3.2}/tecio/cli/tecaux.py +20 -10
  20. {tecio_python-0.3.1 → tecio_python-0.3.2}/tecio/cli/tecdump.py +33 -18
  21. {tecio_python-0.3.1 → tecio_python-0.3.2}/tecio/cli/tecextract.py +131 -35
  22. {tecio_python-0.3.1 → tecio_python-0.3.2}/tecio/cli/tecfix.py +22 -8
  23. {tecio_python-0.3.1 → tecio_python-0.3.2}/tecio/cli/tecmerge.py +84 -43
  24. {tecio_python-0.3.1 → tecio_python-0.3.2}/tecio/cli/teconvert.py +17 -7
  25. {tecio_python-0.3.1 → tecio_python-0.3.2}/tecio/cli/tecscale.py +34 -23
  26. {tecio_python-0.3.1 → tecio_python-0.3.2}/tecio/cli/tecslice.py +27 -10
  27. {tecio_python-0.3.1 → tecio_python-0.3.2}/tecio/cli/tecstats.py +116 -50
  28. {tecio_python-0.3.1 → tecio_python-0.3.2}/tecio/libtecio.py +712 -137
  29. {tecio_python-0.3.1 → tecio_python-0.3.2/tecio_python.egg-info}/PKG-INFO +8 -6
  30. {tecio_python-0.3.1 → tecio_python-0.3.2}/tecio_python.egg-info/SOURCES.txt +1 -2
  31. {tecio_python-0.3.1 → tecio_python-0.3.2}/tests/test_cli.py +130 -130
  32. {tecio_python-0.3.1 → tecio_python-0.3.2}/tests/test_io.py +34 -34
  33. {tecio_python-0.3.1 → tecio_python-0.3.2}/tests/test_read.py +99 -41
  34. {tecio_python-0.3.1 → tecio_python-0.3.2}/tests/test_write.py +371 -117
  35. tecio_python-0.3.1/tecio/_writer.py +0 -331
  36. tecio_python-0.3.1/tecio/old_libtecio.py +0 -3689
  37. {tecio_python-0.3.1 → tecio_python-0.3.2}/LICENSE +0 -0
  38. {tecio_python-0.3.1 → tecio_python-0.3.2}/NOTICE +0 -0
  39. {tecio_python-0.3.1 → tecio_python-0.3.2}/setup.cfg +0 -0
  40. {tecio_python-0.3.1 → tecio_python-0.3.2}/tecio/_constants.py +0 -0
  41. {tecio_python-0.3.1 → tecio_python-0.3.2}/tecio/cli/__init__.py +0 -0
  42. {tecio_python-0.3.1 → tecio_python-0.3.2}/tecio_python.egg-info/dependency_links.txt +0 -0
  43. {tecio_python-0.3.1 → tecio_python-0.3.2}/tecio_python.egg-info/entry_points.txt +0 -0
  44. {tecio_python-0.3.1 → tecio_python-0.3.2}/tecio_python.egg-info/requires.txt +0 -0
  45. {tecio_python-0.3.1 → tecio_python-0.3.2}/tecio_python.egg-info/top_level.txt +0 -0
  46. {tecio_python-0.3.1 → tecio_python-0.3.2}/tests/test_libtecio.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tecio-python
3
- Version: 0.3.1
3
+ Version: 0.3.2
4
4
  Summary: Python interface for reading and writing Tecplot data files
5
5
  Project-URL: Homepage, https://github.com/meersman/tecio
6
6
  Project-URL: Documentation, https://meersman.github.io/tecio/
@@ -82,6 +82,8 @@ export TECIO_LIB=/path/to/libtecio.dylib # macOS
82
82
 
83
83
  ## Quick start
84
84
 
85
+ Writing a file:
86
+
85
87
  ```python
86
88
  import tecio
87
89
  import numpy as np
@@ -90,7 +92,7 @@ x = np.linspace(0, 2 * np.pi, 100)
90
92
  y = np.sin(x)
91
93
 
92
94
  with tecio.open("sine.szplt", "w") as tec:
93
- tec.write_ijk_zone(data=[x, y], variables=["x", "y"])
95
+ tec.write_ordered_zone(data=[x, y], variables=["x", "y"])
94
96
  ```
95
97
 
96
98
  Reading a file:
@@ -98,10 +100,10 @@ Reading a file:
98
100
  ```python
99
101
  with tecio.open("sine.szplt", "r") as tec:
100
102
  print(tec.variables) # ['x', 'y']
101
- x = tec.zone[0].variable[0].values
102
- y = tec.zone[0].variable[1].values
103
+ x = tec.zones[0].variables[0].values
104
+ y = tec.zones[0].variables[1].values
103
105
  # or
104
- x, y = tec.zone[0].get_array(["x", "y"])
106
+ x, y = tec.zones[0].get_array(["x", "y"])
105
107
  ```
106
108
 
107
109
  ## API
@@ -129,7 +131,7 @@ Supported formats:
129
131
 
130
132
  ## Contributing
131
133
 
132
- Contributions are welcome see [CONTRIBUTING.md](CONTRIBUTING.md) for details.
134
+ Contributions are welcome (see [CONTRIBUTING.md](CONTRIBUTING.md) for details).
133
135
 
134
136
  ---
135
137
 
@@ -38,6 +38,8 @@ export TECIO_LIB=/path/to/libtecio.dylib # macOS
38
38
 
39
39
  ## Quick start
40
40
 
41
+ Writing a file:
42
+
41
43
  ```python
42
44
  import tecio
43
45
  import numpy as np
@@ -46,7 +48,7 @@ x = np.linspace(0, 2 * np.pi, 100)
46
48
  y = np.sin(x)
47
49
 
48
50
  with tecio.open("sine.szplt", "w") as tec:
49
- tec.write_ijk_zone(data=[x, y], variables=["x", "y"])
51
+ tec.write_ordered_zone(data=[x, y], variables=["x", "y"])
50
52
  ```
51
53
 
52
54
  Reading a file:
@@ -54,10 +56,10 @@ Reading a file:
54
56
  ```python
55
57
  with tecio.open("sine.szplt", "r") as tec:
56
58
  print(tec.variables) # ['x', 'y']
57
- x = tec.zone[0].variable[0].values
58
- y = tec.zone[0].variable[1].values
59
+ x = tec.zones[0].variables[0].values
60
+ y = tec.zones[0].variables[1].values
59
61
  # or
60
- x, y = tec.zone[0].get_array(["x", "y"])
62
+ x, y = tec.zones[0].get_array(["x", "y"])
61
63
  ```
62
64
 
63
65
  ## API
@@ -85,7 +87,7 @@ Supported formats:
85
87
 
86
88
  ## Contributing
87
89
 
88
- Contributions are welcome see [CONTRIBUTING.md](CONTRIBUTING.md) for details.
90
+ Contributions are welcome (see [CONTRIBUTING.md](CONTRIBUTING.md) for details).
89
91
 
90
92
  ---
91
93
 
@@ -449,7 +449,7 @@ class TecplotReader(VTKPythonAlgorithmBase):
449
449
 
450
450
  self._zone_selection.RemoveAllArrays()
451
451
  self._zone_keys.clear()
452
- for zone in reader.zone:
452
+ for zone in reader.zones:
453
453
  title = zone.title or f"Zone {zone.zone_index}"
454
454
  key = f"{zone.zone_index}: {title}"
455
455
  self._zone_keys[zone.zone_index] = key
@@ -677,7 +677,7 @@ class TecplotReader(VTKPythonAlgorithmBase):
677
677
  static/always-present geometry, so they're excluded from the time axis and
678
678
  instead included at every requested time (see :meth:`RequestData`).
679
679
  """
680
- times = {zone.solution_time for zone in reader.zone if zone.strand_id != 0}
680
+ times = {zone.solution_time for zone in reader.zones if zone.strand_id != 0}
681
681
  return sorted(times)
682
682
 
683
683
  @smproperty.doublevector(
@@ -760,7 +760,7 @@ class TecplotReader(VTKPythonAlgorithmBase):
760
760
  self._add_variable_aux_field_data(output.GetFieldData(), reader)
761
761
 
762
762
  block_index = 0
763
- for zone in reader.zone:
763
+ for zone in reader.zones:
764
764
  key = self._zone_keys.get(zone.zone_index)
765
765
  if key is not None and not self._zone_selection.ArrayIsEnabled(key):
766
766
  continue
@@ -990,7 +990,7 @@ class TecplotReader(VTKPythonAlgorithmBase):
990
990
  n_points = dataset.GetNumberOfPoints()
991
991
  n_cells = dataset.GetNumberOfCells()
992
992
 
993
- for var in zone.variable:
993
+ for var in zone.variables:
994
994
  if not self._array_selection.ArrayIsEnabled(var.name):
995
995
  continue
996
996
  values = var.values
@@ -1039,7 +1039,7 @@ class TecplotReader(VTKPythonAlgorithmBase):
1039
1039
  if name is None:
1040
1040
  flat_components.append(None)
1041
1041
  continue
1042
- var = zone.variable[name]
1042
+ var = zone.variables[name]
1043
1043
  values = var.values
1044
1044
  if values is None: # passive, or absent in this specific zone
1045
1045
  flat_components.append(None)
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "tecio-python"
7
- version = "0.3.1"
7
+ version = "0.3.2"
8
8
  description = "Python interface for reading and writing Tecplot data files"
9
9
 
10
10
  readme = "README.md"
@@ -69,6 +69,9 @@ mat = ["scipy"]
69
69
  include = ["tecio*"]
70
70
  exclude = ["archive*", "test*"]
71
71
 
72
+ [tool.setuptools.data-files]
73
+ "share/tecio/plugins/paraview" = ["plugins/paraview/TecplotTecioReader.py"]
74
+
72
75
  [tool.ruff]
73
76
  line-length = 88
74
77
  target-version = "py310"
@@ -109,7 +112,7 @@ python-version = "3.11"
109
112
  output-format = "concise"
110
113
 
111
114
  [[tool.ty.overrides]]
112
- include = ["tecio/plugins/**"]
115
+ include = ["plugins/**"]
113
116
 
114
117
  [tool.ty.overrides.rules]
115
118
  unresolved-import = "ignore"
@@ -9,7 +9,7 @@ from importlib import metadata
9
9
  try:
10
10
  __version__ = metadata.version("tecio")
11
11
  except metadata.PackageNotFoundError:
12
- __version__ = "0.3.1"
12
+ __version__ = "0.3.2"
13
13
 
14
14
  from . import cli, libtecio
15
15
  from ._constants import (
@@ -1,35 +1,35 @@
1
1
  """Index- and name-based container types for Tecplot data collections.
2
2
 
3
- These containers are shared by the ``tecio`` readers: ``Read.zone`` returns a
4
- :class:`ZoneList` of ``ReadZone`` and ``ReadZone.variable`` returns a
3
+ These containers are shared by the ``tecio`` readers: ``Read.zones`` returns a
4
+ :class:`ZoneList` of ``ReadZone`` and ``ReadZone.variables`` returns a
5
5
  :class:`VariableList` of ``ReadVariable`` for every supported format (SZL, PLT, DAT).
6
- They depend only on small structural protocols (``.name`` for variables, ``.variable``
6
+ They depend only on small structural protocols (``.name`` for variables, ``.variables``
7
7
  for zones) so they import nothing from either hierarchy and cannot introduce a circular
8
8
  dependency.
9
9
 
10
10
  Access model:
11
11
 
12
- reader.zone # ZoneList
13
- reader.zone[0] # ReadZone (element)
14
- reader.zone[1:4] # ZoneList (sub-collection, same kind)
15
- reader.zone[0].variable # VariableList
16
- reader.zone[0].variable["x"] # ReadVariable (object: .values, .is_passive)
17
- reader.zone[0].variable[2] # ReadVariable (0-based index)
12
+ reader.zones # ZoneList
13
+ reader.zones[0] # ReadZone (element)
14
+ reader.zones[1:4] # ZoneList (sub-collection, same kind)
15
+ reader.zones[0].variables # VariableList
16
+ reader.zones[0].variables["x"] # ReadVariable (object: .values, .is_passive)
17
+ reader.zones[0].variables[2] # ReadVariable (0-based index)
18
18
 
19
19
  Subscripting always returns an element or a sub-collection *of the same kind* (never a
20
20
  raw array). The underlying NumPy data is pulled with ``get_array`` on a single zone,
21
21
  which mirrors the pandas ``df[...]`` split: a scalar key returns one array, a list of
22
22
  names returns a tuple of arrays (for unpacking)::
23
23
 
24
- p = reader.zone[0].get_array("p") # ndarray | None
25
- p = reader.zone[0].get_array(2) # ndarray | None (0-based index)
26
- x, y, z = reader.zone[0].get_array(["x", "y", "z"]) # tuple, one per name
24
+ p = reader.zones[0].get_array("p") # ndarray | None
25
+ p = reader.zones[0].get_array(2) # ndarray | None (0-based index)
26
+ x, y, z = reader.zones[0].get_array(["x", "y", "z"]) # tuple, one per name
27
27
 
28
28
  There is deliberately **no** cross-zone array accessor. To pull one variable across
29
29
  many zones (e.g. a transient sequence), iterate explicitly so the outer axis is owned by
30
30
  your code, and stack only when you know the result is rectangular::
31
31
 
32
- seq = [z.get_array("p") for z in reader.zone] # list[ndarray | None]
32
+ seq = [z.get_array("p") for z in reader.zones] # list[ndarray | None]
33
33
  stack = np.stack(seq) # only if shapes all match
34
34
 
35
35
  Name lookup is exact and case-sensitive throughout, so distinct variables such
@@ -78,7 +78,7 @@ class _HasVariableList(Protocol):
78
78
  """A zone element exposing a name/index-addressable variable container."""
79
79
 
80
80
  @property
81
- def variable(self) -> VariableList[Any]: ...
81
+ def variables(self) -> VariableList[Any]: ...
82
82
 
83
83
 
84
84
  _VarT = TypeVar("_VarT", bound=_HasName)
@@ -137,7 +137,7 @@ def select_variable_arrays(
137
137
  class VariableList(Generic[_VarT]):
138
138
  """Read-only sequence of variables with positional *and* named access.
139
139
 
140
- Drop-in for the ``list`` previously returned by ``ReadZone.variable``:
140
+ Drop-in for the ``list`` previously returned by ``ReadZone.variables``:
141
141
  iteration, ``len()``, and integer indexing are unchanged. A string key
142
142
  resolves a variable by its exact, case-sensitive name.
143
143
 
@@ -208,7 +208,7 @@ class VariableList(Generic[_VarT]):
208
208
  """Return the variable names in dataset order."""
209
209
  return [var.name for var in self._items]
210
210
 
211
- def __repr__(self) -> str:
211
+ def __repr__(self) -> str: # pragma: no cover
212
212
  n = len(self._items)
213
213
  if n == 0:
214
214
  return "VariableList([])"
@@ -229,16 +229,16 @@ class VariableList(Generic[_VarT]):
229
229
  class ZoneList(Generic[_ZoneT]):
230
230
  """Read-only sequence of zones: positional access and slicing only.
231
231
 
232
- Drop-in for the ``list`` previously returned by ``Read.zone``: iteration, ``len()``,
233
- and integer indexing are unchanged. Slicing returns another :class:`ZoneList` (not a
234
- plain ``list``) so navigation composes.
232
+ Drop-in for the ``list`` previously returned by ``Read.zones``: iteration,
233
+ ``len()``, and integer indexing are unchanged. Slicing returns another
234
+ :class:`ZoneList` (not a plain ``list``) so navigation composes.
235
235
 
236
236
  This container deliberately exposes **no** data-extraction method. Pulling one
237
237
  variable across many zones is an explicit loop over the zones, keeping the outer
238
238
  (zone) axis owned by the caller (see the module docs).
239
239
 
240
240
  Args:
241
- zones: Ordered list of zone elements (each exposing ``.variable``).
241
+ zones: Ordered list of zone elements (each exposing ``.variables``).
242
242
  """
243
243
 
244
244
  __slots__ = ("_items",)
@@ -277,7 +277,7 @@ class ZoneList(Generic[_ZoneT]):
277
277
  return ZoneList(self._items[key])
278
278
  return self._items[key]
279
279
 
280
- def __repr__(self) -> str:
280
+ def __repr__(self) -> str: # pragma: no cover
281
281
  n = len(self._items)
282
282
  if n == 0:
283
283
  return "ZoneList([])"
@@ -23,6 +23,7 @@ import numpy.typing as npt
23
23
  from ._constants import (
24
24
  DataPacking,
25
25
  DataType,
26
+ FaceNeighborMode,
26
27
  FileType,
27
28
  ValueLocation,
28
29
  ZoneType,
@@ -90,6 +91,14 @@ _STR_TO_DATAPACKING: dict[str, DataPacking] = {
90
91
  "block": DataPacking.BLOCK,
91
92
  }
92
93
 
94
+ # ASCII keyword to FaceNeighborMode
95
+ _STR_TO_FACENEIGHBORMODE: dict[str, FaceNeighborMode] = {
96
+ "localonetoone": FaceNeighborMode.LOCAL_ONE_TO_ONE,
97
+ "localonetomany": FaceNeighborMode.LOCAL_ONE_TO_MANY,
98
+ "globalonetoone": FaceNeighborMode.GLOBAL_ONE_TO_ONE,
99
+ "globalonetomany": FaceNeighborMode.GLOBAL_ONE_TO_MANY,
100
+ }
101
+
93
102
  # Legacy F= (format) keyword to (is_fe, DataPacking)
94
103
  _STR_TO_LEGACY_FORMAT: dict[str, tuple[bool, DataPacking]] = {
95
104
  "POINT": (False, DataPacking.POINT),
@@ -933,10 +942,22 @@ class TecplotDatFEZoneReader(TecplotFEZoneReader):
933
942
  ASCII reader.
934
943
  """
935
944
 
936
- __slots__ = ("_variables", "_node_map", "_aux_raw")
945
+ __slots__ = (
946
+ "_variables",
947
+ "_node_map",
948
+ "_aux_raw",
949
+ "_face_neighbor_mode",
950
+ "_num_face_connections",
951
+ "_face_connections",
952
+ "_face_neighbors_complete",
953
+ )
937
954
  _variables: list[TecplotVariableReader]
938
955
  _node_map: npt.NDArray[Any] | None
939
956
  _aux_raw: dict[str, str]
957
+ _face_neighbor_mode: FaceNeighborMode | None
958
+ _num_face_connections: int | None
959
+ _face_connections: npt.NDArray[np.int64] | None
960
+ _face_neighbors_complete: bool | None
940
961
 
941
962
  def __init__(
942
963
  self,
@@ -952,6 +973,10 @@ class TecplotDatFEZoneReader(TecplotFEZoneReader):
952
973
  node_map: npt.NDArray[Any] | None = None,
953
974
  datapacking: DataPacking = DataPacking.BLOCK,
954
975
  shared_connectivity: int | None = None,
976
+ face_neighbor_mode: FaceNeighborMode | None = None,
977
+ num_face_connections: int | None = None,
978
+ face_connections: npt.NDArray[np.int64] | None = None,
979
+ face_neighbors_complete: bool | None = None,
955
980
  ) -> None:
956
981
  super().__init__(
957
982
  zone_index=zone_index,
@@ -967,6 +992,10 @@ class TecplotDatFEZoneReader(TecplotFEZoneReader):
967
992
  object.__setattr__(self, "_variables", variables)
968
993
  object.__setattr__(self, "_node_map", node_map)
969
994
  object.__setattr__(self, "_aux_raw", auxdata)
995
+ object.__setattr__(self, "_face_neighbor_mode", face_neighbor_mode)
996
+ object.__setattr__(self, "_num_face_connections", num_face_connections)
997
+ object.__setattr__(self, "_face_connections", face_connections)
998
+ object.__setattr__(self, "_face_neighbors_complete", face_neighbors_complete)
970
999
 
971
1000
  def _load_variables(self) -> list[TecplotVariableReader]:
972
1001
  return self._variables
@@ -974,6 +1003,21 @@ class TecplotDatFEZoneReader(TecplotFEZoneReader):
974
1003
  def _load_node_map(self) -> npt.NDArray[Any] | None:
975
1004
  return self._node_map
976
1005
 
1006
+ def _load_face_neighbor_meta(
1007
+ self,
1008
+ ) -> tuple[FaceNeighborMode, int, bool | None] | None:
1009
+ if self._face_neighbor_mode is None or self._num_face_connections is None:
1010
+ return None
1011
+ return (
1012
+ self._face_neighbor_mode,
1013
+ self._num_face_connections,
1014
+ self._face_neighbors_complete,
1015
+ )
1016
+
1017
+ def _load_face_connections(self) -> npt.NDArray[np.int64]:
1018
+ assert self._face_connections is not None # narrowed: meta already confirmed
1019
+ return self._face_connections
1020
+
977
1021
  def _load_auxdata(self) -> TecplotAuxDataReader:
978
1022
  return TecplotDatAuxDataReader(self._aux_raw)
979
1023
 
@@ -1058,7 +1102,7 @@ class TecplotDatReader(TecplotReader):
1058
1102
  return len(self._zones)
1059
1103
 
1060
1104
  @property
1061
- def zone(self) -> ZoneList[TecplotZoneReader]:
1105
+ def zones(self) -> ZoneList[TecplotZoneReader]:
1062
1106
  """Zones in this file, by index or slice."""
1063
1107
  if self._zone_list is None:
1064
1108
  self._zone_list = ZoneList(self._zones)
@@ -1184,6 +1228,53 @@ class TecplotDatReader(TecplotReader):
1184
1228
  # _parse() applies these once _var_auxdata is allocated.
1185
1229
  self._deferred_var_aux_lines.append(line)
1186
1230
 
1231
+ @staticmethod
1232
+ def _infer_ordered_point_count(tokens: _LineBuffer, active_count: int) -> int:
1233
+ """Infer an ordered zone's point count when I/J/K are all omitted.
1234
+
1235
+ Peeks ahead counting numeric tokens up to the next zone/keyword boundary or end
1236
+ of file, using the same boundary rule :meth:`_parse_zone` already uses to know
1237
+ where a header ends and data begins, then restores the original position so the
1238
+ real read (later, driven by the now-known count) starts from the same place.
1239
+
1240
+ Args:
1241
+ tokens: Positioned at the first data line.
1242
+ active_count: Number of active (non-passive, non-shared) variables in
1243
+ dataset order; each data row has exactly this many values.
1244
+
1245
+ Returns:
1246
+ The inferred point count.
1247
+
1248
+ Raises:
1249
+ ValueError: If the total token count isn't evenly divisible by
1250
+ *active_count*, a malformed or truncated data block.
1251
+
1252
+ Example:
1253
+ >>> n = TecplotDatReader._infer_ordered_point_count(tokens, 3)
1254
+ """
1255
+ marker = tokens.position()
1256
+ total = 0
1257
+ while tokens.has_more():
1258
+ line = tokens.peek_stripped()
1259
+ if not line:
1260
+ tokens.next_stripped()
1261
+ continue
1262
+ upper = line.lstrip().upper()
1263
+ if upper.split("=")[0].split()[0] == "ZONE":
1264
+ break
1265
+ if upper.startswith(("DATASETAUXDATA", "VARAUXDATA")):
1266
+ break
1267
+ total += len(tokens.next_stripped().split())
1268
+ tokens.seek(marker)
1269
+
1270
+ if active_count <= 0 or total % active_count != 0:
1271
+ raise ValueError(
1272
+ "Could not infer point count for an ordered zone with no "
1273
+ f"I/J/K: found {total} values, not evenly divisible by "
1274
+ f"{active_count} active variables."
1275
+ )
1276
+ return total // active_count
1277
+
1187
1278
  def _parse_zone(self, tokens: _LineBuffer) -> None:
1188
1279
  """Parse one ZONE block (header + data blocks + connectivity).
1189
1280
 
@@ -1268,11 +1359,29 @@ class TecplotDatReader(TecplotReader):
1268
1359
  "ASCII reader."
1269
1360
  )
1270
1361
 
1362
+ # Passive/shared variables are parsed here (ahead of var_locs/var_types below,
1363
+ # which don't need this ordering) because the point-count inference just below
1364
+ # needs active_count first.
1365
+ passive_set: set[int] = set()
1366
+ if "PASSIVEVARLIST" in kv:
1367
+ passive_set = set(_parse_index_list(kv["PASSIVEVARLIST"]))
1368
+
1369
+ share_map: dict[int, int] = {}
1370
+ if "VARSHARELIST" in kv:
1371
+ share_map = _parse_varsharelist(kv["VARSHARELIST"])
1372
+
1271
1373
  if zone_type == ZoneType.ORDERED:
1272
- I = int(kv.get("I", "1") or "1") # noqa E741
1273
- J = int(kv.get("J", "1") or "1")
1274
- K = int(kv.get("K", "1") or "1")
1275
- num_nodes = I * J * K
1374
+ if "I" not in kv and "J" not in kv and "K" not in kv:
1375
+ # Infer the point count from the data that follows
1376
+ active_count = self.num_vars - len(passive_set) - len(share_map)
1377
+ num_nodes = self._infer_ordered_point_count(tokens, active_count)
1378
+ I = num_nodes # noqa E741
1379
+ J = K = 1
1380
+ else:
1381
+ I = int(kv.get("I", "1") or "1") # noqa E741
1382
+ J = int(kv.get("J", "1") or "1")
1383
+ K = int(kv.get("K", "1") or "1")
1384
+ num_nodes = I * J * K
1276
1385
  num_cells = max(I - 1, 1) * max(J - 1, 1) * max(K - 1, 1)
1277
1386
  else:
1278
1387
  # FE zones accept both the modern (NODES/ELEMENTS) and legacy (N/E)
@@ -1295,6 +1404,26 @@ class TecplotDatReader(TecplotReader):
1295
1404
  else:
1296
1405
  packing = DataPacking.BLOCK
1297
1406
 
1407
+ # Face-neighbor connections (finite-element zones only; ordered zones
1408
+ # don't need them, adjacency is already implicit in the I/J/K
1409
+ # structure). FACENEIGHBORCONNECTIONS is authoritative for how many
1410
+ # values to expect; FEFACENEIGHBORSCOMPLETE says whether that's the
1411
+ # entire adjacency picture or a supplement to Tecplot's own
1412
+ # auto-detection, read below for face_neighbors_complete.
1413
+ declared_face_connections = 0
1414
+ face_neighbor_mode: FaceNeighborMode | None = None
1415
+ face_neighbors_complete: bool | None = None
1416
+ if zone_type != ZoneType.ORDERED and "FACENEIGHBORCONNECTIONS" in kv:
1417
+ declared_face_connections = int(kv["FACENEIGHBORCONNECTIONS"] or "0")
1418
+ if declared_face_connections > 0:
1419
+ mode_raw = kv.get("FACENEIGHBORMODE", "LOCALONETOONE").strip().lower()
1420
+ face_neighbor_mode = _STR_TO_FACENEIGHBORMODE.get(
1421
+ mode_raw, FaceNeighborMode.LOCAL_ONE_TO_ONE
1422
+ )
1423
+ if "FEFACENEIGHBORSCOMPLETE" in kv:
1424
+ complete_raw = kv["FEFACENEIGHBORSCOMPLETE"].strip().upper()
1425
+ face_neighbors_complete = complete_raw == "YES"
1426
+
1298
1427
  # Variable locations (0-based index → ValueLocation)
1299
1428
  var_locs: dict[int, ValueLocation] = {}
1300
1429
  if "VARLOCATION" in kv:
@@ -1312,16 +1441,6 @@ class TecplotDatReader(TecplotReader):
1312
1441
  else:
1313
1442
  var_types = [DataType.FLOAT] * len(self._variable_names)
1314
1443
 
1315
- # Passive variables (0-based)
1316
- passive_set: set[int] = set()
1317
- if "PASSIVEVARLIST" in kv:
1318
- passive_set = set(_parse_index_list(kv["PASSIVEVARLIST"]))
1319
-
1320
- # Shared variables {0-based → 1-based source zone}
1321
- share_map: dict[int, int] = {}
1322
- if "VARSHARELIST" in kv:
1323
- share_map = _parse_varsharelist(kv["VARSHARELIST"])
1324
-
1325
1444
  # Connectivity sharing
1326
1445
  con_share_zone = int(kv.get("CONNECTIVITYSHAREZONE", "0") or "0")
1327
1446
 
@@ -1374,6 +1493,39 @@ class TecplotDatReader(TecplotReader):
1374
1493
  flat = self._read_int_block(tokens, num_cells * nodes_per_cell)
1375
1494
  node_map = flat.reshape(num_cells, nodes_per_cell)
1376
1495
 
1496
+ # -- Read face-neighbor connections (FE zones only, if declared) ---------------
1497
+ #
1498
+ # Immediately follows connectivity in the file. Values are already
1499
+ # 1-based in ASCII (unlike PLT's binary layout, no +1 needed here).
1500
+
1501
+ face_connections: npt.NDArray[np.int64] | None = None
1502
+ one_to_one = (
1503
+ FaceNeighborMode.LOCAL_ONE_TO_ONE,
1504
+ FaceNeighborMode.GLOBAL_ONE_TO_ONE,
1505
+ )
1506
+ if face_neighbor_mode in one_to_one:
1507
+ values_per = (
1508
+ 3 if face_neighbor_mode is FaceNeighborMode.LOCAL_ONE_TO_ONE else 4
1509
+ )
1510
+ face_connections = self._read_int_block(
1511
+ tokens, declared_face_connections * values_per
1512
+ )
1513
+ elif face_neighbor_mode is not None:
1514
+ # "Many" modes: ragged, each record's own 4th value (nz) gives the
1515
+ # count of additional neighbor references that follow it.
1516
+ is_global = face_neighbor_mode is FaceNeighborMode.GLOBAL_ONE_TO_MANY
1517
+ pieces: list[npt.NDArray] = []
1518
+ for _ in range(declared_face_connections):
1519
+ header = self._read_int_block(tokens, 4)
1520
+ nz = int(header[3])
1521
+ rest_count = 2 * nz if is_global else nz
1522
+ rest = self._read_int_block(tokens, rest_count)
1523
+ pieces.append(header)
1524
+ pieces.append(rest)
1525
+ face_connections = (
1526
+ np.concatenate(pieces) if pieces else np.empty(0, dtype=np.int64)
1527
+ )
1528
+
1377
1529
  # -- Build ReadVariable and ReadZone objects -----------------------------------
1378
1530
 
1379
1531
  # For ordered zones, reshape each variable array from flat 1-D to (I, J, K) for
@@ -1398,7 +1550,7 @@ class TecplotDatReader(TecplotReader):
1398
1550
  for var_idx, src_zone_1based in share_map.items():
1399
1551
  if 1 <= src_zone_1based <= len(self._zones):
1400
1552
  var_arrays[var_idx] = (
1401
- self._zones[src_zone_1based - 1].variable[var_idx].values
1553
+ self._zones[src_zone_1based - 1].variables[var_idx].values
1402
1554
  )
1403
1555
 
1404
1556
  # This zone's own 1-based index
@@ -1435,6 +1587,10 @@ class TecplotDatReader(TecplotReader):
1435
1587
  node_map=node_map,
1436
1588
  datapacking=packing,
1437
1589
  shared_connectivity=con_share_zone if con_share_zone else None,
1590
+ face_neighbor_mode=face_neighbor_mode,
1591
+ num_face_connections=declared_face_connections or None,
1592
+ face_connections=face_connections,
1593
+ face_neighbors_complete=face_neighbors_complete,
1438
1594
  )
1439
1595
  )
1440
1596
 
@@ -1453,6 +1609,10 @@ class TecplotDatReader(TecplotReader):
1453
1609
  node_map: npt.NDArray[Any] | None,
1454
1610
  datapacking: DataPacking,
1455
1611
  shared_connectivity: int | None,
1612
+ face_neighbor_mode: FaceNeighborMode | None = None,
1613
+ num_face_connections: int | None = None,
1614
+ face_connections: npt.NDArray[np.int64] | None = None,
1615
+ face_neighbors_complete: bool | None = None,
1456
1616
  ) -> TecplotZoneReader:
1457
1617
  """Construct the right concrete zone reader for one parsed zone."""
1458
1618
  if zone_type == ZoneType.ORDERED:
@@ -1481,6 +1641,10 @@ class TecplotDatReader(TecplotReader):
1481
1641
  node_map=node_map,
1482
1642
  datapacking=datapacking,
1483
1643
  shared_connectivity=shared_connectivity,
1644
+ face_neighbor_mode=face_neighbor_mode,
1645
+ num_face_connections=num_face_connections,
1646
+ face_connections=face_connections,
1647
+ face_neighbors_complete=face_neighbors_complete,
1484
1648
  )
1485
1649
 
1486
1650
  # -- Block readers -----------------------------------------------------------------