tecio-python 0.1.1__tar.gz → 0.2.0__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 (49) hide show
  1. {tecio_python-0.1.1/tecio_python.egg-info → tecio_python-0.2.0}/PKG-INFO +2 -2
  2. {tecio_python-0.1.1 → tecio_python-0.2.0}/README.md +1 -1
  3. {tecio_python-0.1.1 → tecio_python-0.2.0}/pyproject.toml +1 -1
  4. {tecio_python-0.1.1 → tecio_python-0.2.0}/tecio/__init__.py +1 -2
  5. tecio_python-0.2.0/tecio/_meta.py +168 -0
  6. {tecio_python-0.1.1 → tecio_python-0.2.0}/tecio/cli/tec2mat.py +46 -20
  7. {tecio_python-0.1.1 → tecio_python-0.2.0}/tecio/cli/tecdump.py +50 -43
  8. {tecio_python-0.1.1 → tecio_python-0.2.0}/tecio/cli/tecextract.py +51 -18
  9. {tecio_python-0.1.1 → tecio_python-0.2.0}/tecio/cli/tecfix.py +25 -19
  10. {tecio_python-0.1.1 → tecio_python-0.2.0}/tecio/cli/tecmerge.py +41 -19
  11. {tecio_python-0.1.1 → tecio_python-0.2.0}/tecio/cli/teconvert.py +23 -16
  12. {tecio_python-0.1.1 → tecio_python-0.2.0}/tecio/cli/tecscale.py +11 -8
  13. {tecio_python-0.1.1 → tecio_python-0.2.0}/tecio/cli/tecslice.py +15 -9
  14. {tecio_python-0.1.1 → tecio_python-0.2.0}/tecio/cli/tecstats.py +14 -14
  15. {tecio_python-0.1.1 → tecio_python-0.2.0}/tecio/dat/_read.py +141 -25
  16. {tecio_python-0.1.1 → tecio_python-0.2.0}/tecio/dat/_write.py +363 -77
  17. {tecio_python-0.1.1 → tecio_python-0.2.0}/tecio/libtecio.py +39 -2
  18. {tecio_python-0.1.1 → tecio_python-0.2.0}/tecio/plt/_read.py +207 -90
  19. {tecio_python-0.1.1 → tecio_python-0.2.0}/tecio/plt/_write.py +379 -146
  20. {tecio_python-0.1.1 → tecio_python-0.2.0}/tecio/szl/_read.py +75 -26
  21. {tecio_python-0.1.1 → tecio_python-0.2.0}/tecio/szl/_write.py +532 -299
  22. {tecio_python-0.1.1 → tecio_python-0.2.0/tecio_python.egg-info}/PKG-INFO +2 -2
  23. {tecio_python-0.1.1 → tecio_python-0.2.0}/tecio_python.egg-info/SOURCES.txt +4 -8
  24. {tecio_python-0.1.1 → tecio_python-0.2.0}/tests/test_cli.py +663 -112
  25. tecio_python-0.2.0/tests/test_read.py +515 -0
  26. tecio_python-0.2.0/tests/test_write.py +1084 -0
  27. tecio_python-0.1.1/tests/test_dat_read.py +0 -438
  28. tecio_python-0.1.1/tests/test_dat_write.py +0 -1020
  29. tecio_python-0.1.1/tests/test_plt_read.py +0 -265
  30. tecio_python-0.1.1/tests/test_plt_write.py +0 -634
  31. tecio_python-0.1.1/tests/test_szl_read.py +0 -262
  32. tecio_python-0.1.1/tests/test_szl_write.py +0 -816
  33. tecio_python-0.1.1/tests/test_tec2mat.py +0 -320
  34. {tecio_python-0.1.1 → tecio_python-0.2.0}/LICENSE +0 -0
  35. {tecio_python-0.1.1 → tecio_python-0.2.0}/NOTICE +0 -0
  36. {tecio_python-0.1.1 → tecio_python-0.2.0}/setup.cfg +0 -0
  37. {tecio_python-0.1.1 → tecio_python-0.2.0}/tecio/_containers.py +0 -0
  38. {tecio_python-0.1.1 → tecio_python-0.2.0}/tecio/_io.py +0 -0
  39. /tecio_python-0.1.1/tecio/utils.py → /tecio_python-0.2.0/tecio/_utils.py +0 -0
  40. {tecio_python-0.1.1 → tecio_python-0.2.0}/tecio/cli/__init__.py +0 -0
  41. {tecio_python-0.1.1 → tecio_python-0.2.0}/tecio/dat/__init__.py +0 -0
  42. {tecio_python-0.1.1 → tecio_python-0.2.0}/tecio/plt/__init__.py +0 -0
  43. {tecio_python-0.1.1 → tecio_python-0.2.0}/tecio/szl/__init__.py +0 -0
  44. {tecio_python-0.1.1 → tecio_python-0.2.0}/tecio_python.egg-info/dependency_links.txt +0 -0
  45. {tecio_python-0.1.1 → tecio_python-0.2.0}/tecio_python.egg-info/entry_points.txt +0 -0
  46. {tecio_python-0.1.1 → tecio_python-0.2.0}/tecio_python.egg-info/requires.txt +0 -0
  47. {tecio_python-0.1.1 → tecio_python-0.2.0}/tecio_python.egg-info/top_level.txt +0 -0
  48. {tecio_python-0.1.1 → tecio_python-0.2.0}/tests/test_io.py +0 -0
  49. {tecio_python-0.1.1 → tecio_python-0.2.0}/tests/test_libtecio.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tecio-python
