tecio-python 0.2.2__py3-none-any.whl → 0.2.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
tecio/__init__.py CHANGED
@@ -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.0.0"
12
+ __version__ = "0.2.4"
13
13
 
14
14
  from . import cli, dat, libtecio, plt, szl
15
15
  from ._containers import VariableList, ZoneList
tecio/cli/tec2mat.py CHANGED
@@ -136,9 +136,9 @@ Examples:
136
136
  end
137
137
 
138
138
  See Also:
139
- * :mod:`tecio.cli.teconvert`: Convert between Tecplot file formats (``.szplt``,
139
+ * :mod:`tecio.cli.teconvert` - Convert between Tecplot file formats (``.szplt``,
140
140
  ``.plt``, ``.dat``) without leaving the Tecplot ecosystem.
141
- * :mod:`tecio.cli.tecdump`: Inspect the full contents and metadata of a file before
141
+ * :mod:`tecio.cli.tecdump` - Inspect the full contents and metadata of a file before
142
142
  converting it.
143
143
 
144
144
  Note:
tecio/cli/tecdump.py CHANGED
@@ -70,7 +70,7 @@ Examples:
70
70
  See Also:
71
71
  * :mod:`tecio.cli.tecstats` - Compute per-zone min, max, and mean statistics without
72
72
  printing raw values.
73
- * :mod:`tecio.cli.tecfix`: Rewrite a file with invalid variable
73
+ * :mod:`tecio.cli.tecfix` - Rewrite a file with invalid variable
74
74
  arrays set to passive once bad values have been identified.
75
75
  """
76
76
 
tecio/cli/tecfix.py CHANGED
@@ -66,9 +66,9 @@ Examples:
66
66
  tecfix(["flow.szplt", "--output", "clean.szplt"])
67
67
 
68
68
  See Also:
69
- * :mod:`tecio.cli.tecdump`: Inspect file contents and metadata to
69
+ * :mod:`tecio.cli.tecdump` - Inspect file contents and metadata to
70
70
  identify zones or variables that may require fixing.
71
- * :mod:`tecio.cli.tecstats`: Compute per-zone min, max, and mean
71
+ * :mod:`tecio.cli.tecstats` - Compute per-zone min, max, and mean
72
72
  statistics without printing raw values.
73
73
 
74
74
  Note:
tecio/cli/tecmerge.py CHANGED
@@ -83,10 +83,10 @@ Examples:
83
83
  tecmerge(["part1.szplt", "part2.szplt", "--output", "combined.szplt"])
84
84
 
85
85
  See Also:
86
- * :mod:`tecio.cli.tecextract`: Extract a zone/variable subset from a single file —
86
+ * :mod:`tecio.cli.tecextract` - Extract a zone/variable subset from a single file —
87
87
  the inverse of merging.
88
- * :mod:`tecio.cli.tecsplit`: Split a file into separate grid and solution files.
89
- * :mod:`tecio.cli.tecslice`: Extract planar slices from volumetric zone data.
88
+ * :mod:`tecio.cli.tecsplit` - Split a file into separate grid and solution files.
89
+ * :mod:`tecio.cli.tecslice` - Extract planar slices from volumetric zone data.
90
90
 
91
91
  """
92
92
 
@@ -155,6 +155,7 @@ def _parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
155
155
  ),
156
156
  )
