tecio-python 0.2.0__py3-none-any.whl → 0.2.2__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/_containers.py +2 -2
- tecio/_io.py +46 -38
- tecio/_meta.py +4 -4
- tecio/cli/__init__.py +3 -3
- tecio/cli/tec2mat.py +21 -21
- tecio/cli/tecaux.py +971 -0
- tecio/cli/tecdump.py +4 -4
- tecio/cli/tecextract.py +6 -6
- tecio/cli/tecfix.py +4 -4
- tecio/cli/tecmerge.py +12 -12
- tecio/cli/teconvert.py +3 -3
- tecio/cli/tecscale.py +7 -7
- tecio/cli/tecslice.py +12 -12
- tecio/cli/tecstats.py +4 -4
- tecio/dat/__init__.py +1 -1
- tecio/dat/_read.py +33 -21
- tecio/dat/_write.py +22 -22
- tecio/libtecio.py +73 -16
- tecio/plt/_read.py +21 -21
- tecio/plt/_write.py +35 -35
- tecio/szl/_read.py +3 -3
- tecio/szl/_write.py +50 -28
- {tecio_python-0.2.0.dist-info → tecio_python-0.2.2.dist-info}/METADATA +1 -1
- tecio_python-0.2.2.dist-info/RECORD +33 -0
- {tecio_python-0.2.0.dist-info → tecio_python-0.2.2.dist-info}/entry_points.txt +1 -0
- {tecio_python-0.2.0.dist-info → tecio_python-0.2.2.dist-info}/licenses/NOTICE +2 -2
- tecio_python-0.2.0.dist-info/RECORD +0 -32
- {tecio_python-0.2.0.dist-info → tecio_python-0.2.2.dist-info}/WHEEL +0 -0
- {tecio_python-0.2.0.dist-info → tecio_python-0.2.2.dist-info}/licenses/LICENSE +0 -0
- {tecio_python-0.2.0.dist-info → tecio_python-0.2.2.dist-info}/top_level.txt +0 -0
tecio/cli/tecaux.py
ADDED
|
@@ -0,0 +1,971 @@
|
|
|
1
|
+
r"""Add, remove, or export dataset, zone, or variable level auxiliary data.
|
|
2
|
+
|
|
3
|
+
Tecplot's auxiliary data mechanism attaches arbitrary ``name=value`` metadata to a
|
|
4
|
+
dataset, a zone, or a variable (solver name, run date, units, a description, or any
|
|
5
|
+
other annotation that doesn't belong in the numerical data itself_). Managing this
|
|
6
|
+
after the fact ordinarily means writing a one-off script against the TecIO API.
|
|
7
|
+
``tecaux`` does this from the command line, in a single read/write pass, in three
|
|
8
|
+
mutually exclusive modes:
|
|
9
|
+
|
|
10
|
+
* By default (no ``--strip``/``--export-json``): merges new auxiliary entries (from
|
|
11
|
+
``-d``/``-z``/``-v`` and/or ``-j``) into a copy of the input file. Every zone,
|
|
12
|
+
variable, and existing sharing relationship is preserved exactly; an existing key with
|
|
13
|
+
the same name is overwritten.
|
|
14
|
+
* ``--strip`` removes every auxiliary entry at all three levels and writes the result to
|
|
15
|
+
a new file, still preserving sharing.
|
|
16
|
+
* ``--export-json`` writes every *existing* auxiliary entry to a JSON file, in the same
|
|
17
|
+
format ``-j`` reads back -- without touching the source file at all, unless combined
|
|
18
|
+
with ``--strip``.
|
|
19
|
+
|
|
20
|
+
:Usage:
|
|
21
|
+
|
|
22
|
+
.. code:: bash
|
|
23
|
+
|
|
24
|
+
tecaux [-h] [-d KEY=VALUE] ... [-z INDEX KEY=VALUE] ...
|
|
25
|
+
[-v INDEX_OR_NAME KEY=VALUE] ... [-j PATH] [-s] [--export-json]
|
|
26
|
+
[-o PATH] [-f] PATH
|
|
27
|
+
|
|
28
|
+
:Positional Arguments:
|
|
29
|
+
``PATH``
|
|
30
|
+
Path to the input Tecplot file (``.plt``, ``.szplt``, or ``.dat``).
|
|
31
|
+
|
|
32
|
+
:Options:
|
|
33
|
+
``-d KEY=VALUE``, ``--data KEY=VALUE``
|
|
34
|
+
A ``name=value`` pair to set as dataset-level auxiliary data. Repeat the flag
|
|
35
|
+
for multiple pairs.
|
|
36
|
+
|
|
37
|
+
``-z INDEX KEY=VALUE``, ``--zone INDEX KEY=VALUE``
|
|
38
|
+
A ``name=value`` pair to set as zone-level auxiliary data on the one-based zone
|
|
39
|
+
``INDEX`` -- or on every zone if ``INDEX`` is the literal word ``all``. Repeat
|
|
40
|
+
the flag for multiple pairs and/or multiple zones; each occurrence takes exactly
|
|
41
|
+
one zone and one pair, so ``-z 1 A=1 -z 1 B=2`` sets both ``A`` and ``B`` on
|
|
42
|
+
zone 1.
|
|
43
|
+
|
|
44
|
+
``-v INDEX_OR_NAME KEY=VALUE``, ``--var INDEX_OR_NAME KEY=VALUE``
|
|
45
|
+
A ``name=value`` pair to set as variable-level auxiliary data on the variable
|
|
46
|
+
given by a one-based index or a name (case-insensitive) -- or on every variable
|
|
47
|
+
if the target is the literal word ``all``. Repeatable, same as ``-z``.
|
|
48
|
+
|
|
49
|
+
``-j PATH``, ``--json PATH``
|
|
50
|
+
Load bulk auxiliary data from a JSON file (see format below). Applied before
|
|
51
|
+
any ``-d``/``-z``/``-v`` flags, which take precedence on a key collision.
|
|
52
|
+
|
|
53
|
+
``-s``, ``--strip``
|
|
54
|
+
Remove all auxiliary data (dataset, zone, and variable levels) and write the
|
|
55
|
+
result to ``<stem>_no_aux<ext>`` (or ``-o``'s path). Mutually exclusive with
|
|
56
|
+
``-d``/``-z``/``-v``/``-j``. Combine with ``--export-json`` to keep a JSON copy
|
|
57
|
+
of what was removed.
|
|
58
|
+
|
|
59
|
+
``--export-json``
|
|
60
|
+
Write every existing auxiliary entry to ``<stem>_aux.json``, in the same
|
|
61
|
+
``AUXDATASET``/``AUXZONE``/``AUXVAR`` format ``-j`` reads, without modifying
|
|
62
|
+
the source file, unless combined with ``--strip``. Mutually exclusive with
|
|
63
|
+
``-d``/``-z``/``-v``/``-j``.
|
|
64
|
+
|
|
65
|
+
``-o PATH``, ``--output PATH``
|
|
66
|
+
Output file path. The extension controls the output format: ``.szplt``,
|
|
67
|
+
``.plt``, or ``.dat``. Defaults to ``<stem>_aux<ext>``, or
|
|
68
|
+
``<stem>_no_aux<ext>`` with ``--strip``. Not used by ``--export-json`` alone
|
|
69
|
+
which always writes ``<stem>_aux.json``, regardless of ``-o``.
|
|
70
|
+
|
|
71
|
+
``-f``, ``--force``
|
|
72
|
+
Overwrite the output file(s) if they already exist. Without this flag the
|
|
73
|
+
command exits with an error rather than silently clobbering an existing file.
|
|
74
|
+
|
|
75
|
+
:JSON Format:
|
|
76
|
+
.. code:: json
|
|
77
|
+
|
|
78
|
+
{
|
|
79
|
+
"AUXDATASET": {"Solver": "MyCFD", "Version": "2.1"},
|
|
80
|
+
"AUXZONE": {
|
|
81
|
+
"1": {"Description": "Wing"},
|
|
82
|
+
"all": {"Batch": "2024"}
|
|
83
|
+
},
|
|
84
|
+
"AUXVAR": {
|
|
85
|
+
"Pressure": {"Units": "Pa"},
|
|
86
|
+
"1": {"Source": "Experiment"}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
All three top-level keys are optional, and match this library's own attribute/method
|
|
91
|
+
names for the same three levels exactly
|
|
92
|
+
(``Write.auxdataset``/``add_auxdataset_dict``, ``Write.auxvar``/``add_auxvar_dict``;
|
|
93
|
+
``AUXZONE`` is the natural third member of that family even though zone-level aux
|
|
94
|
+
has no "zone"-prefixed name internally.
|
|
95
|
+
|
|
96
|
+
In ``"AUXZONE"``/``"AUXVAR"``, a key is either a one-based index, a variable name
|
|
97
|
+
(``"AUXVAR"`` only), or the literal string ``"all"`` meaning every zone/variable
|
|
98
|
+
(the same three forms ``-z``/``-v`` accept on the command line). Every JSON key must
|
|
99
|
+
be a quoted string, including numeric indices (``"1"``, not ``1``).
|
|
100
|
+
|
|
101
|
+
``--export-json`` writes this exact format back out, keyed by exact 1-based index
|
|
102
|
+
(never ``"all"``, even if every zone happens to share identical aux content) and
|
|
103
|
+
omitting any zone/variable/level with nothing to report.
|
|
104
|
+
|
|
105
|
+
:Returns:
|
|
106
|
+
In the default mode, a new Tecplot file written to the output path with the
|
|
107
|
+
requested auxiliary data merged in. With ``--strip``, a new Tecplot file with all
|
|
108
|
+
auxiliary data removed. With ``--export-json``, a JSON file of everything that was
|
|
109
|
+
found (and the source file is untouched, unless ``--strip`` is also given). Exit
|
|
110
|
+
code is ``0`` on success and non-zero if the input file cannot be read, a
|
|
111
|
+
``-z``/``-v``/JSON target cannot be resolved, ``--strip``/``--export-json`` is
|
|
112
|
+
combined with ``-d``/``-z``/``-v``/``-j``, or an output file already exists and
|
|
113
|
+
``--force`` is not set.
|
|
114
|
+
|
|
115
|
+
Examples:
|
|
116
|
+
Tag a dataset with solver metadata (repeat the flag for multiple pairs)::
|
|
117
|
+
|
|
118
|
+
$ tecaux -d Solver=MyCFD -d Version=2.1 flow.szplt
|
|
119
|
+
|
|
120
|
+
Two pairs on zone 1, one pair on zone 2::
|
|
121
|
+
|
|
122
|
+
$ tecaux -z 1 Description=Wing -z 1 Area=120sqm -z 2 Description=Fuselage \
|
|
123
|
+
flow.szplt
|
|
124
|
+
|
|
125
|
+
Annotate a single variable by name::
|
|
126
|
+
|
|
127
|
+
$ tecaux -v Pressure Units=Pa flow.szplt
|
|
128
|
+
|
|
129
|
+
Every zone at once, via the "all" target::
|
|
130
|
+
|
|
131
|
+
$ tecaux -z all RunDate=2024-01-15 flow.plt
|
|
132
|
+
|
|
133
|
+
Everything in one pass, written as a bash script would naturally lay it out::
|
|
134
|
+
|
|
135
|
+
$ tecaux --data Solver=MyCFD \
|
|
136
|
+
--data Version=2.1 \
|
|
137
|
+
--zone 1 Case=A \
|
|
138
|
+
--zone 1 Description=Wing \
|
|
139
|
+
--zone 2 Case=B \
|
|
140
|
+
--var Pressure Units=Pa \
|
|
141
|
+
-o tagged.szplt flow.szplt
|
|
142
|
+
|
|
143
|
+
Bulk metadata from a file, with one CLI override on top::
|
|
144
|
+
|
|
145
|
+
$ tecaux -j metadata.json -d Version=2.2 flow.szplt
|
|
146
|
+
|
|
147
|
+
Back up everything currently on a file before editing it by hand, without touching
|
|
148
|
+
the file itself::
|
|
149
|
+
|
|
150
|
+
$ tecaux --export-json flow.szplt # writes flow_aux.json
|
|
151
|
+
|
|
152
|
+
Sanitize a file for external sharing, discarding whatever aux data it had::
|
|
153
|
+
|
|
154
|
+
$ tecaux --strip flow.szplt # writes flow_no_aux.szplt
|
|
155
|
+
|
|
156
|
+
Sanitize a file but keep a record of what was removed, in case it's needed later
|
|
157
|
+
(e.g. to reapply with ``-j`` after review)::
|
|
158
|
+
|
|
159
|
+
$ tecaux --strip --export-json flow.szplt
|
|
160
|
+
# writes flow_no_aux.szplt and flow_aux.json
|
|
161
|
+
|
|
162
|
+
Call directly from a Python session::
|
|
163
|
+
|
|
164
|
+
import tecio.cli.tecaux.main as tecaux
|
|
165
|
+
|
|
166
|
+
tecaux(["-d", "Solver=MyCFD", "flow.szplt"])
|
|
167
|
+
|
|
168
|
+
See Also:
|
|
169
|
+
* :mod:`tecio.cli.tecfix` - Rewrite a file with invalid variable arrays set to
|
|
170
|
+
passive, using the same verbatim zone-copy approach.
|
|
171
|
+
* :mod:`tecio.cli.teconvert` - Convert between formats without modifying auxiliary
|
|
172
|
+
data.
|
|
173
|
+
|
|
174
|
+
"""
|
|
175
|
+
|
|
176
|
+
from __future__ import annotations
|
|
177
|
+
|
|
178
|
+
import argparse
|
|
179
|
+
import json
|
|
180
|
+
import sys
|
|
181
|
+
from collections.abc import Sequence
|
|
182
|
+
from pathlib import Path
|
|
183
|
+
from typing import Any
|
|
184
|
+
|
|
185
|
+
import numpy as np
|
|
186
|
+
|
|
187
|
+
from .. import open as tecio_open
|
|
188
|
+
from ..libtecio import ZoneType
|
|
189
|
+
|
|
190
|
+
# --------------------------------------------------------------------------------------
|
|
191
|
+
# Constants
|
|
192
|
+
# --------------------------------------------------------------------------------------
|
|
193
|
+
|
|
194
|
+
#: FE zone types that the Write API cannot copy.
|
|
195
|
+
_FE_POLY: frozenset[ZoneType] = frozenset({ZoneType.FEPOLYGON, ZoneType.FEPOLYHEDRON})
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class _ArgError(Exception):
|
|
199
|
+
"""Raised for a malformed KEY=VALUE pair or unresolvable target."""
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
# --------------------------------------------------------------------------------------
|
|
203
|
+
# Argument parsing
|
|
204
|
+
# --------------------------------------------------------------------------------------
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
|
|
208
|
+
parser = argparse.ArgumentParser(
|
|
209
|
+
prog="tecaux",
|
|
210
|
+
description=(
|
|
211
|
+
# -|-------------------|---------------------------------------------|
|
|
212
|
+
"Add dataset-, zone-, or variable-level auxiliary data to a Tecplot\n"
|
|
213
|
+
"file in a single read/write pass. Everything else (zones, variables,\n"
|
|
214
|
+
"and sharing) is copied verbatim."
|
|
215
|
+
),
|
|
216
|
+
epilog=(
|
|
217
|
+
# -|-------------------|---------------------------------------------|
|
|
218
|
+
"Example usage:\n"
|
|
219
|
+
" Dataset-level metadata (repeat the flag for multiple pairs)\n"
|
|
220
|
+
" $ tecaux -d Solver=MyCFD -d Version=2.1 <file>\n"
|
|
221
|
+
" Two pairs on zone 1, one pair on zone 2\n"
|
|
222
|
+
" $ tecaux -z 1 Description=Wing -z 1 Area=120sqm -z 2 Case=B <file>\n"
|
|
223
|
+
" A single variable by name\n"
|
|
224
|
+
" $ tecaux -v Pressure Units=Pa <file>\n"
|
|
225
|
+
" Every zone at once\n"
|
|
226
|
+
" $ tecaux -z all RunDate=2024-01-15 <file>\n"
|
|
227
|
+
" Bulk metadata from a file\n"
|
|
228
|
+
" $ tecaux -j metadata.json <file>\n"
|
|
229
|
+
" Export existing aux data without touching the source file\n"
|
|
230
|
+
" $ tecaux --export-json <file>\n"
|
|
231
|
+
" Strip all aux data, keeping a JSON backup of what was removed\n"
|
|
232
|
+
" $ tecaux --strip --export-json <file>\n"
|
|
233
|
+
),
|
|
234
|
+
formatter_class=lambda prog: argparse.RawDescriptionHelpFormatter(
|
|
235
|
+
prog, width=70, max_help_position=24
|
|
236
|
+
),
|
|
237
|
+
)
|
|
238
|
+
parser.add_argument(
|
|
239
|
+
"filename",
|
|
240
|
+
type=str,
|
|
241
|
+
help="Input Tecplot file to add auxiliary data to.",
|
|
242
|
+
)
|
|
243
|
+
parser.add_argument(
|
|
244
|
+
"-d",
|
|
245
|
+
"--data",
|
|
246
|
+
action="append",
|
|
247
|
+
default=None,
|
|
248
|
+
metavar="KEY=VALUE",
|
|
249
|
+
help="A name=value pair to set as dataset-level auxiliary data. Repeatable.",
|
|
250
|
+
)
|
|
251
|
+
parser.add_argument(
|
|
252
|
+
"-z",
|
|
253
|
+
"--zone",
|
|
254
|
+
action="append",
|
|
255
|
+
nargs=2,
|
|
256
|
+
default=None,
|
|
257
|
+
metavar=("INDEX", "KEY=VALUE"),
|
|
258
|
+
help=(
|
|
259
|
+
"A name=value pair to set as zone-level auxiliary data on the "
|
|
260
|
+
"given one-based zone index, or on every zone if INDEX is "
|
|
261
|
+
"'all'. Repeatable, one pair per occurrence."
|
|
262
|
+
),
|
|
263
|
+
)
|
|
264
|
+
parser.add_argument(
|
|
265
|
+
"-v",
|
|
266
|
+
"--var",
|
|
267
|
+
action="append",
|
|
268
|
+
nargs=2,
|
|
269
|
+
default=None,
|
|
270
|
+
metavar=("INDEX_OR_NAME", "KEY=VALUE"),
|
|
271
|
+
help=(
|
|
272
|
+
"A name=value pair to set as variable-level auxiliary data on "
|
|
273
|
+
"the given variable (1-based index or name), or on every "
|
|
274
|
+
"variable if the target is 'all'. Repeatable, one pair per "
|
|
275
|
+
"occurrence."
|
|
276
|
+
),
|
|
277
|
+
)
|
|
278
|
+
parser.add_argument(
|
|
279
|
+
"-j",
|
|
280
|
+
"--json",
|
|
281
|
+
type=str,
|
|
282
|
+
default=None,
|
|
283
|
+
metavar="PATH",
|
|
284
|
+
help=(
|
|
285
|
+
"Load bulk auxiliary data from a JSON file, applied before "
|
|
286
|
+
"-d/-z/-v (which override it on a key collision). Top-level "
|
|
287
|
+
"keys: AUXDATASET, AUXZONE, AUXVAR (matching Write.auxdataset/"
|
|
288
|
+
"Write.auxvar). Full format in the module docstring."
|
|
289
|
+
),
|
|
290
|
+
)
|
|
291
|
+
parser.add_argument(
|
|
292
|
+
"-s",
|
|
293
|
+
"--strip",
|
|
294
|
+
action="store_true",
|
|
295
|
+
default=False,
|
|
296
|
+
help=(
|
|
297
|
+
"Remove all auxiliary data (dataset, zone, and variable levels) "
|
|
298
|
+
"and write the result to <stem>_no_aux<ext> (or -o's path). "
|
|
299
|
+
"Mutually exclusive with -d/-z/-v/-j; combine with "
|
|
300
|
+
"--export-json to also save what was removed."
|
|
301
|
+
),
|
|
302
|
+
)
|
|
303
|
+
parser.add_argument(
|
|
304
|
+
"--export-json",
|
|
305
|
+
action="store_true",
|
|
306
|
+
default=False,
|
|
307
|
+
help=(
|
|
308
|
+
"Write every existing auxiliary entry to <stem>_aux.json, in "
|
|
309
|
+
"the same AUXDATASET/AUXZONE/AUXVAR format -j reads -- without "
|
|
310
|
+
"modifying the source file, unless combined with --strip. "
|
|
311
|
+
"Mutually exclusive with -d/-z/-v/-j."
|
|
312
|
+
),
|
|
313
|
+
)
|
|
314
|
+
parser.add_argument(
|
|
315
|
+
"-o",
|
|
316
|
+
"--output",
|
|
317
|
+
type=str,
|
|
318
|
+
default=None,
|
|
319
|
+
metavar="PATH",
|
|
320
|
+
help=(
|
|
321
|
+
"Output file path. The extension controls the output format. "
|
|
322
|
+
"Defaults to <stem>_aux<ext> in the same directory as the input "
|
|
323
|
+
"(or <stem>_no_aux<ext> with --strip; not used by --export-json "
|
|
324
|
+
"alone, which always writes <stem>_aux.json)."
|
|
325
|
+
),
|
|
326
|
+
)
|
|
327
|
+
parser.add_argument(
|
|
328
|
+
"-f",
|
|
329
|
+
"--force",
|
|
330
|
+
action="store_true",
|
|
331
|
+
default=False,
|
|
332
|
+
help="Overwrite the output file if it already exists.",
|
|
333
|
+
)
|
|
334
|
+
return parser.parse_args(argv)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
# ---------------------------------------------------------------------------
|
|
338
|
+
# Helpers
|
|
339
|
+
# ---------------------------------------------------------------------------
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def _parse_kv(token: str) -> tuple[str, str]:
|
|
343
|
+
"""Parse one ``KEY=VALUE`` token.
|
|
344
|
+
|
|
345
|
+
Args:
|
|
346
|
+
token: A single string like ``"Solver=MyCFD"``. Only the first ``"="`` is
|
|
347
|
+
significant, so a value may itself contain ``"="``
|
|
348
|
+
(e.g. ``"Formula=a=b+c"``). A value containing spaces must be quoted as
|
|
349
|
+
a whole on the command line, the same as any other shell argument
|
|
350
|
+
(e.g. ``-z 1 "Description=Wing surface"``) -- this function only ever
|
|
351
|
+
sees whatever single token the shell already produced, so it can't
|
|
352
|
+
recover a value that arrived pre-split into several tokens.
|
|
353
|
+
|
|
354
|
+
Returns:
|
|
355
|
+
``(key, value)``, with the key stripped of surrounding whitespace.
|
|
356
|
+
|
|
357
|
+
Raises:
|
|
358
|
+
_ArgError: If *token* has no ``"="``, or the key is empty.
|
|
359
|
+
|
|
360
|
+
"""
|
|
361
|
+
if "=" not in token:
|
|
362
|
+
raise _ArgError(f"Expected KEY=VALUE, got: {token!r}")
|
|
363
|
+
key, _, value = token.partition("=")
|
|
364
|
+
key = key.strip()
|
|
365
|
+
if not key:
|
|
366
|
+
raise _ArgError(f"Empty key in: {token!r}")
|
|
367
|
+
return key, value
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _resolve_variable(spec: str, var_names: list[str]) -> int | None:
|
|
371
|
+
"""Return a 0-based variable index from a name or 1-based integer string.
|
|
372
|
+
|
|
373
|
+
Args:
|
|
374
|
+
spec: User-supplied string (e.g. ``"3"`` or ``"Pressure"``).
|
|
375
|
+
var_names: Ordered list of variable names from the reader.
|
|
376
|
+
|
|
377
|
+
Returns:
|
|
378
|
+
0-based variable index, or ``None`` if *spec* cannot be resolved. Callers are
|
|
379
|
+
responsible for reporting the failure.
|
|
380
|
+
|
|
381
|
+
"""
|
|
382
|
+
try:
|
|
383
|
+
idx = int(spec)
|
|
384
|
+
except ValueError:
|
|
385
|
+
pass
|
|
386
|
+
else:
|
|
387
|
+
if idx < 1 or idx > len(var_names):
|
|
388
|
+
return None
|
|
389
|
+
return idx - 1
|
|
390
|
+
|
|
391
|
+
spec_lower = spec.lower()
|
|
392
|
+
for i, name in enumerate(var_names):
|
|
393
|
+
if name.lower() == spec_lower:
|
|
394
|
+
return i
|
|
395
|
+
|
|
396
|
+
return None
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def _to_groups(
|
|
400
|
+
raw_pairs: list[list[str]] | None,
|
|
401
|
+
) -> list[tuple[str | None, dict[str, str]]]:
|
|
402
|
+
"""Convert argparse's ``[[target, "KEY=VALUE"], ...]`` into consolidation input.
|
|
403
|
+
|
|
404
|
+
Each occurrence becomes its own single-pair group; ``_consolidate_groups`` merges
|
|
405
|
+
repeats of the same target on its own, so nothing needs to be pre-grouped here.
|
|
406
|
+
|
|
407
|
+
Args:
|
|
408
|
+
raw_pairs: ``args.zone``/``args.var`` as argparse produced them, or ``None`` if
|
|
409
|
+
the flag was never given.
|
|
410
|
+
|
|
411
|
+
Returns:
|
|
412
|
+
``[(target, {key: value}), ...]``, target ``None`` for ``"all"``.
|
|
413
|
+
|
|
414
|
+
Raises:
|
|
415
|
+
_ArgError: If a KEY=VALUE token is malformed.
|
|
416
|
+
|
|
417
|
+
"""
|
|
418
|
+
groups: list[tuple[str | None, dict[str, str]]] = []
|
|
419
|
+
for target_raw, kv_raw in raw_pairs or []:
|
|
420
|
+
target = None if target_raw.lower() == "all" else target_raw
|
|
421
|
+
key, value = _parse_kv(kv_raw)
|
|
422
|
+
groups.append((target, {key: value}))
|
|
423
|
+
return groups
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def _consolidate_groups(
|
|
427
|
+
groups: list[tuple[str | None, dict[str, str]]],
|
|
428
|
+
resolve: Any,
|
|
429
|
+
) -> tuple[dict[str, str], dict[int, dict[str, str]]]:
|
|
430
|
+
"""Merge repeated groups into one broadcast dict and one per-target dict.
|
|
431
|
+
|
|
432
|
+
Args:
|
|
433
|
+
groups: ``[(target, {key: value}), ...]`` -- target is ``None`` ("every
|
|
434
|
+
zone"/"every variable") or a raw string to resolve.
|
|
435
|
+
resolve: Callable taking the raw target string and returning a 1-based index for
|
|
436
|
+
a specific target, or raising ``_ArgError`` if it can't be resolved.
|
|
437
|
+
|
|
438
|
+
Returns:
|
|
439
|
+
``(broadcast, by_target)`` where ``by_target`` is keyed by whatever index
|
|
440
|
+
``resolve`` returns.
|
|
441
|
+
|
|
442
|
+
"""
|
|
443
|
+
broadcast: dict[str, str] = {}
|
|
444
|
+
by_target: dict[int, dict[str, str]] = {}
|
|
445
|
+
for target, pairs in groups:
|
|
446
|
+
if target is None:
|
|
447
|
+
broadcast.update(pairs)
|
|
448
|
+
else:
|
|
449
|
+
idx = resolve(target)
|
|
450
|
+
by_target.setdefault(idx, {}).update(pairs)
|
|
451
|
+
return broadcast, by_target
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
# --------------------------------------------------------------------------------------
|
|
455
|
+
# JSON loading
|
|
456
|
+
# --------------------------------------------------------------------------------------
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def _load_json_aux(
|
|
460
|
+
path: str,
|
|
461
|
+
) -> tuple[
|
|
462
|
+
dict[str, str],
|
|
463
|
+
list[tuple[str | None, dict[str, str]]],
|
|
464
|
+
list[tuple[str | None, dict[str, str]]],
|
|
465
|
+
]:
|
|
466
|
+
"""Load bulk auxiliary data from a JSON file.
|
|
467
|
+
|
|
468
|
+
Top-level keys are ``AUXDATASET``, ``AUXZONE``, ``AUXVAR``.
|
|
469
|
+
|
|
470
|
+
Returns:
|
|
471
|
+
``(dataset_aux, zone_groups, var_groups)`` in the same shape ``_to_groups``
|
|
472
|
+
produces, so both sources merge identically.
|
|
473
|
+
|
|
474
|
+
Raises:
|
|
475
|
+
_ArgError: If the file can't be read or parsed, contains an unrecognized
|
|
476
|
+
top-level key, or a value isn't a string -> string mapping.
|
|
477
|
+
|
|
478
|
+
"""
|
|
479
|
+
try:
|
|
480
|
+
with open(path, encoding="utf-8") as fh:
|
|
481
|
+
data = json.load(fh)
|
|
482
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
483
|
+
raise _ArgError(f"Could not read JSON file {path!r}: {exc}") from exc
|
|
484
|
+
|
|
485
|
+
if not isinstance(data, dict):
|
|
486
|
+
raise _ArgError(f"JSON file {path!r} must contain an object at the top level.")
|
|
487
|
+
|
|
488
|
+
_KNOWN_KEYS = {"AUXDATASET", "AUXZONE", "AUXVAR"}
|
|
489
|
+
unknown: set[str] = {str(k) for k in data} - _KNOWN_KEYS
|
|
490
|
+
if unknown:
|
|
491
|
+
raise _ArgError(
|
|
492
|
+
f"Unrecognized top-level key(s) in {path!r}: {sorted(unknown)}. "
|
|
493
|
+
f"Expected one of: {sorted(_KNOWN_KEYS)}."
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
def _as_str_dict(obj: Any, where: str) -> dict[str, str]:
|
|
497
|
+
if not isinstance(obj, dict):
|
|
498
|
+
raise _ArgError(f"{where} must be an object of name: value strings.")
|
|
499
|
+
return {str(k): str(v) for k, v in obj.items()}
|
|
500
|
+
|
|
501
|
+
dataset_aux = _as_str_dict(data.get("AUXDATASET", {}), "'AUXDATASET'")
|
|
502
|
+
|
|
503
|
+
zone_groups: list[tuple[str | None, dict[str, str]]] = []
|
|
504
|
+
for key, val in data.get("AUXZONE", {}).items():
|
|
505
|
+
target = None if str(key).lower() == "all" else str(key)
|
|
506
|
+
zone_groups.append((target, _as_str_dict(val, f"'AUXZONE.{key}'")))
|
|
507
|
+
|
|
508
|
+
var_groups: list[tuple[str | None, dict[str, str]]] = []
|
|
509
|
+
for key, val in data.get("AUXVAR", {}).items():
|
|
510
|
+
target = None if str(key).lower() == "all" else str(key)
|
|
511
|
+
var_groups.append((target, _as_str_dict(val, f"'AUXVAR.{key}'")))
|
|
512
|
+
|
|
513
|
+
return dataset_aux, zone_groups, var_groups
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def _collect_all_aux(reader: Any) -> dict[str, Any]:
|
|
517
|
+
"""Collect every dataset-, zone-, and variable-level aux entry from *reader*.
|
|
518
|
+
|
|
519
|
+
The exact inverse of ``_load_json_aux``: produces the same
|
|
520
|
+
``AUXDATASET``/``AUXZONE``/``AUXVAR`` structure, so a file exported with
|
|
521
|
+
``--export-json`` can be fed straight back in with ``-j`` (to the same
|
|
522
|
+
file, a modified copy, or an entirely different one) without any
|
|
523
|
+
reshaping. Zones/variables are keyed by their exact 1-based index --
|
|
524
|
+
never collapsed into an ``"all"`` entry even when every zone happens to
|
|
525
|
+
share the same aux content, since a later zone added to the file
|
|
526
|
+
wouldn't have had that entry originally and shouldn't silently inherit
|
|
527
|
+
it on a subsequent import.
|
|
528
|
+
|
|
529
|
+
A zone or variable with no aux entries at all is omitted entirely
|
|
530
|
+
(sparse), not written as an empty object -- matching how a missing key
|
|
531
|
+
on the *input* side already means "nothing to apply here".
|
|
532
|
+
|
|
533
|
+
Args:
|
|
534
|
+
reader: An open ``Read`` instance.
|
|
535
|
+
|
|
536
|
+
Returns:
|
|
537
|
+
A dict with up to three top-level keys (``AUXDATASET``, ``AUXZONE``,
|
|
538
|
+
``AUXVAR``); a level with nothing to export is omitted entirely, so
|
|
539
|
+
a file with no aux data anywhere yields ``{}``.
|
|
540
|
+
|
|
541
|
+
"""
|
|
542
|
+
result: dict[str, Any] = {}
|
|
543
|
+
|
|
544
|
+
dataset_aux = dict(reader.auxdata.items())
|
|
545
|
+
if dataset_aux:
|
|
546
|
+
result["AUXDATASET"] = dataset_aux
|
|
547
|
+
|
|
548
|
+
zone_aux: dict[str, dict[str, str]] = {}
|
|
549
|
+
for i, zone in enumerate(reader.zone):
|
|
550
|
+
entries = dict(zone.auxdata.items())
|
|
551
|
+
if entries:
|
|
552
|
+
zone_aux[str(i + 1)] = entries
|
|
553
|
+
if zone_aux:
|
|
554
|
+
result["AUXZONE"] = zone_aux
|
|
555
|
+
|
|
556
|
+
var_aux: dict[str, dict[str, str]] = {}
|
|
557
|
+
for i in range(reader.num_vars):
|
|
558
|
+
entries = dict(reader.get_var_auxdata(i + 1).items())
|
|
559
|
+
if entries:
|
|
560
|
+
var_aux[str(i + 1)] = entries
|
|
561
|
+
if var_aux:
|
|
562
|
+
result["AUXVAR"] = var_aux
|
|
563
|
+
|
|
564
|
+
return result
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
# --------------------------------------------------------------------------------------
|
|
568
|
+
# Per-zone processing
|
|
569
|
+
# --------------------------------------------------------------------------------------
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def _process_zone(
|
|
573
|
+
zone: Any,
|
|
574
|
+
) -> tuple[list[np.ndarray], list[Any], list[bool], list[int], dict[str, str]]:
|
|
575
|
+
"""Copy one zone's variable data/metadata verbatim.
|
|
576
|
+
|
|
577
|
+
Sharing references are passed through unchanged: this tool writes every zone in the
|
|
578
|
+
same order as the source, so source zone N is always output zone N.
|
|
579
|
+
|
|
580
|
+
Returns:
|
|
581
|
+
A 5-tuple: ``(writer_data, writer_locs, passive_vars, var_sharing,
|
|
582
|
+
existing_aux)``, where the first two are filtered to active, non-shared
|
|
583
|
+
variables only.
|
|
584
|
+
"""
|
|
585
|
+
active_data: list[np.ndarray] = []
|
|
586
|
+
active_locs: list[Any] = []
|
|
587
|
+
passive_vars: list[bool] = []
|
|
588
|
+
var_sharing: list[int] = []
|
|
589
|
+
|
|
590
|
+
for var in zone.variable:
|
|
591
|
+
is_passive = var.is_passive()
|
|
592
|
+
sv = var.shared_zone # 1-based source zone index, or None
|
|
593
|
+
share_int = sv if sv is not None else 0
|
|
594
|
+
|
|
595
|
+
passive_vars.append(is_passive)
|
|
596
|
+
var_sharing.append(share_int)
|
|
597
|
+
active_locs.append(var.value_location)
|
|
598
|
+
|
|
599
|
+
if is_passive or share_int != 0:
|
|
600
|
+
active_data.append(np.array([], dtype=np.float32))
|
|
601
|
+
continue
|
|
602
|
+
|
|
603
|
+
arr = var.values
|
|
604
|
+
if arr is None or arr.size == 0:
|
|
605
|
+
passive_vars[-1] = True
|
|
606
|
+
active_data.append(np.array([], dtype=np.float32))
|
|
607
|
+
else:
|
|
608
|
+
active_data.append(arr)
|
|
609
|
+
|
|
610
|
+
writer_data = [
|
|
611
|
+
arr
|
|
612
|
+
for arr, is_p, sv in zip(active_data, passive_vars, var_sharing, strict=False)
|
|
613
|
+
if not is_p and sv == 0
|
|
614
|
+
]
|
|
615
|
+
writer_locs = [
|
|
616
|
+
loc
|
|
617
|
+
for loc, is_p, sv in zip(active_locs, passive_vars, var_sharing, strict=False)
|
|
618
|
+
if not is_p and sv == 0
|
|
619
|
+
]
|
|
620
|
+
|
|
621
|
+
existing_aux: dict[str, str] = {}
|
|
622
|
+
if len(zone.auxdata) > 0:
|
|
623
|
+
existing_aux = dict(zone.auxdata.items())
|
|
624
|
+
|
|
625
|
+
return writer_data, writer_locs, passive_vars, var_sharing, existing_aux
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
def _write_zone_data(
|
|
629
|
+
writer: Any,
|
|
630
|
+
zone: Any,
|
|
631
|
+
writer_data: list[np.ndarray],
|
|
632
|
+
writer_locs: list[Any],
|
|
633
|
+
passive_vars: list[bool],
|
|
634
|
+
var_sharing: list[int],
|
|
635
|
+
zone_aux: dict[str, str] | None,
|
|
636
|
+
) -> None:
|
|
637
|
+
"""Perform the actual write zone call for one already processed zone.
|
|
638
|
+
|
|
639
|
+
Shared between add-mode (``main()``) and strip/export-mode
|
|
640
|
+
(``_run_strip_or_export()``) -- the only thing that differs between them
|
|
641
|
+
is what *zone_aux* is (a merged dict, or ``None`` when stripping), so
|
|
642
|
+
this is the one piece worth keeping in exactly one place rather than
|
|
643
|
+
risking the ``con_sharing``/``node_map`` handling drifting between two
|
|
644
|
+
copies.
|
|
645
|
+
|
|
646
|
+
Args:
|
|
647
|
+
writer: Open ``Write`` instance for the destination file.
|
|
648
|
+
zone: Source ``ReadZone`` being copied.
|
|
649
|
+
writer_data: Active, non-shared variable arrays (from ``_process_zone``).
|
|
650
|
+
writer_locs: Matching value locations (from ``_process_zone``).
|
|
651
|
+
passive_vars: Per-variable passive flags, dataset order.
|
|
652
|
+
var_sharing: Per-variable sharing, dataset order.
|
|
653
|
+
zone_aux: Zone-level aux to write, or ``None`` for none at all.
|
|
654
|
+
|
|
655
|
+
"""
|
|
656
|
+
zt = zone.zone_type
|
|
657
|
+
common_kw: dict[str, Any] = dict(
|
|
658
|
+
title=zone.title,
|
|
659
|
+
value_locations=writer_locs,
|
|
660
|
+
passive_vars=passive_vars,
|
|
661
|
+
var_sharing=var_sharing,
|
|
662
|
+
solution_time=zone.solution_time,
|
|
663
|
+
strand_id=zone.strand_id,
|
|
664
|
+
aux=zone_aux,
|
|
665
|
+
)
|
|
666
|
+
|
|
667
|
+
if zt == ZoneType.ORDERED:
|
|
668
|
+
writer.write_ijk_zone(data=writer_data, **common_kw)
|
|
669
|
+
else:
|
|
670
|
+
con_sharing = zone.shared_connectivity
|
|
671
|
+
writer.write_fe_zone(
|
|
672
|
+
zone_type=zt,
|
|
673
|
+
data=writer_data,
|
|
674
|
+
node_map=None if con_sharing else zone.node_map,
|
|
675
|
+
con_sharing=con_sharing,
|
|
676
|
+
**common_kw,
|
|
677
|
+
)
|
|
678
|
+
|
|
679
|
+
|
|
680
|
+
# --------------------------------------------------------------------------------------
|
|
681
|
+
# Main entry point
|
|
682
|
+
# --------------------------------------------------------------------------------------
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
def _run_strip_or_export(args: argparse.Namespace, src: Path) -> int:
|
|
686
|
+
"""Handle ``--strip``/``--export-json``, independently or together.
|
|
687
|
+
|
|
688
|
+
Args:
|
|
689
|
+
args: Parsed arguments; ``args.strip`` and/or ``args.export_json``
|
|
690
|
+
is ``True`` (``main()`` only calls this when at least one is).
|
|
691
|
+
src: Validated-to-exist input file path.
|
|
692
|
+
|
|
693
|
+
Returns:
|
|
694
|
+
Exit code -- ``0`` on success, ``1`` on error.
|
|
695
|
+
|
|
696
|
+
"""
|
|
697
|
+
if args.output is not None and args.export_json and not args.strip:
|
|
698
|
+
print(
|
|
699
|
+
"Warning: -o is ignored by --export-json alone -- the export "
|
|
700
|
+
f"always goes to {src.stem}_aux.json.",
|
|
701
|
+
file=sys.stderr,
|
|
702
|
+
)
|
|
703
|
+
|
|
704
|
+
json_dst = src.with_name(f"{src.stem}_aux.json")
|
|
705
|
+
strip_dst = (
|
|
706
|
+
Path(args.output)
|
|
707
|
+
if args.output is not None
|
|
708
|
+
else src.with_stem(src.stem + "_no_aux")
|
|
709
|
+
)
|
|
710
|
+
|
|
711
|
+
if args.export_json and json_dst.exists() and not args.force:
|
|
712
|
+
print(
|
|
713
|
+
f"Error: output file already exists: {json_dst}\nUse --force to overwrite.",
|
|
714
|
+
file=sys.stderr,
|
|
715
|
+
)
|
|
716
|
+
return 1
|
|
717
|
+
if args.strip and strip_dst.exists() and not args.force:
|
|
718
|
+
print(
|
|
719
|
+
(
|
|
720
|
+
f"Error: output file already exists: {strip_dst}\n"
|
|
721
|
+
"Use --force to overwrite."
|
|
722
|
+
),
|
|
723
|
+
file=sys.stderr,
|
|
724
|
+
)
|
|
725
|
+
return 1
|
|
726
|
+
|
|
727
|
+
try:
|
|
728
|
+
with tecio_open(str(src), "r") as reader:
|
|
729
|
+
if args.export_json:
|
|
730
|
+
aux = _collect_all_aux(reader)
|
|
731
|
+
with open(json_dst, "w", encoding="utf-8") as fh:
|
|
732
|
+
json.dump(aux, fh, indent=2)
|
|
733
|
+
print(f"Exported auxiliary data: {src} -> {json_dst}")
|
|
734
|
+
|
|
735
|
+
if args.strip:
|
|
736
|
+
print(f"Stripping auxiliary data: {src} -> {strip_dst}")
|
|
737
|
+
with tecio_open(
|
|
738
|
+
str(strip_dst),
|
|
739
|
+
"w",
|
|
740
|
+
title=reader.title,
|
|
741
|
+
variables=reader.variables,
|
|
742
|
+
file_type=reader.file_type,
|
|
743
|
+
) as writer:
|
|
744
|
+
for i, zone in enumerate(reader.zone):
|
|
745
|
+
zt = zone.zone_type
|
|
746
|
+
if zt in _FE_POLY:
|
|
747
|
+
print(
|
|
748
|
+
f"Warning: zone {i + 1} ('{zone.title}') is "
|
|
749
|
+
f"{zt.name} and cannot be copied -- skipping.",
|
|
750
|
+
file=sys.stderr,
|
|
751
|
+
)
|
|
752
|
+
continue
|
|
753
|
+
|
|
754
|
+
writer_data, writer_locs, passive_vars, var_sharing, _ = (
|
|
755
|
+
_process_zone(zone)
|
|
756
|
+
)
|
|
757
|
+
# No dataset/var aux is ever added to the writer, and zone_aux
|
|
758
|
+
# is unconditionally None here.
|
|
759
|
+
_write_zone_data(
|
|
760
|
+
writer,
|
|
761
|
+
zone,
|
|
762
|
+
writer_data,
|
|
763
|
+
writer_locs,
|
|
764
|
+
passive_vars,
|
|
765
|
+
var_sharing,
|
|
766
|
+
None,
|
|
767
|
+
)
|
|
768
|
+
print(f"Done. Stripped output written to: {strip_dst}")
|
|
769
|
+
|
|
770
|
+
except Exception as exc: # noqa: BLE001
|
|
771
|
+
print(f"Error: {exc}", file=sys.stderr)
|
|
772
|
+
if args.strip:
|
|
773
|
+
strip_dst.unlink(missing_ok=True)
|
|
774
|
+
if args.export_json:
|
|
775
|
+
json_dst.unlink(missing_ok=True)
|
|
776
|
+
return 1
|
|
777
|
+
|
|
778
|
+
return 0
|
|
779
|
+
|
|
780
|
+
|
|
781
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
782
|
+
"""Add, remove, or export auxiliary data to a Tecplot file.
|
|
783
|
+
|
|
784
|
+
Returns:
|
|
785
|
+
Exit code -- ``0`` on success, ``1`` on error.
|
|
786
|
+
|
|
787
|
+
"""
|
|
788
|
+
args = _parse_args(argv)
|
|
789
|
+
|
|
790
|
+
src = Path(args.filename)
|
|
791
|
+
if not src.exists():
|
|
792
|
+
print(f"Error: input file not found: {src}", file=sys.stderr)
|
|
793
|
+
return 1
|
|
794
|
+
|
|
795
|
+
if args.strip or args.export_json:
|
|
796
|
+
add_flags_given = bool(args.data or args.zone or args.var or args.json)
|
|
797
|
+
if add_flags_given:
|
|
798
|
+
print(
|
|
799
|
+
"Error: --strip/--export-json cannot be combined with "
|
|
800
|
+
"-d/-z/-v/-j -- run them as separate calls.",
|
|
801
|
+
file=sys.stderr,
|
|
802
|
+
)
|
|
803
|
+
return 1
|
|
804
|
+
return _run_strip_or_export(args, src)
|
|
805
|
+
|
|
806
|
+
dst = (
|
|
807
|
+
Path(args.output)
|
|
808
|
+
if args.output is not None
|
|
809
|
+
else src.with_stem(src.stem + "_aux")
|
|
810
|
+
)
|
|
811
|
+
|
|
812
|
+
if dst.exists() and not args.force:
|
|
813
|
+
print(
|
|
814
|
+
f"Error: output file already exists: {dst}\nUse --force to overwrite.",
|
|
815
|
+
file=sys.stderr,
|
|
816
|
+
)
|
|
817
|
+
return 1
|
|
818
|
+
|
|
819
|
+
try:
|
|
820
|
+
# JSON is the baseline; -d/-z/-v groups are appended after it, so they naturally
|
|
821
|
+
# win on a key collision.
|
|
822
|
+
dataset_aux: dict[str, str] = {}
|
|
823
|
+
zone_groups: list[tuple[str | None, dict[str, str]]] = []
|
|
824
|
+
var_groups: list[tuple[str | None, dict[str, str]]] = []
|
|
825
|
+
|
|
826
|
+
if args.json is not None:
|
|
827
|
+
j_dataset, j_zones, j_vars = _load_json_aux(args.json)
|
|
828
|
+
dataset_aux.update(j_dataset)
|
|
829
|
+
zone_groups.extend(j_zones)
|
|
830
|
+
var_groups.extend(j_vars)
|
|
831
|
+
|
|
832
|
+
for kv in args.data or []:
|
|
833
|
+
k, v = _parse_kv(kv)
|
|
834
|
+
dataset_aux[k] = v
|
|
835
|
+
zone_groups.extend(_to_groups(args.zone))
|
|
836
|
+
var_groups.extend(_to_groups(args.var))
|
|
837
|
+
except _ArgError as exc:
|
|
838
|
+
print(f"Error: {exc}", file=sys.stderr)
|
|
839
|
+
return 1
|
|
840
|
+
|
|
841
|
+
if not (dataset_aux or zone_groups or var_groups):
|
|
842
|
+
print(
|
|
843
|
+
"Warning: no -d, -z, -v, or -j given -- output will be a verbatim copy.",
|
|
844
|
+
file=sys.stderr,
|
|
845
|
+
)
|
|
846
|
+
|
|
847
|
+
try:
|
|
848
|
+
with tecio_open(str(src), "r") as reader:
|
|
849
|
+
var_names: list[str] = reader.variables
|
|
850
|
+
num_vars: int = reader.num_vars
|
|
851
|
+
num_zones: int = reader.num_zones
|
|
852
|
+
|
|
853
|
+
def _resolve_zone_target(raw: str) -> int:
|
|
854
|
+
try:
|
|
855
|
+
idx = int(raw)
|
|
856
|
+
except ValueError as exc:
|
|
857
|
+
raise _ArgError(
|
|
858
|
+
f"Zone target must be an index or 'all', got: {raw!r}"
|
|
859
|
+
) from exc
|
|
860
|
+
if idx < 1 or idx > num_zones:
|
|
861
|
+
raise _ArgError(f"Zone index {idx} out of range [1, {num_zones}].")
|
|
862
|
+
return idx
|
|
863
|
+
|
|
864
|
+
def _resolve_var_target(raw: str) -> int:
|
|
865
|
+
idx0 = _resolve_variable(raw, var_names)
|
|
866
|
+
if idx0 is None:
|
|
867
|
+
raise _ArgError(
|
|
868
|
+
f"Could not resolve variable target {raw!r}. "
|
|
869
|
+
f"Available: {var_names}"
|
|
870
|
+
)
|
|
871
|
+
return idx0 + 1 # keyed 1-based, matching zone targets
|
|
872
|
+
|
|
873
|
+
try:
|
|
874
|
+
zone_broadcast, zone_by_target = _consolidate_groups(
|
|
875
|
+
zone_groups, _resolve_zone_target
|
|
876
|
+
)
|
|
877
|
+
var_broadcast, var_by_target = _consolidate_groups(
|
|
878
|
+
var_groups, _resolve_var_target
|
|
879
|
+
)
|
|
880
|
+
except _ArgError as exc:
|
|
881
|
+
print(f"Error: {exc}", file=sys.stderr)
|
|
882
|
+
return 1
|
|
883
|
+
|
|
884
|
+
print(f"Adding auxiliary data: {src} -> {dst}")
|
|
885
|
+
if dataset_aux:
|
|
886
|
+
print(f" Dataset : {dataset_aux}")
|
|
887
|
+
for target, pairs in zone_groups:
|
|
888
|
+
where = f"zone {target}" if target is not None else "every zone"
|
|
889
|
+
print(f" Zone : {pairs} ({where})")
|
|
890
|
+
for target, pairs in var_groups:
|
|
891
|
+
where = f"variable {target}" if target is not None else "every variable"
|
|
892
|
+
print(f" Variable: {pairs} ({where})")
|
|
893
|
+
|
|
894
|
+
with tecio_open(
|
|
895
|
+
str(dst),
|
|
896
|
+
"w",
|
|
897
|
+
title=reader.title,
|
|
898
|
+
variables=var_names,
|
|
899
|
+
file_type=reader.file_type,
|
|
900
|
+
) as writer:
|
|
901
|
+
merged_dataset_aux = {**dict(reader.auxdata.items()), **dataset_aux}
|
|
902
|
+
if merged_dataset_aux:
|
|
903
|
+
writer.add_auxdataset_dict(merged_dataset_aux)
|
|
904
|
+
|
|
905
|
+
auxvar: dict[int, dict[str, str]] = {}
|
|
906
|
+
for i in range(num_vars):
|
|
907
|
+
one_based = i + 1
|
|
908
|
+
existing = dict(reader.get_var_auxdata(one_based).items())
|
|
909
|
+
merged = {
|
|
910
|
+
**existing,
|
|
911
|
+
**var_broadcast,
|
|
912
|
+
**var_by_target.get(one_based, {}),
|
|
913
|
+
}
|
|
914
|
+
if merged:
|
|
915
|
+
auxvar[one_based] = merged
|
|
916
|
+
if auxvar:
|
|
917
|
+
writer.add_auxvar_dict(auxvar)
|
|
918
|
+
|
|
919
|
+
# add_auxdataset_dict()/add_auxvar_dict() only buffer; the write happens
|
|
920
|
+
# in flush_aux(), normally auto-triggered by the *lazy*-open path on the
|
|
921
|
+
# first zone write. Passing variables= above means this writer is
|
|
922
|
+
# already open (eager), so that automatic trigger never fires.
|
|
923
|
+
writer.flush_aux()
|
|
924
|
+
|
|
925
|
+
for i, zone in enumerate(reader.zone):
|
|
926
|
+
zone_num = i + 1
|
|
927
|
+
zt = zone.zone_type
|
|
928
|
+
|
|
929
|
+
if zt in _FE_POLY:
|
|
930
|
+
print(
|
|
931
|
+
f"Warning: zone {zone_num} ('{zone.title}') is "
|
|
932
|
+
f"{zt.name} and cannot be copied -- skipping.",
|
|
933
|
+
file=sys.stderr,
|
|
934
|
+
)
|
|
935
|
+
continue
|
|
936
|
+
|
|
937
|
+
(
|
|
938
|
+
writer_data,
|
|
939
|
+
writer_locs,
|
|
940
|
+
passive_vars,
|
|
941
|
+
var_sharing,
|
|
942
|
+
existing_zone_aux,
|
|
943
|
+
) = _process_zone(zone)
|
|
944
|
+
|
|
945
|
+
merged_zone_aux = {
|
|
946
|
+
**existing_zone_aux,
|
|
947
|
+
**zone_broadcast,
|
|
948
|
+
**zone_by_target.get(zone_num, {}),
|
|
949
|
+
} or None
|
|
950
|
+
|
|
951
|
+
_write_zone_data(
|
|
952
|
+
writer,
|
|
953
|
+
zone,
|
|
954
|
+
writer_data,
|
|
955
|
+
writer_locs,
|
|
956
|
+
passive_vars,
|
|
957
|
+
var_sharing,
|
|
958
|
+
merged_zone_aux,
|
|
959
|
+
)
|
|
960
|
+
|
|
961
|
+
except Exception as exc: # noqa: BLE001
|
|
962
|
+
print(f"Error: {exc}", file=sys.stderr)
|
|
963
|
+
dst.unlink(missing_ok=True)
|
|
964
|
+
return 1
|
|
965
|
+
|
|
966
|
+
print(f"Done. Output written to: {dst}")
|
|
967
|
+
return 0
|
|
968
|
+
|
|
969
|
+
|
|
970
|
+
if __name__ == "__main__":
|
|
971
|
+
sys.exit(main())
|