3
- Version: 0.1.1
3
+ Version: 0.2.0
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/
@@ -97,7 +97,7 @@ Reading a file:
97
97
 
98
98
  ```python
99
99
  with tecio.open("sine.szplt", "r") as tec:
100
- print(tec.variables) # ['x', 'y']
100
+ print(tec.variables) # ['x', 'y']
101
101
  x = tec.zone[0].variable[0].values
102
102
  y = tec.zone[0].variable[1].values
103
103
  # or
@@ -53,7 +53,7 @@ Reading a file:
53
53
 
54
54
  ```python
55
55
  with tecio.open("sine.szplt", "r") as tec:
56
- print(tec.variables) # ['x', 'y']
56
+ print(tec.variables) # ['x', 'y']
57
57
  x = tec.zone[0].variable[0].values
58
58
  y = tec.zone[0].variable[1].values
59
59
  # or
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "tecio-python"
7
- version = "0.1.1"
7
+ version = "0.2.0"
8
8
  description = "Python interface for reading and writing Tecplot data files"
9
9
 
10
10
  readme = "README.md"
@@ -11,7 +11,7 @@ try:
11
11
  except metadata.PackageNotFoundError:
12
12
  __version__ = "0.0.0"
13
13
 
14
- from . import cli, dat, libtecio, plt, szl, utils
14
+ from . import cli, dat, libtecio, plt, szl
15
15
  from ._containers import VariableList, ZoneList
16
16
  from ._io import AppendReadWrite, AppendWrite, open
17
17
 
