nonos-cli 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nonos_cli/__init__.py +618 -0
- nonos_cli/__main__.py +6 -0
- nonos_cli/config.py +50 -0
- nonos_cli/logging.py +43 -0
- nonos_cli/logo.txt +23 -0
- nonos_cli/parsing.py +142 -0
- nonos_cli-0.1.0.dist-info/METADATA +237 -0
- nonos_cli-0.1.0.dist-info/RECORD +11 -0
- nonos_cli-0.1.0.dist-info/WHEEL +4 -0
- nonos_cli-0.1.0.dist-info/entry_points.txt +3 -0
- nonos_cli-0.1.0.dist-info/licenses/LICENSE +674 -0
nonos_cli/__init__.py
ADDED
|
@@ -0,0 +1,618 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""
|
|
3
|
+
Visualization tool for idefix/pluto/fargo3d (M)HD simulations of protoplanetary disks
|
|
4
|
+
"""
|
|
5
|
+
# adapted from pbenitez-llambay, gwafflard-fernandez, cmt robert & glesur
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
import functools
|
|
9
|
+
import importlib.resources as importlib_resources
|
|
10
|
+
import os
|
|
11
|
+
import re
|
|
12
|
+
import sys
|
|
13
|
+
import time
|
|
14
|
+
from collections import ChainMap
|
|
15
|
+
from importlib import import_module
|
|
16
|
+
from importlib.metadata import version
|
|
17
|
+
from importlib.util import find_spec
|
|
18
|
+
from multiprocessing import Pool
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import TYPE_CHECKING, Any
|
|
21
|
+
|
|
22
|
+
import inifix
|
|
23
|
+
import numpy as np
|
|
24
|
+
from tqdm import tqdm
|
|
25
|
+
|
|
26
|
+
from nonos.api import GasDataSet
|
|
27
|
+
from nonos.api._angle_parsing import _parse_planet_file
|
|
28
|
+
from nonos.loaders import loader_from
|
|
29
|
+
from nonos.styling import set_mpl_style
|
|
30
|
+
from nonos_cli.config import DEFAULTS
|
|
31
|
+
from nonos_cli.logging import configure_logger, logger, parse_verbose_level
|
|
32
|
+
from nonos_cli.parsing import (
|
|
33
|
+
is_set,
|
|
34
|
+
parse_image_format,
|
|
35
|
+
parse_output_number_range,
|
|
36
|
+
parse_range,
|
|
37
|
+
range_converter,
|
|
38
|
+
userval_or_default,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
if TYPE_CHECKING:
|
|
42
|
+
from typing import Literal
|
|
43
|
+
|
|
44
|
+
from matplotlib.backend_bases import FigureCanvasBase
|
|
45
|
+
from matplotlib.figure import Figure
|
|
46
|
+
|
|
47
|
+
LIB_VERSION = version("nonos")
|
|
48
|
+
CLI_VERSION = version("nonos-cli")
|
|
49
|
+
|
|
50
|
+
KNOWN_CMAP_PACKAGE_PREFIXES = {
|
|
51
|
+
"cb": "cblind",
|
|
52
|
+
"cmo": "cmocean",
|
|
53
|
+
"cmr": "cmasher",
|
|
54
|
+
"cmyt": "cmyt",
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def get_non_interactive_figure(fmt: str) -> "Figure":
|
|
59
|
+
from matplotlib.backends.backend_agg import FigureCanvasAgg
|
|
60
|
+
from matplotlib.backends.backend_pdf import FigureCanvasPdf
|
|
61
|
+
from matplotlib.backends.backend_ps import FigureCanvasPS
|
|
62
|
+
from matplotlib.backends.backend_svg import FigureCanvasSVG
|
|
63
|
+
from matplotlib.figure import Figure
|
|
64
|
+
|
|
65
|
+
FigureCanvas: type[FigureCanvasBase]
|
|
66
|
+
|
|
67
|
+
if fmt in ["png", "jpg", "jpeg", "raw", "rgba", "tif", "tiff"]:
|
|
68
|
+
FigureCanvas = FigureCanvasAgg
|
|
69
|
+
elif fmt == "pdf":
|
|
70
|
+
FigureCanvas = FigureCanvasPdf
|
|
71
|
+
elif fmt in ["ps", "eps"]:
|
|
72
|
+
FigureCanvas = FigureCanvasPS
|
|
73
|
+
elif fmt == "svg":
|
|
74
|
+
FigureCanvas = FigureCanvasSVG
|
|
75
|
+
else:
|
|
76
|
+
raise ValueError(f"unknown file format {fmt}")
|
|
77
|
+
|
|
78
|
+
fig = Figure()
|
|
79
|
+
FigureCanvas(fig)
|
|
80
|
+
return fig
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# process function for parallelisation purpose with progress bar
|
|
84
|
+
# counterParallel = Value('i', 0) # initialization of a counter
|
|
85
|
+
def process_field(
|
|
86
|
+
on,
|
|
87
|
+
operations: list[str],
|
|
88
|
+
field,
|
|
89
|
+
plane,
|
|
90
|
+
geometry,
|
|
91
|
+
diff,
|
|
92
|
+
log,
|
|
93
|
+
planet_file: str | None,
|
|
94
|
+
extent: 'Literal["unset"] | tuple[str, str] | tuple[str, str, str, str]',
|
|
95
|
+
vmin,
|
|
96
|
+
vmax,
|
|
97
|
+
scaling: float,
|
|
98
|
+
cmap: str,
|
|
99
|
+
title,
|
|
100
|
+
unit_conversion: int,
|
|
101
|
+
datadir,
|
|
102
|
+
show: bool,
|
|
103
|
+
dpi: int,
|
|
104
|
+
fmt: str,
|
|
105
|
+
theta,
|
|
106
|
+
z,
|
|
107
|
+
phi,
|
|
108
|
+
distance,
|
|
109
|
+
*,
|
|
110
|
+
log_level,
|
|
111
|
+
):
|
|
112
|
+
configure_logger(level=log_level)
|
|
113
|
+
set_mpl_style(scaling=scaling)
|
|
114
|
+
if geometry == "unset":
|
|
115
|
+
geometry = None
|
|
116
|
+
ds = GasDataSet(on, geometry=geometry, directory=datadir)
|
|
117
|
+
dsop = ds[field]
|
|
118
|
+
if diff:
|
|
119
|
+
dsop = dsop.diff(0)
|
|
120
|
+
if "vm" in operations:
|
|
121
|
+
dsop = dsop.vertical_at_midplane()
|
|
122
|
+
elif "vp" in operations:
|
|
123
|
+
dsop = dsop.vertical_projection(z=z)
|
|
124
|
+
elif "lt" in operations:
|
|
125
|
+
dsop = dsop.latitudinal_at_theta(theta=theta)
|
|
126
|
+
elif "lp" in operations:
|
|
127
|
+
dsop = dsop.latitudinal_projection(theta=theta)
|
|
128
|
+
elif "vz" in operations:
|
|
129
|
+
dsop = dsop.vertical_at_z(z=z)
|
|
130
|
+
|
|
131
|
+
if "ap" in operations:
|
|
132
|
+
dsop = dsop.azimuthal_at_phi(phi=phi)
|
|
133
|
+
elif "apl" in operations:
|
|
134
|
+
dsop = dsop.azimuthal_at_planet(planet_file=planet_file)
|
|
135
|
+
elif "aa" in operations:
|
|
136
|
+
dsop = dsop.azimuthal_average()
|
|
137
|
+
|
|
138
|
+
if "rr" in operations:
|
|
139
|
+
dsop = dsop.radial_at_r(distance=distance)
|
|
140
|
+
|
|
141
|
+
logger.debug("operations performed: {}", operations)
|
|
142
|
+
|
|
143
|
+
dim = 3 - dsop.shape.count(1)
|
|
144
|
+
logger.debug("plotting a {}D plot.", dim)
|
|
145
|
+
|
|
146
|
+
if plane is None:
|
|
147
|
+
dsop_dict = dsop.coords.to_dict()
|
|
148
|
+
default_plane = []
|
|
149
|
+
for key, val in dsop_dict.items():
|
|
150
|
+
if not isinstance(val, str) and val.shape[0] > 2:
|
|
151
|
+
default_plane.append(key)
|
|
152
|
+
# default_plane = ["x","y"]
|
|
153
|
+
plane = default_plane
|
|
154
|
+
|
|
155
|
+
if show:
|
|
156
|
+
import matplotlib.pyplot as plt
|
|
157
|
+
|
|
158
|
+
fig = plt.figure()
|
|
159
|
+
else:
|
|
160
|
+
fig = get_non_interactive_figure(fmt)
|
|
161
|
+
|
|
162
|
+
plot_kwargs = {
|
|
163
|
+
"log": log,
|
|
164
|
+
"vmin": vmin,
|
|
165
|
+
"vmax": vmax,
|
|
166
|
+
"cmap": cmap,
|
|
167
|
+
"title": f"${title}$",
|
|
168
|
+
"unit_conversion": unit_conversion,
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if cm_prefix := cmap.rpartition(".")[0]:
|
|
172
|
+
if (cm_package := KNOWN_CMAP_PACKAGE_PREFIXES.get(cm_prefix)) is None:
|
|
173
|
+
logger.warning(
|
|
174
|
+
"requested colormap {cmap!r} with the unknown prefix {cm_prefix!r}. "
|
|
175
|
+
"The default colormap will be used instead.",
|
|
176
|
+
cmap=cmap,
|
|
177
|
+
cm_prefix=cm_prefix,
|
|
178
|
+
)
|
|
179
|
+
plot_kwargs.pop("cmap")
|
|
180
|
+
elif not find_spec(cm_package):
|
|
181
|
+
logger.warning(
|
|
182
|
+
"requested colormap {cmap!r}, but {cm_package} is not installed. "
|
|
183
|
+
"The default colormap will be used instead.",
|
|
184
|
+
cmap=cmap,
|
|
185
|
+
cm_package=cm_package,
|
|
186
|
+
)
|
|
187
|
+
plot_kwargs.pop("cmap")
|
|
188
|
+
else:
|
|
189
|
+
# all known colormap packages work by registering their colormaps
|
|
190
|
+
# at import time.
|
|
191
|
+
import_module(cm_package)
|
|
192
|
+
|
|
193
|
+
ax = fig.add_subplot(111, polar=False)
|
|
194
|
+
if dim == 1:
|
|
195
|
+
if "cmap" in plot_kwargs:
|
|
196
|
+
plot_kwargs.pop("cmap")
|
|
197
|
+
|
|
198
|
+
plotable = dsop.map(plane[0], rotate_with=planet_file)
|
|
199
|
+
plotable.plot(fig, ax, **plot_kwargs)
|
|
200
|
+
avalue = plotable.abscissa.data
|
|
201
|
+
# mypy doesn't narrow int to Literal[1] (it should)
|
|
202
|
+
extent_parsed = parse_range(extent, dim=dim) # type: ignore [call-overload]
|
|
203
|
+
extent_parsed = range_converter(
|
|
204
|
+
extent_parsed,
|
|
205
|
+
abscissa=avalue,
|
|
206
|
+
ordinate=np.zeros(2),
|
|
207
|
+
)
|
|
208
|
+
ax.set_xlim(extent_parsed[0], extent_parsed[1])
|
|
209
|
+
elif dim == 2:
|
|
210
|
+
dsop.map(plane[0], plane[1], rotate_with=planet_file).plot(
|
|
211
|
+
fig, ax, **plot_kwargs
|
|
212
|
+
)
|
|
213
|
+
plotable = dsop.map(plane[0], plane[1], rotate_with=planet_file)
|
|
214
|
+
avalue = plotable.abscissa.data
|
|
215
|
+
ovalue = plotable.ordinate.data
|
|
216
|
+
# mypy doesn't narrow int to Literal[2] (it should)
|
|
217
|
+
extent_parsed = parse_range(extent, dim=dim) # type: ignore [call-overload]
|
|
218
|
+
extent_parsed = range_converter(
|
|
219
|
+
extent_parsed,
|
|
220
|
+
abscissa=avalue,
|
|
221
|
+
ordinate=ovalue,
|
|
222
|
+
)
|
|
223
|
+
ax.set_xlim(extent_parsed[0], extent_parsed[1])
|
|
224
|
+
ax.set_ylim(extent_parsed[2], extent_parsed[3])
|
|
225
|
+
else:
|
|
226
|
+
raise ValueError("Got {dim=}, expected 1 or 2")
|
|
227
|
+
|
|
228
|
+
logger.debug("processed the data before plotting.")
|
|
229
|
+
|
|
230
|
+
if "x" and "y" in plane:
|
|
231
|
+
ax.set_aspect("equal")
|
|
232
|
+
|
|
233
|
+
if show:
|
|
234
|
+
import matplotlib.pyplot as plt
|
|
235
|
+
|
|
236
|
+
plt.show()
|
|
237
|
+
plt.close(fig)
|
|
238
|
+
else:
|
|
239
|
+
logger.debug("saving plot: started")
|
|
240
|
+
filename = f"{''.join(plane)}_{field}_{'_'.join(operations)}{'_diff' if diff else '_'}{'_log' if log else ''}{on:04d}.{fmt}"
|
|
241
|
+
fig.savefig(filename, bbox_inches="tight", dpi=dpi)
|
|
242
|
+
logger.debug("saving plot: finished ({})", filename)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def get_parser() -> argparse.ArgumentParser:
|
|
246
|
+
parser = argparse.ArgumentParser(
|
|
247
|
+
prog="nonos",
|
|
248
|
+
description=__doc__,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
# TODO: pass this as kwarg when support for Python 3.13 is dropped
|
|
252
|
+
# https://docs.python.org/3.14/library/argparse.html#suggest-on-error
|
|
253
|
+
parser.suggest_on_error = True # type: ignore [attr-defined]
|
|
254
|
+
|
|
255
|
+
parser.add_argument(
|
|
256
|
+
"-dir",
|
|
257
|
+
dest="datadir",
|
|
258
|
+
help=f"location of output files and param files (default: '{DEFAULTS['datadir']}').",
|
|
259
|
+
)
|
|
260
|
+
parser.add_argument(
|
|
261
|
+
"-field",
|
|
262
|
+
# choices=["RHO", "VX1", "VX2", "VX3", "BX1", "BX2", "BX3", "PRS"],
|
|
263
|
+
help=f"name of field to plot (default: '{DEFAULTS['field']}').",
|
|
264
|
+
)
|
|
265
|
+
parser.add_argument(
|
|
266
|
+
"-geometry",
|
|
267
|
+
type=str,
|
|
268
|
+
choices=["polar", "cylindrical", "spherical", "cartesian"],
|
|
269
|
+
help=f"if the geometry of idefix outputs is not recognized (default: '{DEFAULTS['geometry']}').",
|
|
270
|
+
)
|
|
271
|
+
parser.add_argument(
|
|
272
|
+
"-operation",
|
|
273
|
+
type=str,
|
|
274
|
+
nargs="+",
|
|
275
|
+
choices=["vm", "vp", "vz", "lt", "lp", "aa", "ap", "apl", "rr"],
|
|
276
|
+
help=f"operation to apply to the fild (default: '{DEFAULTS['operation']}').",
|
|
277
|
+
)
|
|
278
|
+
parser.add_argument(
|
|
279
|
+
"-plane",
|
|
280
|
+
type=str,
|
|
281
|
+
nargs="+",
|
|
282
|
+
help=f"abscissa and ordinate of the plane of projection (default: '{DEFAULTS['plane']}'), example: r phi",
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
# TODO(GWF): add support for -rotate_by -1.2453036989845032 (idefix_newvtk_planet2d)
|
|
286
|
+
parser.add_argument(
|
|
287
|
+
"-corotate",
|
|
288
|
+
type=int,
|
|
289
|
+
default=None,
|
|
290
|
+
help="planet number that defines with which planet the grid corotates.",
|
|
291
|
+
)
|
|
292
|
+
parser.add_argument(
|
|
293
|
+
"-range",
|
|
294
|
+
type=str,
|
|
295
|
+
nargs="+",
|
|
296
|
+
help=f"range of matplotlib window (default: {DEFAULTS['range']}), example: x x -2 2",
|
|
297
|
+
)
|
|
298
|
+
parser.add_argument(
|
|
299
|
+
"-vmin",
|
|
300
|
+
type=float,
|
|
301
|
+
help=f"min value (default: {DEFAULTS['vmin']})",
|
|
302
|
+
)
|
|
303
|
+
parser.add_argument(
|
|
304
|
+
"-vmax",
|
|
305
|
+
type=float,
|
|
306
|
+
help=f"max value (default: {DEFAULTS['vmax']})",
|
|
307
|
+
)
|
|
308
|
+
parser.add_argument(
|
|
309
|
+
"-theta",
|
|
310
|
+
type=float,
|
|
311
|
+
help=f"if latitudinal operation (default: {DEFAULTS['theta']})",
|
|
312
|
+
)
|
|
313
|
+
parser.add_argument(
|
|
314
|
+
"-z",
|
|
315
|
+
type=float,
|
|
316
|
+
help=f"if vertical operation (default: {DEFAULTS['z']})",
|
|
317
|
+
)
|
|
318
|
+
parser.add_argument(
|
|
319
|
+
"-phi",
|
|
320
|
+
type=float,
|
|
321
|
+
help=f"if azimuthal operation (default: {DEFAULTS['phi']})",
|
|
322
|
+
)
|
|
323
|
+
parser.add_argument(
|
|
324
|
+
"-distance",
|
|
325
|
+
type=float,
|
|
326
|
+
help=f"if radial operation (default: {DEFAULTS['distance']})",
|
|
327
|
+
)
|
|
328
|
+
parser.add_argument(
|
|
329
|
+
"-cpu",
|
|
330
|
+
"-ncpu",
|
|
331
|
+
dest="ncpu",
|
|
332
|
+
type=int,
|
|
333
|
+
help=f"number of parallel processes (default: {DEFAULTS['ncpu']}).",
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
select_group = parser.add_mutually_exclusive_group()
|
|
337
|
+
select_group.add_argument(
|
|
338
|
+
"-on",
|
|
339
|
+
type=int,
|
|
340
|
+
nargs="+",
|
|
341
|
+
help="output number(s) (on) to plot. "
|
|
342
|
+
"This can be a single value or a range (start, end, [step]) where both ends are inclusive. "
|
|
343
|
+
"(default: last output available).",
|
|
344
|
+
)
|
|
345
|
+
select_group.add_argument(
|
|
346
|
+
"-all",
|
|
347
|
+
action="store_true",
|
|
348
|
+
help="save an image for every available snapshot (this will force show=False).",
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
# boolean flags use False as a default value (...by default)
|
|
352
|
+
# forcing them to None instead will allow them to be pruned
|
|
353
|
+
# when we build the ChainMap config
|
|
354
|
+
flag_group = parser.add_argument_group("boolean flags")
|
|
355
|
+
flag_group.add_argument(
|
|
356
|
+
"-diff",
|
|
357
|
+
action="store_true",
|
|
358
|
+
default=None,
|
|
359
|
+
help="plot the relative perturbation of the field f, i.e. (f-f0)/f0.",
|
|
360
|
+
)
|
|
361
|
+
flag_group.add_argument(
|
|
362
|
+
"-log",
|
|
363
|
+
action="store_true",
|
|
364
|
+
default=None,
|
|
365
|
+
help="plot the log10 of the field f, i.e. log(f).",
|
|
366
|
+
)
|
|
367
|
+
flag_group.add_argument(
|
|
368
|
+
"-pbar",
|
|
369
|
+
dest="progressBar",
|
|
370
|
+
action="store_true",
|
|
371
|
+
default=None,
|
|
372
|
+
help="display a progress bar",
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
parser.add_argument(
|
|
376
|
+
"-scaling",
|
|
377
|
+
dest="scaling",
|
|
378
|
+
type=float,
|
|
379
|
+
help=f"scale the overall sizes of features in the graph (fonts, linewidth...) (default: {DEFAULTS['scaling']}).",
|
|
380
|
+
)
|
|
381
|
+
parser.add_argument(
|
|
382
|
+
"-cmap",
|
|
383
|
+
help=f"choice of colormap for the 2D maps (default: '{DEFAULTS['cmap']}').",
|
|
384
|
+
)
|
|
385
|
+
parser.add_argument(
|
|
386
|
+
"-title",
|
|
387
|
+
type=str,
|
|
388
|
+
help=f"name of the field in the colorbar for the 2D maps (default: '{DEFAULTS['title']}').",
|
|
389
|
+
)
|
|
390
|
+
parser.add_argument(
|
|
391
|
+
"-uc",
|
|
392
|
+
"-unit_conversion",
|
|
393
|
+
dest="unit_conversion",
|
|
394
|
+
type=float,
|
|
395
|
+
help=f"conversion factor for the considered quantity (default: '{DEFAULTS['unit_conversion']}').",
|
|
396
|
+
)
|
|
397
|
+
parser.add_argument(
|
|
398
|
+
"-fmt",
|
|
399
|
+
"-format",
|
|
400
|
+
dest="format",
|
|
401
|
+
help=f"select output image file format (default: {DEFAULTS['format']})",
|
|
402
|
+
)
|
|
403
|
+
parser.add_argument(
|
|
404
|
+
"-dpi",
|
|
405
|
+
type=int,
|
|
406
|
+
help=f"image file resolution (default: {DEFAULTS['dpi']})",
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
cli_only_group = parser.add_argument_group("CLI-only options")
|
|
410
|
+
cli_input_group = cli_only_group.add_mutually_exclusive_group()
|
|
411
|
+
cli_input_group.add_argument(
|
|
412
|
+
"-input", "-i", dest="input", type=str, help="specify a configuration file."
|
|
413
|
+
)
|
|
414
|
+
cli_input_group.add_argument(
|
|
415
|
+
"-isolated", action="store_true", help="ignore any existing 'nonos.ini' file."
|
|
416
|
+
)
|
|
417
|
+
cli_action_group = cli_only_group.add_mutually_exclusive_group()
|
|
418
|
+
cli_action_group.add_argument(
|
|
419
|
+
"-d",
|
|
420
|
+
"-display",
|
|
421
|
+
dest="display",
|
|
422
|
+
action="store_true",
|
|
423
|
+
help="open a graphic window with the plot (only works with a single image)",
|
|
424
|
+
)
|
|
425
|
+
cli_action_group.add_argument(
|
|
426
|
+
"-version",
|
|
427
|
+
"--version",
|
|
428
|
+
action="store_true",
|
|
429
|
+
help="show raw version number and exit",
|
|
430
|
+
)
|
|
431
|
+
cli_action_group.add_argument(
|
|
432
|
+
"-logo",
|
|
433
|
+
action="store_true",
|
|
434
|
+
help="show Nonos logo with version number, and exit.",
|
|
435
|
+
)
|
|
436
|
+
cli_action_group.add_argument(
|
|
437
|
+
"-config", action="store_true", help="show configuration and exit."
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
cli_debug_group = cli_only_group.add_mutually_exclusive_group()
|
|
441
|
+
cli_debug_group.add_argument(
|
|
442
|
+
"-v",
|
|
443
|
+
"-verbose",
|
|
444
|
+
"--verbose",
|
|
445
|
+
action="count",
|
|
446
|
+
default=0,
|
|
447
|
+
help="increase output verbosity (-v: info, -vv: debug).",
|
|
448
|
+
)
|
|
449
|
+
return parser
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def main(argv: list[str] | None = None) -> int:
|
|
453
|
+
parser = get_parser()
|
|
454
|
+
clargs = vars(parser.parse_args(argv))
|
|
455
|
+
|
|
456
|
+
# special cases: destructively consume CLI-only arguments with dict.pop
|
|
457
|
+
|
|
458
|
+
if clargs.pop("logo"):
|
|
459
|
+
logo = importlib_resources.files("nonos_cli").joinpath("logo.txt").read_text()
|
|
460
|
+
print(f"{logo}{__doc__}")
|
|
461
|
+
print(f"nonos-cli {CLI_VERSION}")
|
|
462
|
+
print(f"nonos {LIB_VERSION}")
|
|
463
|
+
return 0
|
|
464
|
+
|
|
465
|
+
if clargs.pop("version"):
|
|
466
|
+
print(f"nonos-cli {CLI_VERSION}")
|
|
467
|
+
print(f"nonos {LIB_VERSION}")
|
|
468
|
+
return 0
|
|
469
|
+
|
|
470
|
+
# clargs.pop("verbose")
|
|
471
|
+
level = parse_verbose_level(clargs.pop("verbose"))
|
|
472
|
+
configure_logger(level=level)
|
|
473
|
+
|
|
474
|
+
if clargs.pop("isolated"):
|
|
475
|
+
config_file_args: dict[str, Any] = {}
|
|
476
|
+
elif (ifile := clargs.pop("input")) is not None:
|
|
477
|
+
if not Path(ifile).is_file():
|
|
478
|
+
logger.error("Couldn't find requested input file {!r}.", ifile)
|
|
479
|
+
return 1
|
|
480
|
+
logger.warning("Using parameters from {!r}.", ifile)
|
|
481
|
+
config_file_args = inifix.load(ifile)
|
|
482
|
+
elif Path("nonos.ini").is_file():
|
|
483
|
+
logger.warning("Using parameters from 'nonos.ini'.")
|
|
484
|
+
config_file_args = inifix.load("nonos.ini")
|
|
485
|
+
else:
|
|
486
|
+
config_file_args = {}
|
|
487
|
+
|
|
488
|
+
# check that every parameter in the configuration is also exposed to the CLI
|
|
489
|
+
assert not set(DEFAULTS).difference(set(clargs))
|
|
490
|
+
|
|
491
|
+
# squeeze out any unset value form cli config to leave room for file parameters
|
|
492
|
+
clargs = {k: v for k, v in clargs.items() if v is not None}
|
|
493
|
+
|
|
494
|
+
# NOTE: init.config is also a ChainMap instance with a default layer
|
|
495
|
+
# this may be seen either as hyperstatism (good thing) or error prone redundancy (bad thing)
|
|
496
|
+
args = ChainMap(clargs, config_file_args, DEFAULTS)
|
|
497
|
+
if clargs.pop("config"):
|
|
498
|
+
conf_repr = {}
|
|
499
|
+
for key in DEFAULTS:
|
|
500
|
+
conf_repr[key] = args[key]
|
|
501
|
+
print(f"# Generated with nonos {LIB_VERSION} + nonos-cli {CLI_VERSION}")
|
|
502
|
+
s = inifix.dumps(conf_repr)
|
|
503
|
+
print(inifix.format_string(s))
|
|
504
|
+
return 0
|
|
505
|
+
|
|
506
|
+
try:
|
|
507
|
+
loader = loader_from(directory=args["datadir"])
|
|
508
|
+
except (FileNotFoundError, RuntimeError, ValueError) as exc:
|
|
509
|
+
logger.error("{}", exc)
|
|
510
|
+
return 1
|
|
511
|
+
|
|
512
|
+
data_files = loader.binary_reader.get_bin_files(args["datadir"])
|
|
513
|
+
|
|
514
|
+
available = set()
|
|
515
|
+
for fn in data_files:
|
|
516
|
+
if (num := re.search(r"\d+", fn.name)) is not None:
|
|
517
|
+
available.add(int(num.group()))
|
|
518
|
+
|
|
519
|
+
if args.pop("all"):
|
|
520
|
+
requested = available
|
|
521
|
+
else:
|
|
522
|
+
try:
|
|
523
|
+
requested = set(
|
|
524
|
+
parse_output_number_range(args["on"], maxval=max(available))
|
|
525
|
+
)
|
|
526
|
+
except ValueError as exc:
|
|
527
|
+
logger.error("{}", exc)
|
|
528
|
+
return 1
|
|
529
|
+
|
|
530
|
+
if not (toplot := list(requested.intersection(available))):
|
|
531
|
+
logger.error(
|
|
532
|
+
"No requested output file was found (requested {}, found {}).",
|
|
533
|
+
requested,
|
|
534
|
+
available,
|
|
535
|
+
)
|
|
536
|
+
return 1
|
|
537
|
+
args["on"] = toplot
|
|
538
|
+
|
|
539
|
+
if (show := clargs.pop("display")) and len(args["on"]) > 1:
|
|
540
|
+
logger.warning(
|
|
541
|
+
"display mode can not be used with multiple images, turning it off."
|
|
542
|
+
)
|
|
543
|
+
show = False
|
|
544
|
+
|
|
545
|
+
if not show:
|
|
546
|
+
try:
|
|
547
|
+
args["format"] = parse_image_format(args["format"])
|
|
548
|
+
except ValueError as exc:
|
|
549
|
+
logger.error("{}", exc)
|
|
550
|
+
return 1
|
|
551
|
+
|
|
552
|
+
# check that every CLI-only argument was consumed at this point
|
|
553
|
+
assert not set(clargs).difference(set(DEFAULTS))
|
|
554
|
+
|
|
555
|
+
args["field"] = args["field"].upper()
|
|
556
|
+
extent = args["range"]
|
|
557
|
+
|
|
558
|
+
if args["ncpu"] > (ncpu := min(args["ncpu"], os.cpu_count())):
|
|
559
|
+
logger.warning(
|
|
560
|
+
"Requested {args_ncpu}, but the runner only has access to {ncpu}.",
|
|
561
|
+
args_ncpu=args["ncpu"],
|
|
562
|
+
ncpu=ncpu,
|
|
563
|
+
)
|
|
564
|
+
|
|
565
|
+
planet_file: str | None
|
|
566
|
+
if not is_set(args["corotate"]):
|
|
567
|
+
planet_file = None
|
|
568
|
+
else:
|
|
569
|
+
planet_file = _parse_planet_file(planet_number=args["corotate"])
|
|
570
|
+
|
|
571
|
+
# call of the process_field function, whether it be in parallel or not
|
|
572
|
+
# TODO: reduce this to the bare minimum
|
|
573
|
+
func_kwargs = dict( # noqa: C408
|
|
574
|
+
operations=userval_or_default(args["operation"], default=["vm"]),
|
|
575
|
+
field=args["field"],
|
|
576
|
+
plane=userval_or_default(args["plane"], default=None),
|
|
577
|
+
geometry=userval_or_default(args["geometry"], default="unset"),
|
|
578
|
+
diff=args["diff"],
|
|
579
|
+
log=args["log"],
|
|
580
|
+
planet_file=planet_file,
|
|
581
|
+
extent=extent,
|
|
582
|
+
vmin=userval_or_default(args["vmin"], default=None),
|
|
583
|
+
vmax=userval_or_default(args["vmax"], default=None),
|
|
584
|
+
scaling=args["scaling"],
|
|
585
|
+
cmap=args["cmap"],
|
|
586
|
+
title=userval_or_default(args["title"], default=args["field"]),
|
|
587
|
+
unit_conversion=args["unit_conversion"],
|
|
588
|
+
datadir=args["datadir"],
|
|
589
|
+
show=show,
|
|
590
|
+
dpi=args["dpi"],
|
|
591
|
+
fmt=args["format"],
|
|
592
|
+
theta=userval_or_default(args["theta"], default=None),
|
|
593
|
+
z=userval_or_default(args["z"], default=None),
|
|
594
|
+
phi=userval_or_default(args["phi"], default=None),
|
|
595
|
+
distance=userval_or_default(args["distance"], default=None),
|
|
596
|
+
log_level=level,
|
|
597
|
+
)
|
|
598
|
+
|
|
599
|
+
progress = functools.partial(
|
|
600
|
+
tqdm if args["progressBar"] else lambda it, *_arg, **_kwargs: it,
|
|
601
|
+
desc="Processing snapshots",
|
|
602
|
+
total=len(args["on"]),
|
|
603
|
+
file=sys.stdout,
|
|
604
|
+
)
|
|
605
|
+
|
|
606
|
+
logger.info("Starting main loop")
|
|
607
|
+
tstart = time.time()
|
|
608
|
+
if ncpu == 1:
|
|
609
|
+
for on in progress(args["on"]):
|
|
610
|
+
process_field(on, **func_kwargs) # pyright: ignore[reportArgumentType]
|
|
611
|
+
else:
|
|
612
|
+
func = functools.partial(process_field, **func_kwargs)
|
|
613
|
+
with Pool(ncpu) as pool:
|
|
614
|
+
list(progress(pool.imap(func, args["on"])))
|
|
615
|
+
if not show:
|
|
616
|
+
logger.info("Operation took {:.2f}s", time.time() - tstart)
|
|
617
|
+
|
|
618
|
+
return 0
|
nonos_cli/__main__.py
ADDED
nonos_cli/config.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
DEFAULTS = {
|
|
2
|
+
# directory where the simulation results are stored
|
|
3
|
+
"datadir": ".",
|
|
4
|
+
# which field?
|
|
5
|
+
"field": "RHO",
|
|
6
|
+
# which operation?
|
|
7
|
+
"operation": "unset",
|
|
8
|
+
# if latitudinal operation
|
|
9
|
+
"theta": "unset",
|
|
10
|
+
# if vertical operation
|
|
11
|
+
"z": "unset",
|
|
12
|
+
# if azimuthal operation
|
|
13
|
+
"phi": "unset",
|
|
14
|
+
# if radial operation
|
|
15
|
+
"distance": "unset",
|
|
16
|
+
# if the geometry of idefix outputs is not recognized
|
|
17
|
+
"geometry": "unset",
|
|
18
|
+
# a single output number ('on') to display or a range (min, max, [step])
|
|
19
|
+
"on": "unset",
|
|
20
|
+
# perturbation of field
|
|
21
|
+
"diff": False,
|
|
22
|
+
# field in log
|
|
23
|
+
"log": False,
|
|
24
|
+
# default extent of the matplotlib window
|
|
25
|
+
"range": "unset",
|
|
26
|
+
# default min field
|
|
27
|
+
"vmin": "unset",
|
|
28
|
+
# default max field
|
|
29
|
+
"vmax": "unset",
|
|
30
|
+
# plane to represent (default is midplane)
|
|
31
|
+
"plane": "unset",
|
|
32
|
+
# do we display the progress (loading+plotting)
|
|
33
|
+
"progressBar": False,
|
|
34
|
+
# do the grid rotate with the planet?
|
|
35
|
+
"corotate": "unset",
|
|
36
|
+
# number of cpus to use
|
|
37
|
+
"ncpu": 1,
|
|
38
|
+
# scaling factor for font size of text in graphs (among other things)
|
|
39
|
+
"scaling": 1,
|
|
40
|
+
# choice of colormap
|
|
41
|
+
"cmap": "RdYlBu_r",
|
|
42
|
+
# name of colorbar
|
|
43
|
+
"title": "unset",
|
|
44
|
+
# conversion factor
|
|
45
|
+
"unit_conversion": 1,
|
|
46
|
+
# select image file format
|
|
47
|
+
"format": "unset",
|
|
48
|
+
# select image resolution
|
|
49
|
+
"dpi": 200,
|
|
50
|
+
}
|