157
157
  parser.add_argument(
158
+ "-f",
158
159
  "--force",
159
160
  action="store_true",
160
161
  default=False,
tecio/cli/teconvert.py CHANGED
@@ -71,7 +71,7 @@ Examples:
71
71
  teconvert(["-dat", "-o", "flow.dat", "flow.szplt"])
72
72
 
73
73
  See Also:
74
- :mod:`tecio.cli.tecextract`: Extract a zone/variable subset while simultaneously
74
+ :mod:`tecio.cli.tecextract` - Extract a zone/variable subset while simultaneously
75
75
  converting format via the output file extension.
76
76
  """
77
77
 
@@ -132,7 +132,7 @@ def _parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
132
132
  " Explicit output path\n"
133
133
  " $ teconvert -plt -o /tmp/out.plt flow.dat\n"
134
134
  " Overwrite existing\n"
135
- " teconvert --force -dat flow.szplt\n"
135
+ " $ teconvert --force -dat flow.szplt\n"
136
136
  ),
137
137
  formatter_class=lambda prog: argparse.RawDescriptionHelpFormatter(
138
138
  prog, width=70, max_help_position=24
tecio/cli/tecscale.py CHANGED
@@ -80,7 +80,7 @@ Examples:
80
80
  tecscale(["-variable", "Pressure", "-scale", "1e-3", "flow.szplt"])
81
81
 
82
82
  See Also:
83
- :mod:`tecio.cli.teconvert`: Convert between Tecplot file formats without applying
83
+ :mod:`tecio.cli.teconvert` - Convert between Tecplot file formats without applying
84
84
  any variable transformation.
85
85
 
86
86
  """
tecio/cli/tecslice.py CHANGED
@@ -114,10 +114,10 @@ Examples:
114
114
  tecslice(["-i", "::2", "-o", "thinned.szplt", "flow.szplt"])
115
115
 
116
116
  See Also:
117
- * :mod:`tecio.cli.tecextract`: Extract a subset of zones or variables by index
117
+ * :mod:`tecio.cli.tecextract` - Extract a subset of zones or variables by index
118
118
  rather than by positional slice.
119
- * :mod:`tecio.cli.tecsplit`: Split a file into separate grid and solution files.
120
- * :mod:`tecio.cli.tecmerge`: Merge zones from multiple files into a single output.
119
+ * :mod:`tecio.cli.tecsplit` - Split a file into separate grid and solution files.
120
+ * :mod:`tecio.cli.tecmerge` - Merge zones from multiple files into a single output.
121
121
  """
122
122
 
123
123
  from __future__ import annotations
@@ -277,7 +277,7 @@ def _parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
277
277
  description=(
278
278
  # -|--------------------|---------------------------------------------|
279
279
  "Slice a Tecplot file along IJK indices (ordered zones) and/or\n"
280
- "solution time (all zone types).\n"
280
+ "solution time (all zone types).\n\n"
281
281
  "Slice notation: start:end:skip (any component may be omitted)."
282
282
  ),
283
283
  epilog=(
@@ -329,9 +329,9 @@ def _parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
329
329
  ijk = parser.add_argument_group(
330
330
  "IJK slicing",
331
331
  (
332
- "Colon-notation slices for structured axes. "
333
- "Indices are 1-based and inclusive. "
334
- "Applied only to ordered (structured) zones."
332
+ # -|--------------------|---------------------------------------------|
333
+ "Colon-notation slices for structured axes. Indices are 1-based and\n"
334
+ "inclusive. Applied only to ordered (structured) zones."
335
335
  ),
336
336
  )
337
337
  for axis in ("i", "j", "k"):
@@ -351,8 +351,9 @@ def _parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
351
351
  tslice = parser.add_argument_group(
352
352
  "solution-time slicing",
353
353
  (
354
- "Applied per strand to all zone types. "
355
- "Strand-0 zones are always written unchanged."
354
+ # -|--------------------|---------------------------------------------|
355
+ "Applied per strand to all zone types. Strand-0 zones are always\n"
356
+ "written unchanged."
356
357
  ),
357
358
  )
358
359
  tslice.add_argument(
tecio/cli/tecstats.py CHANGED
@@ -85,9 +85,9 @@ Examples:
85
85
  tecstats(["-zone", "2", "-variable", "3", "flow.szplt"])
86
86
 
87
87
  See Also:
88
- * :mod:`tecio.cli.tecdump`: Inspect the full contents and metadata of a file,
88
+ * :mod:`tecio.cli.tecdump` - Inspect the full contents and metadata of a file,
89
89
  including auxiliary data and raw variable arrays.
90
- * :mod:`tecio.cli.tecfix`: Rewrite a file with invalid variable arrays set to
90
+ * :mod:`tecio.cli.tecfix` - Rewrite a file with invalid variable arrays set to
91
91
  passive once bad values have been identified via statistics.
92
92
 
93
93
  """
tecio/dat/_read.py CHANGED
@@ -137,6 +137,10 @@ _STR_TO_DATATYPE: dict[str, DataType] = {
137
137
  #: Values per line for Write data blocks.
138
138
  _VALUES_PER_LINE: int = 5
139
139
 
140
+ # Safety cap on the number of grow-and-reparse iterations the vectorized block readers
141
+ # will attempt before giving up and falling back to the tolerant token-by-token parser
142
+ _MAX_FAST_BLOCK_ITERATIONS: int = 64
143
+
140
144
 
141
145
  # ======================================================================================
142
146
  # Shared internal helpers
@@ -174,6 +178,42 @@ def _strip_comment(line: str) -> str:
174
178
  return line[:idx].rstrip() if idx >= 0 else line.rstrip()
175
179
 
176
180
 
181
+ def _is_float_block_boundary(line: str) -> bool:
182
+ r"""Return ``True`` if a stripped *line* ends a float data block early.
183
+
184
+ Shared by :meth:`Read._read_float_block_slow` (line-by-line) and
185
+ :meth:`Read._read_float_block_fast` (bulk, on parse failure) so the two readers
186
+ agree on exactly where a block stops.
187
+
188
+ Example:
189
+ >>> _is_float_block_boundary("ZONE T=\\"next\\"")
190
+ True
191
+ """
192
+ upper = line.lstrip().upper()
193
+ if not upper:
194
+ return False
195
+ if upper.split("=")[0].split()[0] == "ZONE":
196
+ return True
197
+ return (
198
+ upper.startswith(("DATASETAUXDATA", "VARAUXDATA"))
199
+ or upper.startswith("TITLE")
200
+ and "=" in upper
201
+ or upper.startswith("VARIABLES")
202
+ and "=" in upper
203
+ )
204
+
205
+
206
+ def _is_int_block_boundary(line: str) -> bool:
207
+ r"""Return ``True`` if a stripped *line* ends an int (connectivity) block.
208
+
209
+ Example:
210
+ >>> _is_int_block_boundary("DATASETAUXDATA foo=\\"bar\\"")
211
+ True
212
+ """
213
+ upper = line.lstrip().upper()
214
+ return upper.startswith(("ZONE", "DATASETAUXDATA"))
215
+
216
+
177
217
  def _infer_data_type(arr: npt.NDArray) -> DataType:
178
218
  """Return the most appropriate :class:`DataType` for *arr*'s dtype.
179
219
 
@@ -569,38 +609,99 @@ def _apply_varauxdata(line: str, var_auxdata_list: list) -> None:
569
609
  class _LineBuffer:
570
610
  """Peekable iterator over stripped, comment-free text lines.
571
611
 
612
+ Also exposes a small *raw* interface (:meth:`take_raw`, :meth:`position`,
613
+ :meth:`seek`) used only by the vectorized numeric-block readers
614
+ (:meth:`Read._read_float_block_fast`/:meth:`Read._read_int_block_fast`). Those
615
+ readers bypass per-line comment-stripping for speed and instead detect the rare line
616
+ that *isn't* plain numeric data (a comment, a header keyword) by letting NumPy's
617
+ parser fail on it, then falling back to this class's ordinary stripped-line
618
+ interface. Keeping both interfaces on one object means the fast path can hand back
619
+ an exact resume position for that fallback via :meth:`position`/:meth:`seek`.
620
+
572
621
  Example:
573
622
  >>> buf = _LineBuffer(lines)
574
623
  """
575
624
 
625
+ __slots__ = ("_lines", "_peeked", "_peeked_pos", "_pos")
626
+
576
627
  def __init__(self, lines: list[str]) -> None:
577
628
  self._lines: list[str] = lines
578
629
  self._pos: int = 0
630
+ # One-line lookahead cache so a peek_stripped() immediately followed by
631
+ # next_stripped() strips comments/whitespace only once instead of twice.
632
+ self._peeked: str | None = None
633
+ self._peeked_pos: int = -1
579
634
 
580
635
  def has_more(self) -> bool:
581
636
  """Return ``True`` if there are unconsumed lines."""
582
- return self._pos < len(self._lines)
637
+ return self._peeked is not None or self._pos < len(self._lines)
583
638
 
584
639
  def peek_stripped(self) -> str:
585
640
  """Return the next non-blank stripped line without consuming it."""
641
+ if self._peeked is not None:
642
+ return self._peeked
586
643
  pos = self._pos
587
- while pos < len(self._lines):
588
- raw = _strip_comment(self._lines[pos])
589
- stripped = raw.strip()
644
+ n = len(self._lines)
645
+ while pos < n:
646
+ stripped = _strip_comment(self._lines[pos]).strip()
647
+ pos += 1
590
648
  if stripped:
649
+ self._peeked = stripped
650
+ self._peeked_pos = pos
591
651
  return stripped
592
- pos += 1
652
+ self._peeked = ""
653
+ self._peeked_pos = pos
593
654
  return ""
594
655
 
595
656
  def next_stripped(self) -> str:
596
657
  """Consume and return the next non-blank stripped line."""
597
- while self._pos < len(self._lines):
598
- raw = _strip_comment(self._lines[self._pos])
599
- self._pos += 1
600
- stripped = raw.strip()
601
- if stripped:
602
- return stripped
603
- return ""
658
+ if self._peeked is None:
659
+ self.peek_stripped()
660
+ value = self._peeked or ""
661
+ self._pos = self._peeked_pos
662
+ self._peeked = None
663
+ self._peeked_pos = -1
664
+ return value
665
+
666
+ def take_raw(self, count: int) -> list[str]:
667
+ """Consume and return up to *count* **unprocessed** source lines.
668
+
669
+ Unlike :meth:`next_stripped`, this does not strip comments, skip blank lines, or
670
+ strip whitespace -- it is a raw slice of the underlying line list, advancing the
671
+ position by exactly how many lines were returned (fewer than *count* at end of
672
+ file). Any pending :meth:`peek_stripped` lookahead is discarded first, since
673
+ this call always resumes from the true unconsumed position.
674
+
675
+ Intended only for the numeric fast-path readers, which feed the result straight
676
+ to a whitespace-tolerant NumPy parser and don't need per-line preprocessing.
677
+
678
+ Example:
679
+ >>> lines = buf.take_raw(64)
680
+ """
681
+ self._peeked = None
682
+ self._peeked_pos = -1
683
+ end = min(self._pos + count, len(self._lines))
684
+ out = self._lines[self._pos : end]
685
+ self._pos = end
686
+ return out
687
+
688
+ def position(self) -> int:
689
+ """Return an opaque marker for the current position.
690
+
691
+ Example:
692
+ >>> marker = buf.position()
693
+ """
694
+ return self._pos
695
+
696
+ def seek(self, marker: int) -> None:
697
+ """Restore the position to a marker previously returned by :meth:`position`.
698
+
699
+ Example:
700
+ >>> buf.seek(marker)
701
+ """
702
+ self._pos = marker
703
+ self._peeked = None
704
+ self._peeked_pos = -1
604
705
 
605
706
 
606
707
  # ======================================================================================
@@ -706,20 +807,23 @@ class ReadAuxData:
706
807
  class ReadVariable:
707
808
  """Variable metadata and data for one zone parsed from an ASCII DAT file.
708
809
 
709
- Interface matches :class:`szl.ReadVariable`.
710
-
711
- Example:
712
- >>> var = ReadVariable(name, data, value_location, ...)
810
+ Args:
811
+ zone_index: 1-based index of the zone this variable belongs to.
812
+ var_index: 1-based dataset variable index.
713
813
  """
714
814
 
715
815
  def __init__(
716
816
  self,
817
+ zone_index: int,
818
+ var_index: int,
717
819
  name: str,
718
820
  data: npt.NDArray | None,
719
821
  value_location: ValueLocation = ValueLocation.NODAL,
720
822
  is_passive: bool = False,
721
823
  shared_zone: int | None = None,
722
824
  ) -> None:
825
+ self.zone_index: int = zone_index
826
+ self.var_index: int = var_index
723
827
  self._name: str = name
724
828
  self._data: npt.NDArray | None = data
725
829
  self._value_location: ValueLocation = value_location
@@ -844,6 +948,7 @@ class ReadZone:
844
948
  """Zone data parsed from a Tecplot ASCII DAT file.
845
949
 
846
950
  Attributes:
951
+ zone_index: 1-based dataset zone index.
847
952
  datapacking: :class:`~tecio.libtecio.DataPacking` member reflecting the
848
953
  ``DATAPACKING`` keyword found in the zone header (``BLOCK`` or
849
954
  ``POINT``). The data arrays are identical either way; this attribute
@@ -853,14 +958,15 @@ class ReadZone:
853
958
  automatically resolves to the source zone's array.
854
959
 
855
960
  Example:
856
- >>> zone = ReadZone(title, zone_type, I, J, K, ...)
961
+ >>> zone = ReadZone(zone_index, title, zone_type, I, J, K, ...)
857
962
  """
858
963
 
859
964
  def __init__(
860
965
  self,
966
+ zone_index: int,
861
967
  title: str,
862
968
  zone_type: ZoneType,
863
- I: int, # noqa: E741
969
+ I: int, # noqa E741
864
970
  J: int,
865
971
  K: int,
866
972
  solution_time: float,
@@ -871,6 +977,7 @@ class ReadZone:
871
977
  datapacking: DataPacking = DataPacking.BLOCK,
872
978
  shared_connectivity: int | None = None,
873
979
  ) -> None:
980
+ self.zone_index: int = zone_index
874
981
  self.title: str = title
875
982
  self.zone_type: ZoneType = zone_type
876
983
  self.I: int = I
@@ -1052,7 +1159,6 @@ class Read:
1052
1159
  release on exit. Provided for API consistency with the Write classes and to
1053
1160
  support the ``with tecio.open(...) as r:`` pattern.
1054
1161
  """
1055
- pass
1056
1162
 
1057
1163
  # -- Properties --------------------------------------------------------------------
1058
1164
 
@@ -1264,9 +1370,7 @@ class Read:
1264
1370
  # Stop at a new zone, top-level keyword, or the first data line.
1265
1371
  if nxt_upper.split("=")[0].split()[0] == "ZONE":
1266
1372
  break
1267
- if nxt_upper.startswith("DATASETAUXDATA") or nxt_upper.startswith(
1268
- "VARAUXDATA"
1269
- ):
1373
+ if nxt_upper.startswith(("DATASETAUXDATA", "VARAUXDATA")):
1270
1374
  break
1271
1375
  first_ch = nxt.lstrip()[0] if nxt.lstrip() else ""
1272
1376
  # A data line begins with a numeric token (covers leading-dot values
@@ -1332,7 +1436,7 @@ class Read:
1332
1436
  )
1333
1437
 
1334
1438
  if zone_type == ZoneType.ORDERED:
1335
- I = int(kv.get("I", "1") or "1") # noqa: E741
1439
+ I = int(kv.get("I", "1") or "1") # noqa E741
1336
1440
  J = int(kv.get("J", "1") or "1")
1337
1441
  K = int(kv.get("K", "1") or "1")
1338
1442
  num_nodes = I * J * K
@@ -1342,7 +1446,7 @@ class Read:
1342
1446
  # spellings for the node and element counts.
1343
1447
  num_nodes = int(kv.get("NODES", kv.get("N", "0")) or "0")
1344
1448
  num_cells = int(kv.get("ELEMENTS", kv.get("E", "0")) or "0")
1345
- I, J, K = num_nodes, num_cells, 0 # noqa: E741
1449
+ I, J, K = num_nodes, num_cells, 0 # noqa E741
1346
1450
 
1347
1451
  # Packing: DATAPACKING wins, then legacy F, else BLOCK (Tecplot default).
1348
1452
  if "DATAPACKING" in kv:
@@ -1459,8 +1563,13 @@ class Read:
1459
1563
  self._zones[src_zone_1based - 1].variable[var_idx].values
1460
1564
  )