@@ -28,7 +28,6 @@ __all__ = [
28
28
  "dat",
29
29
  "plt",
30
30
  "szl",
31
- "utils",
32
31
  "cli",
33
32
  "AppendWrite",
34
33
  "AppendReadWrite",
@@ -0,0 +1,168 @@
1
+ r"""Structural metadata records shared by the Tecplot file writers.
2
+
3
+ The writer for each output format -- SZL (``.szplt``), PLT (``.plt``), and ASCII DAT
4
+ (``.dat``) -- keeps a running, in-memory description of what it has committed to disk:
5
+ the dataset header, auxiliary-data counts, and one record per zone. Defining that
6
+ description once here lets all three writers share a single representation instead of
7
+ each maintaining ad-hoc bookkeeping.
8
+
9
+ The records mirror the file- and zone-level fields of the Tecplot data format
10
+ (``TECINI142`` for the file header and ``TECZNE142`` for each zone): dataset title, file
11
+ type, variable names, and the per-variable passive, value-location, and share-from
12
+ arrays, together with per-zone dimensions and aux-item counts.
13
+
14
+ Design notes:
15
+ * Only lightweight descriptors are stored -- shapes, enums, and small integers --
16
+ never the variable data arrays, so the record stays cheap in memory even for files
17
+ with many zones.
18
+ * ``slots=True`` removes the per-instance ``__dict__``, and immutable tuple fields
19
+ keep the per-variable lists compact. A :class:`ZoneMeta` is a write-once snapshot
20
+ and is therefore ``frozen``; :class:`WriterMeta` is mutable because it grows as
21
+ zones are written.
22
+ * Enum types are imported only under :data:`typing.TYPE_CHECKING`. With ``from
23
+ __future__ import annotations`` the annotations are never evaluated at runtime,
24
+ which keeps this module free of any import cycle with :mod:`tecio.libtecio`.
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ from dataclasses import dataclass, field
30
+ from typing import TYPE_CHECKING
31
+
32
+ if TYPE_CHECKING:
33
+ from .libtecio import DataType, FileType, ValueLocation, ZoneType
34
+
35
+
36
+ # =====================================================================================
37
+ # Zone-level record
38
+ # =====================================================================================
39
+
40
+
41
+ @dataclass(slots=True, frozen=True)
42
+ class ZoneMeta:
43
+ """Lightweight, write-once description of a single written zone.
44
+
45
+ Per-variable fields (:attr:`value_locations`, :attr:`passive_vars`,
46
+ :attr:`shared_vars`, :attr:`data_types`) span every dataset variable and correspond
47
+ to the ``ValueLocation``, ``PassiveVarList``, ``ShareVarFromZone``, and
48
+ variable-type arrays of a ``TECZNE142`` zone header.
49
+
50
+ Attributes:
51
+ index: 1-based zone index returned by the C library.
52
+ title: Zone title.
53
+ zone_type: The zone's :class:`~tecio.libtecio.ZoneType`.
54
+ solution_time: Solution time (``0.0`` for static zones).
55
+ strand_id: Strand ID (``0`` for static zones).
56
+ num_aux_items: Number of zone-level auxiliary items written.
57
+ dimensions: Nodal ``(imax, jmax, kmax)`` for ORDERED zones, else ``None``.
58
+ num_nodes: Node count for FE zones, else ``None``.
59
+ num_elements: Element count for FE zones, else ``None``.
60
+ value_locations: Per-variable value location, full dataset length.
61
+ passive_vars: Per-variable passive flags, full dataset length.
62
+ shared_vars: Per-variable share-from zone index (1-based; ``0`` for not
63
+ shared), full dataset length.
64
+ data_types: Per-variable data type, full dataset length.
65
+
66
+ """
67
+
68
+ index: int
69
+ title: str
70
+ zone_type: ZoneType
71
+ solution_time: float = 0.0
72
+ strand_id: int = 0
73
+ num_aux_items: int = 0
74
+ # Ordered zones carry IJK dimensions; FE zones carry node/element counts.
75
+ dimensions: tuple[int, int, int] | None = None
76
+ num_nodes: int | None = None
77
+ num_elements: int | None = None
78
+ # Per-variable descriptors (length == dataset variable count).
79
+ value_locations: tuple[ValueLocation, ...] = ()
80
+ passive_vars: tuple[bool, ...] = ()
81
+ shared_vars: tuple[int, ...] = ()
82
+ data_types: tuple[DataType, ...] = ()
83
+
84
+ @property
85
+ def nodal_shape(self) -> tuple[int, int, int] | None:
86
+ """Nodal ``(imax, jmax, kmax)`` for ORDERED zones, else ``None``."""
87
+ return self.dimensions
88
+
89
+ @property
90
+ def cell_shape(self) -> tuple[int, ...] | None:
91
+ """Cell-centred ``(imax-1, jmax-1, kmax-1)`` (floored at 1) or ``None``."""
92
+ if self.dimensions is None:
93
+ return None
94
+ return tuple(max(n - 1, 1) for n in self.dimensions)
95
+
96
+
97
+ # =====================================================================================
98
+ # Dataset-level record
99
+ # =====================================================================================
100
+
101
+
102
+ @dataclass(slots=True)
103
+ class WriterMeta:
104
+ """Running record of everything a writer has committed to a file.
105
+
106
+ Populated incrementally as the file header, auxiliary data, and zones are
107
+ written, so it always reflects the current on-disk state. It is the single
108
+ source of truth for cross-zone validation (for example, resolving the shape
109
+ of a shared variable) and is suitable for summarising the writer state.
110
+
111
+ Attributes:
112
+ path: Output file path.
113
+ title: Dataset title.
114
+ file_type: :class:`~tecio.libtecio.FileType` of the output.
115
+ file_format: Format tag, e.g. ``"szplt"``, ``"plt"``, or
116
+ ``"dat"``.
117
+ variables: Variable name list, or ``None`` before the file
118
+ header has been written (lazy-open).
119
+ num_dataset_aux_items: Count of dataset-level aux items written.
120
+ num_var_aux_items: Total variable-level aux items written.
121
+ zones: Mapping of 1-based zone index to :class:`ZoneMeta`,
122
+ in write order.
123
+ """
124
+
125
+ path: str
126
+ title: str
127
+ file_type: FileType
128
+ file_format: str
129
+ variables: list[str] | None = None
130
+ num_dataset_aux_items: int = 0
131
+ num_var_aux_items: int = 0
132
+ zones: dict[int, ZoneMeta] = field(default_factory=dict)
133
+
134
+ # -- Derived quantities -----------------------------------------------------------
135
+
136
+ @property
137
+ def num_vars(self) -> int:
138
+ """Number of dataset variables, or ``0`` before the header is written."""
139
+ return len(self.variables) if self.variables is not None else 0
140
+
141
+ @property
142
+ def num_zones(self) -> int:
143
+ """Number of zones written so far."""
144
+ return len(self.zones)
145
+
146
+ # -- Update methods (called by the writer as it commits data) ---------------------
147
+
148
+ def set_variables(self, names: list[str]) -> None:
149
+ """Record the dataset variable names once the header is written."""
150
+ self.variables = names
151
+
152
+ def note_dataset_aux(self, count: int) -> None:
153
+ """Accumulate the number of dataset-level aux items written."""
154
+ self.num_dataset_aux_items += count
155
+
156
+ def note_var_aux(self, count: int) -> None:
157
+ """Accumulate the number of variable-level aux items written."""
158
+ self.num_var_aux_items += count
159
+
160
+ def record_zone(self, zone: ZoneMeta) -> None:
161
+ """Register a fully written zone by its 1-based index."""
162
+ self.zones[zone.index] = zone
163
+
164
+ # -- Retrieval helpers ------------------------------------------------------------
165
+
166
+ def zone(self, index: int) -> ZoneMeta | None:
167
+ """Return the :class:`ZoneMeta` for *index*, or ``None`` if unknown."""
168
+ return self.zones.get(index)
@@ -69,7 +69,10 @@ Output structure:
69
69
  .var_locations cell 'NODAL' | 'CELL_CENTERED' | ''
70
70
  .var_dtypes cell 'FLOAT' | 'DOUBLE' | 'INT32' | ...
71
71
  .var_shared_from array 1-based source zone, or 0 if not shared
72
- .node_map array (num_elements x nodes_per_cell), FE only
72
+ .node_map array (num_elements x nodes_per_cell), FE only,
73
+ omitted if connectivity is shared
74
+ .node_map_shared_from double 1-based source zone, FE only, present only
75
+ if this zone shares its connectivity
73
76
 
74
77
  Variable arrays are stored at their on-disk NumPy dtype, so single/double/integer
75
78
  precision is preserved. The real variable names are kept only in ``info.var_names``
@@ -79,7 +82,9 @@ Output structure:
79
82
  Passive and shared variables carry no data: their ``var_<k>`` field is an empty
80
83
  matrix ``[]``. A shared variable is therefore never duplicated on disk -- the data
81
84
  lives in its source zone and ``var_shared_from`` records where, so the MATLAB user
82
- can dereference it (see the examples below).
85
+ can dereference it (see the examples below). Shared FE connectivity follows the
86
+ same convention: a zone sharing its node map has no ``node_map`` field at all, only
87
+ ``node_map_shared_from``.
83
88
 
84
89
  Examples:
85
90
  Convert an SZL file to ``flow.mat``::
@@ -119,6 +124,17 @@ Examples:
119
124
  end
120
125
  end
121
126
 
127
+ Shared FE connectivity resolves the same way, via ``node_map_shared_from``::
128
+
129
+ function m = tecnodemap(d, zoneIdx)
130
+ z = d.(sprintf('zone_%d', zoneIdx));
131
+ if isfield(z, 'node_map')
132
+ m = z.node_map;
133
+ else
134
+ m = d.(sprintf('zone_%d', z.node_map_shared_from)).node_map;
135
+ end
136
+ end
137
+
122
138
  See Also:
123
139
  * :mod:`tecio.cli.teconvert`: Convert between Tecplot file formats (``.szplt``,
124
140
  ``.plt``, ``.dat``) without leaving the Tecplot ecosystem.
@@ -153,9 +169,9 @@ import numpy as np
153
169
  from .. import open as tecio_open
154
170
  from ..libtecio import ZoneType
155
171
 
156
- # ---------------------------------------------------------------------------
172
+ # --------------------------------------------------------------------------------------
157
173
  # Constants
158
- # ---------------------------------------------------------------------------
174
+ # --------------------------------------------------------------------------------------
159
175
 
160
176
  #: Zone types whose connectivity is face-based and cannot be read; their
161
177
  #: node map is omitted from the output.
@@ -165,17 +181,18 @@ _FE_POLY: frozenset[ZoneType] = frozenset({
165
181
  })
166
182
 
167
183
 
168
- # ---------------------------------------------------------------------------
184
+ # --------------------------------------------------------------------------------------
169
185
  # Argument parsing
170
- # ---------------------------------------------------------------------------
186
+ # --------------------------------------------------------------------------------------
171
187
 
172
188
 
173
189
  def _parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
174
190
  parser = argparse.ArgumentParser(
175
191
  prog="tec2mat",
176
192
  description=(
177
- "Convert a Tecplot file to a MATLAB .mat file. Each input file maps to "
178
- "one output file, with every zone stored as a named struct."
193
+ # -|-------------------|---------------------------------------------|
194
+ "Convert a Tecplot file to a MATLAB .mat file. Each input file maps\n"
195
+ "to one output file, with every zone stored as a named struct."
179
196
  ),
180
197
  epilog=(
181
198
  "Example usage:\n"
@@ -236,9 +253,9 @@ def _parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
236
253
  return parser.parse_args(argv)
237
254
 
238
255
 
239
- # ---------------------------------------------------------------------------
256
+ # --------------------------------------------------------------------------------------
240
257
  # Conversion helpers
241
- # ---------------------------------------------------------------------------
258
+ # --------------------------------------------------------------------------------------
242
259
 
243
260
 
244
261
  def _build_info_dict(reader: Any) -> dict[str, Any]:
@@ -360,19 +377,28 @@ def _zone_to_dict(zone: Any, num_vars: int) -> dict[str, Any]:
360
377
  d["var_dtypes"] = np.array(dtypes, dtype=object)
361
378
  d["var_shared_from"] = shared_from
362
379
 
363
- # Connectivity for simple FE zones only. Ordered zones have no node map,
364
- # and poly zones expose none through the readers.
380
+ # Connectivity for simple FE zones only. Ordered zones have no node map, and poly
381
+ # zones expose none through the readers.
382
+ #
383
+ # A zone that shares connectivity is handled the same way as a shared variable
384
+ # above: rather than duplicating the (potentially large) node map into every zone's
385
+ # struct, store the 1-based source zone number so the MATLAB user can dereference
386
+ # zone_<src>.node_map themselves.
365
387
  if zone.zone_type != ZoneType.ORDERED and zone.zone_type not in _FE_POLY:
366
- node_map = zone.node_map
367
- if node_map is not None:
368
- d["node_map"] = node_map
388
+ con_src = zone.shared_connectivity
389
+ if con_src is not None:
390
+ d["node_map_shared_from"] = np.int32(con_src)
391
+ else:
392
+ node_map = zone.node_map
393
+ if node_map is not None:
394
+ d["node_map"] = node_map
369
395
 
370
396
  return d
371
397
 
372
398
 
373
- # ---------------------------------------------------------------------------
399
+ # --------------------------------------------------------------------------------------
374
400
  # Main entry point
375
- # ---------------------------------------------------------------------------
401
+ # --------------------------------------------------------------------------------------
376
402
 
377
403
 
378
404
  def main(argv: Sequence[str] | None = None) -> int:
@@ -384,9 +410,9 @@ def main(argv: Sequence[str] | None = None) -> int:
384
410
  """
385
411
  args = _parse_args(argv)
386
412
 
387
- # SciPy is an optional dependency, imported lazily so that merely importing
388
- # this module (e.g. to call main() from a script) does not require SciPy
389
- # unless the tool is actually run.
413
+ # SciPy is an optional dependency, imported lazily so that merely importing this
414
+ # module (e.g. to call main() from a script) does not require SciPy unless the tool
415
+ # is actually run.
390
416
  try:
391
417
  from scipy import io as scipy_io
392
418
  except ImportError:
@@ -87,10 +87,12 @@ def _parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
87
87
  parser = argparse.ArgumentParser(
88
88
  description="Dump all contents of a Tecplot file.",
89
89
  epilog=(
90
+ # -|-------------------|---------------------------------------------|
90
91
  "Example usage:\n"
91
92
  " Print all contents of file\n"
92
93
  " $ tecdump <input file>\n"
93
- " Print all values from zone 1 variable 3 (set maxval to large number)\n"
94
+ " Print all values from zone 1 variable 3 (set maxval to large\n"
95
+ " number)\n"
94
96
  " $ tecdump -zone 1 -variable 3 -maxvals 1e6 <input file>\n"
95
97
  ),
96
98
  formatter_class=lambda prog: argparse.RawDescriptionHelpFormatter(
@@ -158,67 +160,68 @@ def main(argv: Sequence[str] | None = None) -> int:
158
160
  # Create tec reader object
159
161
  with tecio_open(args.filename, "r") as tec:
160
162
  print("\nFile Record")
161
- print("=" * 70)
162
- print(f"File Type : {tec.file_type}")
163
- print(f"Dataset Title : {tec.title}")
164
- print(f"Num Vars : {tec.num_vars}")
165
- print(f"Variables : {tec.variables}")
166
- print(f"Num Zones : {tec.num_zones}")
167
- print(f"Dataset Aux Items : {tec.num_auxdata_items}")
163
+ print("=" * 78)
164
+ print(f"File Type : {tec.file_type}")
165
+ print(f"Dataset Title : {tec.title}")
166
+ print(f"Num Vars : {tec.num_vars}")
167
+ print(f"Variables : {tec.variables}")
168
+ print(f"Num Zones : {tec.num_zones}")
169
+ print(f"Dataset Aux Items : {tec.num_auxdata_items}")
168
170
 
169
171
  # Print dataset-level auxiliary data if available
170
172
  print("\n\nDataset Auxiliary Data")
171
- print("-" * 70)
173
+ print("-" * 78)
172
174
  if len(tec.auxdata) > 0:
173
175
  for name, value in tec.auxdata.items():
174
- print(f" {name:>15} : {value}")
176
+ print(f" {name:>20} : {value}")
175
177
 
176
178
  # Print variable-level auxiliary data if available
177
179
  print("\n\nVariable Auxiliary Data")
178
- print("-" * 70)
180
+ print("-" * 78)
179
181
  for i in range(tec.num_vars):
180
182
  var_aux = tec.get_var_auxdata(i + 1)
181
183
  if len(var_aux) > 0:
182
- print(f"Var {i + 1:3} Aux Data : {dict(var_aux)}")
184
+ print(f"Var {i + 1:3} Aux Data : {dict(var_aux)}")
183
185
  for name, value in var_aux.items():
184
- print(f" {name:>15} : {value}")
186
+ print(f" {name:>20} : {value}")
185
187
 
186
188
  # Print zone record
187
189
  if args.print_zones:
188
190
  print("\n\nZone Record")
189
- print("-" * 70)
191
+ print("-" * 78)
190
192
  for i in range(tec.num_zones):
191
193
  if (args.zone is None) or (i + 1 == args.zone):
192
194
  zone = tec.zone[i]
193
195
  print(f"\nZone {i + 1:3}")
194
- print(f" Title : {zone.title}")
195
- print(f" Zone Type : {zone.zone_type}")
196
+ print(f" Title : {zone.title}")
197
+ print(f" Zone Type : {zone.zone_type}")
196
198
  if zone.zone_type == ZoneType.ORDERED:
197
- print(f" I,J,K : {zone.dimensions}")
199
+ print(f" I,J,K : {zone.dimensions}")
198
200
  else:
199
- print(f" Num Nodes : {zone.num_nodes}")
200
- print(f" Num Elements : {zone.num_elements}")
201
- print(f" Is Enabled : {zone.is_enabled()}")
202
- print(f" Solution Time : {zone.solution_time}")
203
- print(f" Strand ID : {zone.strand_id}")
201
+ print(f" Num Nodes : {zone.num_nodes}")
202
+ print(f" Num Elements : {zone.num_elements}")
203
+ print(f" Is Enabled : {zone.is_enabled()}")
204
+ print(f" Solution Time : {zone.solution_time}")
205
+ print(f" Strand ID : {zone.strand_id}")
204
206
 
205
207
  # Print zone-level auxiliary data
206
208
  if len(zone.auxdata) > 0:
207
- print(f" Zone Aux Data : {dict(zone.auxdata)}")
209
+ print(f" Zone Aux Data : {dict(zone.auxdata)}")
208
210
  for name, value in zone.auxdata.items():
209
- print(f" {name:>15} : {value}")
211
+ print(f" {name:>20} : {value}")
210
212
 
211
213
  # Show node map for FE zones
212
214
  if zone.zone_type != ZoneType.ORDERED:
213
- print(f" Nodes Per Cell : {zone.nodes_per_cell}")
215
+ print(f" Shared Connectivity : {zone.shared_connectivity}")
216
+ print(f" Nodes Per Cell : {zone.nodes_per_cell}")
214
217
  if zone.node_map is not None:
215
- print(f" Node Map Shape : {zone.node_map.shape}")
218
+ print(f" Node Map Shape : {zone.node_map.shape}")
216
219
  value_str = np.array2string(
217
220
  zone.node_map,
218
- prefix=" Connectivity : ",
221
+ prefix=" Node Map : ",
219
222
  separator=", ",
220
223
  )
221
- print(f" Connectivity : {value_str}")
224
+ print(f" Node Map : {value_str}")
222
225
 
223
226
  # Print variable record
224
227
  if args.print_vars:
@@ -226,28 +229,32 @@ def main(argv: Sequence[str] | None = None) -> int:
226
229
  if (args.variable is None) or (j + 1 == args.variable):
227
230
  var = zone.variable[j]
228
231
  print(f" Variable {j + 1:3}")
229
- print(f" Name : {var.name}")
230
- print(f" Data Type : {var.data_type}")
231
- print(f" Location : {var.value_location}")
232
- print(f" Is Enabled : {var.is_enabled()}")
233
- print(f" Is Passive : {var.is_passive()}")
234
- print(f" Shared Zone : {var.shared_zone}")
235
- print(f" Num Values : {var.num_values}")
232
+ print(f" Name : {var.name}")
233
+ print(f" Is Enabled : {var.is_enabled()}")
234
+ print(f" Is Passive : {var.is_passive()}")
236
235
 
237
236
  # Check if variable is shared or passive (no data)
238
- if (
239
- var.shared_zone is None
240
- and not var.is_passive()
241
- and var.values is not None
242
- ):
243
- print(f" Array shape : {var.values.shape}")
237
+ if (not var.is_passive()) and (var.values is not None):
238
+ print(
239
+ f" Location : {var.value_location}"
240
+ ) # noqa: E501
241
+ print(f" Shared Zone : {var.shared_zone}")
242
+ print(f" Num Values : {var.num_values}")
243
+ print(f" Data Type : {var.data_type}")
244
+ print(
245
+ f" NumPy Data Type : {var.values.dtype}"
246
+ ) # noqa: E501
247
+ print(
248
+ f" Array Shape : {var.values.shape}"
249
+ ) # noqa: E501
250
+
244
251
  # Get first 100 values or all if fewer than 100
245
252
  value_str = np.array2string(
246
253
  var.values,
247
- prefix=" Values : ",
254
+ prefix=" Values : ",
248
255
  separator=", ",
249
256
  )
250
- print(f" Values : {value_str}")
257
+ print(f" Values : {value_str}")
251
258
 
252
259
  return 0
253
260
 
@@ -93,9 +93,9 @@ import numpy as np
93
93
  from .. import open as tecio_open
94
94
  from ..libtecio import ZoneType
95
95
 
96
- # ---------------------------------------------------------------------------
96
+ # --------------------------------------------------------------------------------------
97
97
  # Helpers
98
- # ---------------------------------------------------------------------------
98
+ # --------------------------------------------------------------------------------------
99
99
 
100
100
 
101
101
  def _parse_index_list(value: str) -> list[int]:
@@ -119,19 +119,21 @@ def _parse_index_list(value: str) -> list[int]:
119
119
  ) from exc
120
120
 
121
121
 
122
- # ---------------------------------------------------------------------------
122
+ # --------------------------------------------------------------------------------------
123
123
  # Argument parsing
124
- # ---------------------------------------------------------------------------
124
+ # --------------------------------------------------------------------------------------
125
125
 
126
126
 
127
127
  def _parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
128
128
  parser = argparse.ArgumentParser(
129
129
  prog="tecextract",
130
130
  description=(
131
- "Extract a subset of zones and/or variables from a Tecplot file. "
131
+ # -|--------------------|---------------------------------------------|
132
+ "Extract a subset of zones and/or variables from a Tecplot file.\n"
132
133
  "Output format is determined by the -o extension."
133
134
  ),
134
135
  epilog=(
136
+ # -|--------------------|---------------------------------------------|
135
137
  "Example usage:\n"
136
138
  " Extract zones 1 and 3\n"
137
139
  " $ tecextract -zones 1,3 <file>\n"
@@ -284,6 +286,15 @@ def main(argv: Sequence[str] | None = None) -> int:
284
286
  if auxvar:
285
287
  writer.add_auxvar_dict(auxvar)
286
288
 
289
+ # Maps a source zone's 1-based index to its 1-based index in the output
290
+ # file, populated as zones are actually written:
291
+ # - A variable/connectivity shared from a zone that's also in this map
292
+ # can have its sharing preserved (just pointing at the new, compacted
293
+ # index) instead of being materialized as independent data
294
+ # - Only a share whose source zone was excluded from the extraction
295
+ # genuinely has nowhere to point and must fall back to real data.
296
+ zone_index_map: dict[int, int] = {}
297
+
287
298
  for i, zone in enumerate(reader.zone):
288
299
  zone_num = i + 1
289
300
  if zone_num not in zone_set:
@@ -306,18 +317,26 @@ def main(argv: Sequence[str] | None = None) -> int:
306
317
 
307
318
  for orig_idx in out_var_indices:
308
319
  var = zone.variable[orig_idx - 1]
309
- passive_vars.append(var.is_passive())
310
- sv = var.shared_zone
311
- # Sharing refers to zones by their index in the
312
- # *output* file, which may differ from the source.
313
- # We cannot safely remap shares to a reduced zone
314
- # set, so drop sharing — write as independent data.
315
- var_sharing.append(0)
320
+ is_passive = var.is_passive()
321
+ passive_vars.append(is_passive)
316
322
  active_locs.append(var.value_location)
317
323
 
318
- if var.is_passive() or sv is not None:
324
+ sv = var.shared_zone
325
+ remapped = zone_index_map.get(sv) if sv is not None else None
326
+
327
+ if is_passive:
328
+ var_sharing.append(0)
329
+ active_data.append(np.array([], dtype=np.float32))
330
+ elif remapped is not None:
331
+ # Source zone was also extracted -> preserve the data
332
+ # sharing relationship
333
+ var_sharing.append(remapped)
319
334
  active_data.append(np.array([], dtype=np.float32))
320
335
  else:
336
+ # Variables not shared at all, or shared from a zone that is
337
+ # not output, in which case branch sharing -> write the
338
+ # actual values as independent data
339
+ var_sharing.append(0)
321
340
  arr = var.values
322
341
  if arr is None or arr.size == 0:
323
342
  passive_vars[-1] = True
@@ -327,13 +346,17 @@ def main(argv: Sequence[str] | None = None) -> int:
327
346
 
328
347
  writer_data = [
329
348
  arr
330
- for arr, is_p in zip(active_data, passive_vars, strict=False)
331
- if not is_p
349
+ for arr, is_p, sv in zip(
350
+ active_data, passive_vars, var_sharing, strict=False
351
+ )
352
+ if not is_p and sv == 0
332
353
  ]
333
354
  writer_locs = [
334
355
  loc
335
- for loc, is_p in zip(active_locs, passive_vars, strict=False)
336
- if not is_p
356
+ for loc, is_p, sv in zip(
357
+ active_locs, passive_vars, var_sharing, strict=False
358
+ )
359
+ if not is_p and sv == 0
337
360
  ]
338
361
 
339
362
  zone_aux: dict[str, str] | None = None
@@ -353,13 +376,23 @@ def main(argv: Sequence[str] | None = None) -> int:
353
376
  if zt == ZoneType.ORDERED:
354
377
  writer.write_ijk_zone(data=writer_data, **common_kw)
355
378
  else:
379
+ con_src = zone.shared_connectivity
380
+ con_remapped = (
381
+ zone_index_map.get(con_src) if con_src is not None else None
382
+ )
356
383
  writer.write_fe_zone(
357
384
  zone_type=zt,
358
385
  data=writer_data,
359
- node_map=zone.node_map,
386
+ node_map=None if con_remapped else zone.node_map,
387
+ con_sharing=con_remapped,
360
388
  **common_kw,
361
389
  )
362
390
 
391
+ # Record where this source zone landed in the output, so a later
392
+ # zone sharing from it can point at the real (compacted) index
393
+ # instead of falling back to independent data.
394
+ zone_index_map[zone_num] = writer.current_zone
395
+
363
396
  except Exception as exc: # noqa: BLE001
364
397
  print(f"Error: {exc}", file=sys.stderr)
365
398
  dst.unlink(missing_ok=True)