1461
1565
 
1566
+ # This zone's own 1-based index
1567
+ zone_index = len(self._zones) + 1
1568
+
1462
1569
  read_vars = [
1463
1570
  ReadVariable(
1571
+ zone_index=zone_index,
1572
+ var_index=idx + 1,
1464
1573
  name=name,
1465
1574
  data=_shaped(
1466
1575
  var_arrays[idx],
@@ -1475,6 +1584,7 @@ class Read:
1475
1584
 
1476
1585
  self._zones.append(
1477
1586
  ReadZone(
1587
+ zone_index=zone_index,
1478
1588
  title=zone_title,
1479
1589
  zone_type=zone_type,
1480
1590
  I=I,
@@ -1497,26 +1607,98 @@ class Read:
1497
1607
  """Read *n_values* floats from *tokens* into an intermediate float64 array.
1498
1608
 
1499
1609
  This is a pure text-parsing primitive. Callers cast the result to each
1500
- variable's real dtype afterward, once individual per-variable arrays have been
1610
+ variable's real dtype afterward, once individual per-variable arrays have been
1501
1611
  split out.
1502
1612
 
1613
+ Dispatches to :meth:`_read_float_block_fast`, a vectorized NumPy parser that
1614
+ handles the overwhelming majority of (well-formed) input in a handful of C-level
1615
+ calls regardless of block size, and falls back to :meth:`_read_float_block_slow`
1616
+ -- a tolerant, token-by-token parser -- only if the fast path can't make sense
1617
+ of what it read (a stray comment or non-numeric token inside the block, or a
1618
+ boundary reached before *n_values* values were collected). The fallback re-reads
1619
+ from *tokens* at the position the fast path started from, so the two never
1620
+ disagree about what was consumed.
1621
+
1503
1622
  See Also:
1504
1623
  :meth:`_read_block_var_data`/:meth:`_read_point_var_data`.
1505
1624
 
1506
1625
  Examples:
1507
1626
  >>> arr = Read._read_float_block(tokens, 100)
1508
1627
  """
1628
+ if n_values <= 0:
1629
+ return np.empty(0, dtype=np.float64)
1630
+
1631
+ marker = tokens.position()
1632
+ try:
1633
+ arr = Read._read_float_block_fast(tokens, n_values)
1634
+ except ValueError:
1635
+ arr = None
1636
+ if arr is not None:
1637
+ return arr
1638
+
1639
+ tokens.seek(marker)
1640
+ return Read._read_float_block_slow(tokens, n_values)
1641
+
1642
+ @staticmethod
1643
+ def _read_float_block_fast(
1644
+ tokens: _LineBuffer, n_values: int
1645
+ ) -> npt.NDArray | None:
1646
+ """Vectorized fast path for :meth:`_read_float_block`.
1647
+
1648
+ Reads raw (unprocessed) lines in a handful of growing bulk grabs (sized from a
1649
+ one-line width probe, so a multi-million-value block costs a small constant
1650
+ number of Python-level calls). Passing ``sep`` here is the (fully supported)
1651
+ text-parsing mode of that function, not its deprecated binary-decoding mode.
1652
+
1653
+ Returns ``None`` (never a short array) if a block boundary is hit before
1654
+ *n_values* values are collected, so the caller can unambiguously fall back to
1655
+ the slow, boundary-aware parser. A ``ValueError`` from ``numpy.fromstring`` is
1656
+ allowed to propagate to the caller for the same reason.
1657
+
1658
+ Example:
1659
+ >>> arr = Read._read_float_block_fast(tokens, 100)
1660
+
1661
+ """
1662
+ pieces: list[npt.NDArray] = []
1663
+ have = 0
1664
+ per_line: int | None = None
1665
+ for _ in range(_MAX_FAST_BLOCK_ITERATIONS):
1666
+ remaining = n_values - have
1667
+ if remaining <= 0:
1668
+ break
1669
+ # First grab is a single-line probe to measure the block's (typically
1670
+ # constant) values-per-line width
1671
+ n_lines = 1 if per_line is None else -(-remaining // per_line)
1672
+ batch = tokens.take_raw(n_lines)
1673
+ if not batch:
1674
+ return None # ran out of input before reaching n_values
1675
+ piece = np.fromstring("".join(batch), dtype=np.float64, sep=" ")
1676
+ if per_line is None:
1677
+ per_line = max(piece.size, 1)
1678
+ pieces.append(piece)
1679
+ have += piece.size
1680
+ else:
1681
+ return None # didn't converge; let the slow path sort it out
1682
+ arr = pieces[0] if len(pieces) == 1 else np.concatenate(pieces)
1683
+ return arr[:n_values] if arr.size >= n_values else None
1684
+
1685
+ @staticmethod
1686
+ def _read_float_block_slow(tokens: _LineBuffer, n_values: int) -> npt.NDArray:
1687
+ """Tolerant, token-by-token fallback for :meth:`_read_float_block`.
1688
+
1689
+ Reads one stripped line at a time, converting tokens one at a time and silently
1690
+ skipping any that aren't valid floats, stopping at a block boundary (see
1691
+ :func:`_is_float_block_boundary`) even if *n_values* hasn't been reached
1692
+ yet. Slow by design, but guarantees a result either way.
1693
+
1694
+ Example:
1695
+ >>> arr = Read._read_float_block_slow(tokens, 100)
1696
+
1697
+ """
1509
1698
  values: list[float] = []
1510
1699
  while len(values) < n_values and tokens.has_more():
1511
1700
  nxt = tokens.peek_stripped()
1512
- upper = nxt.lstrip().upper()
1513
- if (
1514
- upper.split("=")[0].split()[0] == "ZONE"
1515
- or upper.startswith("DATASETAUXDATA")
1516
- or upper.startswith("VARAUXDATA")
1517
- or (upper.startswith("TITLE") and "=" in upper)
1518
- or (upper.startswith("VARIABLES") and "=" in upper)
1519
- ):
1701
+ if _is_float_block_boundary(nxt):
1520
1702
  break
1521
1703
  line = tokens.next_stripped()
1522
1704
  for tok in line.split():
@@ -1547,6 +1729,10 @@ class Read:
1547
1729
  to that variable's actual declared dtype (from ``DT=``, or the spec's documented
1548
1730
  SINGLE default) right here, where each variable's array is finalized.
1549
1731
 
1732
+ Reading one variable at a time (rather than all of them in a single combined
1733
+ call) is deliberate: :meth:`_read_float_block`'s vectorized fast path already
1734
+ costs only a handful of C-level calls per block regardless of size.
1735
+
1550
1736
  Note:
1551
1737
  Passive and shared variables contribute a ``None`` placeholder to the
1552
1738
  returned list. The caller resolves shared variables against the zones parsed
@@ -1649,14 +1835,70 @@ class Read:
1649
1835
  def _read_int_block(tokens: _LineBuffer, n_values: int) -> npt.NDArray:
1650
1836
  """Read exactly *n_values* integers from *tokens* into an int64 array.
1651
1837
 
1838
+ Used for connectivity (node-map) data, which can run to tens of millions of
1839
+ entries for large volume meshes. Follows the same vectorized-fast-path /
1840
+ tolerant-slow-path split as :meth:`_read_float_block`; see that method's
1841
+ docstring for the rationale.
1842
+
1652
1843
  Examples:
1653
1844
  >>> arr = Read._read_int_block(tokens, 24)
1654
1845
  """
1846
+ if n_values <= 0:
1847
+ return np.empty(0, dtype=np.int64)
1848
+
1849
+ marker = tokens.position()
1850
+ try:
1851
+ arr = Read._read_int_block_fast(tokens, n_values)
1852
+ except ValueError:
1853
+ arr = None
1854
+ if arr is not None:
1855
+ return arr
1856
+
1857
+ tokens.seek(marker)
1858
+ return Read._read_int_block_slow(tokens, n_values)
1859
+
1860
+ @staticmethod
1861
+ def _read_int_block_fast(tokens: _LineBuffer, n_values: int) -> npt.NDArray | None:
1862
+ """Vectorized fast path for :meth:`_read_int_block`.
1863
+
1864
+ See :meth:`_read_float_block_fast`, which this mirrors exactly except for the
1865
+ target dtype.
1866
+
1867
+ Example:
1868
+ >>> arr = Read._read_int_block_fast(tokens, 24)
1869
+ """
1870
+ pieces: list[npt.NDArray] = []
1871
+ have = 0
1872
+ per_line: int | None = None
1873
+ for _ in range(_MAX_FAST_BLOCK_ITERATIONS):
1874
+ remaining = n_values - have
1875
+ if remaining <= 0:
1876
+ break
1877
+ n_lines = 1 if per_line is None else -(-remaining // per_line)
1878
+ batch = tokens.take_raw(n_lines)
1879
+ if not batch:
1880
+ return None
1881
+ piece = np.fromstring("".join(batch), dtype=np.int64, sep=" ")
1882
+ if per_line is None:
1883
+ per_line = max(piece.size, 1)
1884
+ pieces.append(piece)
1885
+ have += piece.size
1886
+ else:
1887
+ return None
1888
+ arr = pieces[0] if len(pieces) == 1 else np.concatenate(pieces)
1889
+ return arr[:n_values] if arr.size >= n_values else None
1890
+
1891
+ @staticmethod
1892
+ def _read_int_block_slow(tokens: _LineBuffer, n_values: int) -> npt.NDArray:
1893
+ """Tolerant, token-by-token fallback for :meth:`_read_int_block`.
1894
+
1895
+ Example:
1896
+ >>> arr = Read._read_int_block_slow(tokens, 24)
1897
+ """
1655
1898
  values: list[int] = []
1656
1899
  while len(values) < n_values and tokens.has_more():
1657
1900
  nxt = tokens.peek_stripped()
1658
- upper = nxt.lstrip().upper()
1659
- if upper.startswith("ZONE") or upper.startswith("DATASETAUXDATA"):
1901
+ if _is_int_block_boundary(nxt):
1660
1902
  break
1661
1903
  line = tokens.next_stripped()
1662
1904
  for tok in line.split():
tecio/plt/_read.py CHANGED
@@ -552,7 +552,7 @@ class ReadVariable:
552
552
 
553
553
  # Byte-swap if needed so the result is always native-endian
554
554
  if data.dtype.byteorder not in ("=", "|", np.dtype(dt).str[0]):
555
- data = data.byteswap().newbyteorder()
555
+ data = data.byteswap().view(data.dtype.newbyteorder())
556
556
 
557
557
  # Reshape for full reads of ordered zones, using the owning zone's dimensions
558
558
  # and value location (they describe the on-disk layout including cell-centered