py-pluto 1.1.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.
- pyPLUTO/__init__.py +22 -0
- pyPLUTO/amr.py +745 -0
- pyPLUTO/baseloadmixin.py +258 -0
- pyPLUTO/baseloadstate.py +45 -0
- pyPLUTO/codes/echo_load.py +161 -0
- pyPLUTO/configure.py +261 -0
- pyPLUTO/gui/config.py +174 -0
- pyPLUTO/gui/custom_var.py +435 -0
- pyPLUTO/gui/globals.py +108 -0
- pyPLUTO/gui/main.py +17 -0
- pyPLUTO/gui/main_window.py +177 -0
- pyPLUTO/gui/panels.py +66 -0
- pyPLUTO/gui/utils.py +273 -0
- pyPLUTO/h_pypluto.py +84 -0
- pyPLUTO/image.py +302 -0
- pyPLUTO/imagefuncs/colorbar.py +240 -0
- pyPLUTO/imagefuncs/contour.py +254 -0
- pyPLUTO/imagefuncs/create_axes.py +464 -0
- pyPLUTO/imagefuncs/display.py +306 -0
- pyPLUTO/imagefuncs/figure.py +395 -0
- pyPLUTO/imagefuncs/imagetools.py +487 -0
- pyPLUTO/imagefuncs/interactive.py +403 -0
- pyPLUTO/imagefuncs/legend.py +250 -0
- pyPLUTO/imagefuncs/plot.py +311 -0
- pyPLUTO/imagefuncs/range.py +242 -0
- pyPLUTO/imagefuncs/scatter.py +270 -0
- pyPLUTO/imagefuncs/set_axis.py +497 -0
- pyPLUTO/imagefuncs/streamplot.py +297 -0
- pyPLUTO/imagefuncs/zoom.py +428 -0
- pyPLUTO/imagemixin.py +259 -0
- pyPLUTO/imagestate.py +45 -0
- pyPLUTO/load.py +447 -0
- pyPLUTO/loadfuncs/baseloadtools.py +71 -0
- pyPLUTO/loadfuncs/codeselection.py +48 -0
- pyPLUTO/loadfuncs/defpluto.py +123 -0
- pyPLUTO/loadfuncs/descriptor.py +102 -0
- pyPLUTO/loadfuncs/findfiles.py +182 -0
- pyPLUTO/loadfuncs/findformat.py +245 -0
- pyPLUTO/loadfuncs/initload.py +203 -0
- pyPLUTO/loadfuncs/loadvars.py +227 -0
- pyPLUTO/loadfuncs/offsetdata.py +87 -0
- pyPLUTO/loadfuncs/offsetfluid.py +408 -0
- pyPLUTO/loadfuncs/read_files.py +213 -0
- pyPLUTO/loadfuncs/readdata.py +619 -0
- pyPLUTO/loadfuncs/readdata_old.py +567 -0
- pyPLUTO/loadfuncs/readdefplini.py +101 -0
- pyPLUTO/loadfuncs/readfluid.py +479 -0
- pyPLUTO/loadfuncs/readformat.py +277 -0
- pyPLUTO/loadfuncs/readgridalone.py +224 -0
- pyPLUTO/loadfuncs/readgridfile.py +255 -0
- pyPLUTO/loadfuncs/readgridout.py +451 -0
- pyPLUTO/loadfuncs/readpart.py +419 -0
- pyPLUTO/loadfuncs/readtab.py +105 -0
- pyPLUTO/loadfuncs/write_files.py +283 -0
- pyPLUTO/loadmixin.py +419 -0
- pyPLUTO/loadpart.py +233 -0
- pyPLUTO/loadstate.py +68 -0
- pyPLUTO/newload.py +81 -0
- pyPLUTO/pytools.py +145 -0
- pyPLUTO/toolfuncs/findlines.py +551 -0
- pyPLUTO/toolfuncs/fourier.py +149 -0
- pyPLUTO/toolfuncs/nabla.py +676 -0
- pyPLUTO/toolfuncs/parttools.py +152 -0
- pyPLUTO/toolfuncs/transform.py +638 -0
- pyPLUTO/utils/annotator.py +27 -0
- pyPLUTO/utils/inspector.py +145 -0
- pyPLUTO/utils/make_docstrings.py +3 -0
- py_pluto-1.1.4.dist-info/METADATA +218 -0
- py_pluto-1.1.4.dist-info/RECORD +73 -0
- py_pluto-1.1.4.dist-info/WHEEL +5 -0
- py_pluto-1.1.4.dist-info/entry_points.txt +2 -0
- py_pluto-1.1.4.dist-info/licenses/LICENSE +27 -0
- py_pluto-1.1.4.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
"""A custom variable evaluator for PyPLUTO."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
import tempfile
|
|
6
|
+
|
|
7
|
+
import numexpr as ne
|
|
8
|
+
import numpy as np
|
|
9
|
+
from PySide6.QtCore import Qt
|
|
10
|
+
from PySide6.QtWidgets import (
|
|
11
|
+
QComboBox,
|
|
12
|
+
QDialog,
|
|
13
|
+
QDialogButtonBox,
|
|
14
|
+
QFormLayout,
|
|
15
|
+
QLabel,
|
|
16
|
+
QPlainTextEdit,
|
|
17
|
+
QVBoxLayout,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
SENTINEL = "Custom var..."
|
|
21
|
+
|
|
22
|
+
# --- tiny normalizer ---------------------------------------------------------
|
|
23
|
+
# Allow both D.something and bare names; allow np./numpy. prefixes.
|
|
24
|
+
_NORM_PATTERNS = [
|
|
25
|
+
(re.compile(r"\bD\."), ""), # D.Bx1 -> Bx1
|
|
26
|
+
(re.compile(r"\bnp\."), ""), # np.sqrt -> sqrt
|
|
27
|
+
(re.compile(r"\bnumpy\."), ""), # numpy.sqrt -> sqrt
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _normalize_expr(expr: str) -> str:
|
|
32
|
+
s = expr.strip()
|
|
33
|
+
for pat, repl in _NORM_PATTERNS:
|
|
34
|
+
s = pat.sub(repl, s)
|
|
35
|
+
return s
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _frozen_var_names(D):
|
|
39
|
+
"""Names that must never be overridden by custom vars."""
|
|
40
|
+
names = set(map(str, getattr(D, "_load_vars", []))) # loaded-from-file vars
|
|
41
|
+
names.update({"x1", "x2", "x3"}) # base grid
|
|
42
|
+
g = (getattr(D, "geom", "") or "").upper()
|
|
43
|
+
if g == "CARTESIAN":
|
|
44
|
+
names.update({"x", "y", "z"})
|
|
45
|
+
elif g == "POLAR":
|
|
46
|
+
names.update({"R", "phi", "z", "x", "y", "xr", "yr"})
|
|
47
|
+
elif g == "SPHERICAL":
|
|
48
|
+
names.update({"r", "theta", "phi", "R", "z", "rt", "zt"})
|
|
49
|
+
return names
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _apply_grid_shaping(local, D):
|
|
53
|
+
"""
|
|
54
|
+
Reshape only x1/x2/x3 if they are 1-D so they broadcast with D.nshp.
|
|
55
|
+
|
|
56
|
+
PyPLUTO order: 1D -> (nx1,), 2D -> (nx1, nx2), 3D -> (nx1, nx2, nx3)
|
|
57
|
+
x1 aligns to axis 0, x2 to axis 1, x3 to axis 2.
|
|
58
|
+
"""
|
|
59
|
+
nshp = D.nshp if isinstance(D.nshp, tuple) else tuple(D.nshp)
|
|
60
|
+
for axis, name in enumerate(("x1", "x2", "x3")[: D.dim]):
|
|
61
|
+
v = local.get(name)
|
|
62
|
+
if (
|
|
63
|
+
isinstance(v, np.ndarray)
|
|
64
|
+
and v.ndim == 1
|
|
65
|
+
and v.shape[0] == nshp[axis]
|
|
66
|
+
):
|
|
67
|
+
try:
|
|
68
|
+
# make a singleton shape on other axes, then broadcast to D.nshp
|
|
69
|
+
pattern = [1] * D.dim
|
|
70
|
+
pattern[axis] = v.shape[0] # axis-aligned length
|
|
71
|
+
v_view = v.reshape(tuple(pattern))
|
|
72
|
+
local[name] = np.broadcast_to(
|
|
73
|
+
v_view, nshp
|
|
74
|
+
) # FINAL SHAPE == D.nshp
|
|
75
|
+
except Exception:
|
|
76
|
+
pass # stay quiet per your policy
|
|
77
|
+
|
|
78
|
+
IS_3D = 3
|
|
79
|
+
if D.geom == "CARTESIAN":
|
|
80
|
+
local["x"] = local["x1"]
|
|
81
|
+
local["y"] = local["x2"]
|
|
82
|
+
local["z"] = local["x3"]
|
|
83
|
+
elif D.geom == "POLAR":
|
|
84
|
+
local["R"] = local["x1"]
|
|
85
|
+
local["phi"] = local["x2"]
|
|
86
|
+
local["z"] = local["x3"]
|
|
87
|
+
local["x"] = D.x1c.T[:, :, None] if D.dim == IS_3D else D.x1c.T
|
|
88
|
+
local["y"] = D.x2c.T[:, :, None] if D.dim == IS_3D else D.x2c.T
|
|
89
|
+
elif D.geom == "SPHERICAL":
|
|
90
|
+
local["r"] = local["x1"]
|
|
91
|
+
local["theta"] = local["x2"]
|
|
92
|
+
local["phi"] = local["x3"]
|
|
93
|
+
local["R"] = D.x1p.T[:, :, None] if D.dim == IS_3D else D.x1p.T
|
|
94
|
+
local["z"] = D.x2p.T[:, :, None] if D.dim == IS_3D else D.x2p.T
|
|
95
|
+
if D.dim == 3:
|
|
96
|
+
local["rt"] = D.x1t.T[:, None, :]
|
|
97
|
+
local["zt"] = D.x3t.T[:, None, :]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# --- evaluator ---------------------------------------------------------------
|
|
101
|
+
def evaluate_custom_var(D, name: str, expr: str, *, assign: bool = True):
|
|
102
|
+
"""
|
|
103
|
+
numexpr-only, memmap output for arrays, scalar stays scalar.
|
|
104
|
+
|
|
105
|
+
Accepts D.foo or foo; np.func/numpy.func or func directly.
|
|
106
|
+
"""
|
|
107
|
+
expr = _normalize_expr(expr)
|
|
108
|
+
|
|
109
|
+
# Do not allow overriding loaded or grid variables
|
|
110
|
+
if name in _frozen_var_names(D):
|
|
111
|
+
raise ValueError(f"protected name: {name}")
|
|
112
|
+
|
|
113
|
+
# Build locals from D (public, non-callable) + constants
|
|
114
|
+
local = {"pi": float(np.pi), "e": float(np.e)}
|
|
115
|
+
for k, v in D.__dict__.items():
|
|
116
|
+
if not k.startswith("_") and not callable(v):
|
|
117
|
+
local[k] = v
|
|
118
|
+
|
|
119
|
+
_apply_grid_shaping(local, D) # <<< add this
|
|
120
|
+
|
|
121
|
+
# Parse/compile (syntax check)
|
|
122
|
+
try:
|
|
123
|
+
compiled = ne.NumExpr(expr)
|
|
124
|
+
except Exception as e:
|
|
125
|
+
raise ValueError(f"compile error: {e}")
|
|
126
|
+
|
|
127
|
+
# Validate cheaply on only the used names (1-element views)
|
|
128
|
+
names = compiled.input_names
|
|
129
|
+
tiny = {}
|
|
130
|
+
for n in names:
|
|
131
|
+
v = local.get(n)
|
|
132
|
+
if v is None:
|
|
133
|
+
raise ValueError(f"unknown name: {n}")
|
|
134
|
+
tiny[n] = v.reshape(-1)[:1] if isinstance(v, np.ndarray) else v
|
|
135
|
+
try:
|
|
136
|
+
tiny_res = ne.evaluate(expr, local_dict=tiny, global_dict={})
|
|
137
|
+
except Exception as e:
|
|
138
|
+
raise ValueError(f"validation error: {e}")
|
|
139
|
+
|
|
140
|
+
# Infer shape from arrays actually referenced (broadcasting)
|
|
141
|
+
arrs = [local[n] for n in names if isinstance(local[n], np.ndarray)]
|
|
142
|
+
if not arrs:
|
|
143
|
+
# Scalar result
|
|
144
|
+
try:
|
|
145
|
+
res = ne.evaluate(expr, local_dict=local, global_dict={})
|
|
146
|
+
except Exception as e:
|
|
147
|
+
raise ValueError(f"evaluate error: {e}")
|
|
148
|
+
if isinstance(res, np.ndarray) and res.shape == ():
|
|
149
|
+
res = res.item()
|
|
150
|
+
else:
|
|
151
|
+
out_shape = np.broadcast(
|
|
152
|
+
*[np.empty(a.shape, dtype=[]) for a in arrs]
|
|
153
|
+
).shape
|
|
154
|
+
out_dtype = getattr(tiny_res, "dtype", arrs[0].dtype)
|
|
155
|
+
# Tempfile-backed memmap keeps result on disk
|
|
156
|
+
tmp = tempfile.NamedTemporaryFile(
|
|
157
|
+
prefix=f"{name}_", suffix=".dat", delete=False
|
|
158
|
+
)
|
|
159
|
+
tmp_path = tmp.name
|
|
160
|
+
tmp.close()
|
|
161
|
+
try:
|
|
162
|
+
out = np.memmap(
|
|
163
|
+
tmp_path, mode="w+", dtype=out_dtype, shape=out_shape
|
|
164
|
+
)
|
|
165
|
+
ne.evaluate(expr, local_dict=local, global_dict={}, out=out)
|
|
166
|
+
res = out
|
|
167
|
+
except Exception as e:
|
|
168
|
+
try:
|
|
169
|
+
if "out" in locals():
|
|
170
|
+
del out
|
|
171
|
+
if os.path.exists(tmp_path):
|
|
172
|
+
os.remove(tmp_path)
|
|
173
|
+
except Exception:
|
|
174
|
+
pass
|
|
175
|
+
raise ValueError(f"evaluate error: {e}")
|
|
176
|
+
|
|
177
|
+
if assign:
|
|
178
|
+
setattr(D, name, res)
|
|
179
|
+
|
|
180
|
+
return res
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
# --- Dialog: now a single textbox with one-or-more "name = expr" lines -------
|
|
184
|
+
_LINE_RE = re.compile(r"^\s*(!?[A-Za-z_]\w*)\s*=\s*(.+?)\s*$")
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class CustomVarDialog(QDialog):
|
|
188
|
+
def __init__(self, D, parent=None):
|
|
189
|
+
super().__init__(parent)
|
|
190
|
+
self.D = D
|
|
191
|
+
self.setWindowTitle("Add Custom Variables")
|
|
192
|
+
self.setMinimumWidth(460)
|
|
193
|
+
|
|
194
|
+
self.exprs = QPlainTextEdit()
|
|
195
|
+
self.exprs.setPlaceholderText("Define variables:\nA = ...\nB = ...")
|
|
196
|
+
self.exprs.setMinimumHeight(160)
|
|
197
|
+
|
|
198
|
+
self.err = QLabel("")
|
|
199
|
+
self.err.setStyleSheet("color:#b00020;")
|
|
200
|
+
self.err.setWordWrap(True)
|
|
201
|
+
|
|
202
|
+
form = QFormLayout()
|
|
203
|
+
form.addRow("Variables:", self.exprs)
|
|
204
|
+
|
|
205
|
+
btns = QDialogButtonBox(
|
|
206
|
+
QDialogButtonBox.StandardButton.Ok
|
|
207
|
+
| QDialogButtonBox.StandardButton.Cancel
|
|
208
|
+
)
|
|
209
|
+
btns.accepted.connect(self._accept)
|
|
210
|
+
btns.rejected.connect(self.reject)
|
|
211
|
+
|
|
212
|
+
lay = QVBoxLayout(self)
|
|
213
|
+
lay.addLayout(form)
|
|
214
|
+
lay.addWidget(self.err)
|
|
215
|
+
lay.addWidget(btns)
|
|
216
|
+
|
|
217
|
+
self._pairs: list[tuple[str, str]] = []
|
|
218
|
+
self.exprs.textChanged.connect(lambda: self.err.setText(""))
|
|
219
|
+
|
|
220
|
+
def _parse_lines(self, text: str):
|
|
221
|
+
"""Return a list of tuples.
|
|
222
|
+
|
|
223
|
+
(display_name, expr_display, hidden, clean_name, expr_clean)
|
|
224
|
+
- display_name: what the user typed for the name (may start with '!')
|
|
225
|
+
- expr_display: right-hand side exactly as typed (keeps '# comment')
|
|
226
|
+
- hidden: True if name starts with '!'
|
|
227
|
+
- clean_name: display_name without leading '!' (actual Python attribute)
|
|
228
|
+
- expr_clean: expr_display with trailing comment stripped (before evaluation)
|
|
229
|
+
|
|
230
|
+
"""
|
|
231
|
+
pairs = []
|
|
232
|
+
for raw in text.splitlines():
|
|
233
|
+
if not raw.strip():
|
|
234
|
+
continue
|
|
235
|
+
m = _LINE_RE.match(raw)
|
|
236
|
+
if not m:
|
|
237
|
+
raise ValueError(f"invalid line: {raw!r} (use NAME = EXPR)")
|
|
238
|
+
display_name, expr_display = m.group(1), m.group(2).strip()
|
|
239
|
+
hidden = display_name.startswith("!")
|
|
240
|
+
clean_name = display_name[1:] if hidden else display_name
|
|
241
|
+
expr_clean = expr_display.split("#", 1)[
|
|
242
|
+
0
|
|
243
|
+
].strip() # ignore comments when evaluating
|
|
244
|
+
if not expr_clean:
|
|
245
|
+
raise ValueError(
|
|
246
|
+
f"empty expression after comment stripping in line: {raw!r}"
|
|
247
|
+
)
|
|
248
|
+
pairs.append(
|
|
249
|
+
(display_name, expr_display, hidden, clean_name, expr_clean)
|
|
250
|
+
)
|
|
251
|
+
if not pairs:
|
|
252
|
+
raise ValueError("Please enter at least one 'NAME = EXPR' line.")
|
|
253
|
+
return pairs
|
|
254
|
+
|
|
255
|
+
def _accept(self):
|
|
256
|
+
text = self.exprs.toPlainText()
|
|
257
|
+
try:
|
|
258
|
+
pairs = self._parse_lines(text)
|
|
259
|
+
# validate sequentially using clean_name / expr_clean
|
|
260
|
+
seq = [(p[3], p[4]) for p in pairs] # (clean_name, expr_clean)
|
|
261
|
+
_validate_lines_sequential(self.D, seq)
|
|
262
|
+
except Exception as ex:
|
|
263
|
+
self.err.setText(f"Invalid definitions: {ex}")
|
|
264
|
+
return
|
|
265
|
+
self._pairs = pairs
|
|
266
|
+
self.accept()
|
|
267
|
+
|
|
268
|
+
@property
|
|
269
|
+
def values(self):
|
|
270
|
+
# Backwards-compatible name (used by _on_activated)
|
|
271
|
+
return self._pairs
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def setup_var_selector(combo: QComboBox, D):
|
|
275
|
+
"""Set up a QComboBox to handle custom variable creation and re-apply."""
|
|
276
|
+
if combo.property("_cv_connected"):
|
|
277
|
+
# still re-apply on each load
|
|
278
|
+
_reapply_custom_vars(combo, D)
|
|
279
|
+
return
|
|
280
|
+
combo.setProperty("_cv_connected", True)
|
|
281
|
+
combo.setProperty("_last", 0 if combo.count() else -1)
|
|
282
|
+
if combo.property("_cv_defs") is None:
|
|
283
|
+
combo.setProperty("_cv_defs", []) # list[(name, expr)]
|
|
284
|
+
combo.activated[int].connect(lambda i: _on_activated(combo, i, D))
|
|
285
|
+
_reapply_custom_vars(combo, D)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _build_locals(D):
|
|
289
|
+
local = {"pi": float(np.pi), "e": float(np.e)}
|
|
290
|
+
for k, v in D.__dict__.items():
|
|
291
|
+
if not k.startswith("_") and not callable(v):
|
|
292
|
+
local[k] = v
|
|
293
|
+
return local
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _validate_lines_sequential(D, pairs):
|
|
297
|
+
"""Cheap, sequential validation using 1-element array views."""
|
|
298
|
+
base = _build_locals(D)
|
|
299
|
+
_apply_grid_shaping(base, D) # <<< add this
|
|
300
|
+
# tiny env: scalars unchanged, arrays -> 1 element
|
|
301
|
+
tiny = {
|
|
302
|
+
k: (v.reshape(-1)[:1] if isinstance(v, np.ndarray) else v)
|
|
303
|
+
for k, v in base.items()
|
|
304
|
+
}
|
|
305
|
+
tiny["pi"], tiny["e"] = float(np.pi), float(np.e)
|
|
306
|
+
for name, expr in pairs:
|
|
307
|
+
s = _normalize_expr(expr)
|
|
308
|
+
compiled = ne.NumExpr(s)
|
|
309
|
+
names = compiled.input_names
|
|
310
|
+
# ensure all symbols exist
|
|
311
|
+
env = {}
|
|
312
|
+
for n in names:
|
|
313
|
+
if n not in tiny:
|
|
314
|
+
raise ValueError(f"unknown name: {n}")
|
|
315
|
+
env[n] = tiny[n]
|
|
316
|
+
if name in _frozen_var_names(D):
|
|
317
|
+
raise ValueError(
|
|
318
|
+
f"'{name}' is protected and cannot be redefined"
|
|
319
|
+
)
|
|
320
|
+
try:
|
|
321
|
+
res = ne.evaluate(s, local_dict=env, global_dict={})
|
|
322
|
+
except Exception as e:
|
|
323
|
+
raise ValueError(f"error in '{name} = {expr}': {e}")
|
|
324
|
+
# store the tiny result for subsequent lines
|
|
325
|
+
tiny[name] = res
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _on_activated(combo: QComboBox, idx: int, D):
|
|
329
|
+
# Always use the live D from the main window if available
|
|
330
|
+
D = getattr(combo.window(), "D", D)
|
|
331
|
+
|
|
332
|
+
if combo.itemText(idx) != SENTINEL:
|
|
333
|
+
combo.setProperty("_last", idx)
|
|
334
|
+
return
|
|
335
|
+
last = combo.property("_last")
|
|
336
|
+
if last is not None and last >= 0:
|
|
337
|
+
combo.setCurrentIndex(last)
|
|
338
|
+
dlg = CustomVarDialog(D, combo.window())
|
|
339
|
+
if dlg.exec() != QDialog.DialogCode.Accepted:
|
|
340
|
+
return
|
|
341
|
+
|
|
342
|
+
# tuples: (display_name, expr_display, hidden, clean_name, expr_clean)
|
|
343
|
+
pairs = dlg.values or []
|
|
344
|
+
stored = list(combo.property("_cv_defs") or [])
|
|
345
|
+
|
|
346
|
+
for display_name, expr_display, hidden, clean_name, expr_clean in pairs:
|
|
347
|
+
# evaluate & assign on D using clean values
|
|
348
|
+
try:
|
|
349
|
+
evaluate_custom_var(D, clean_name, expr_clean, assign=True)
|
|
350
|
+
except Exception:
|
|
351
|
+
continue # silent skip per requirement
|
|
352
|
+
|
|
353
|
+
# add to combo only if not hidden
|
|
354
|
+
if not hidden:
|
|
355
|
+
# duplicate? select existing
|
|
356
|
+
for i in range(combo.count()):
|
|
357
|
+
if combo.itemText(i).lower() == clean_name.lower():
|
|
358
|
+
combo.setCurrentIndex(i)
|
|
359
|
+
combo.setProperty("_last", i)
|
|
360
|
+
break
|
|
361
|
+
else:
|
|
362
|
+
# insert before sentinel
|
|
363
|
+
sent = next(
|
|
364
|
+
(
|
|
365
|
+
i
|
|
366
|
+
for i in range(combo.count())
|
|
367
|
+
if combo.itemText(i) == SENTINEL
|
|
368
|
+
),
|
|
369
|
+
-1,
|
|
370
|
+
)
|
|
371
|
+
pos = sent if sent != -1 else combo.count()
|
|
372
|
+
combo.insertItem(pos, clean_name)
|
|
373
|
+
combo.setItemData(
|
|
374
|
+
pos, expr_clean, role=Qt.ItemDataRole.UserRole
|
|
375
|
+
)
|
|
376
|
+
combo.setCurrentIndex(pos)
|
|
377
|
+
combo.setProperty("_last", pos)
|
|
378
|
+
|
|
379
|
+
# store triple so we can reapply (expr_clean) and display comments (expr_display)
|
|
380
|
+
stored.append((display_name, expr_clean, expr_display))
|
|
381
|
+
|
|
382
|
+
combo.setProperty("_cv_defs", stored)
|
|
383
|
+
|
|
384
|
+
# Refresh info panel if present (shows the display expr with comments)
|
|
385
|
+
top = combo.window()
|
|
386
|
+
if hasattr(top, "info_label") and stored:
|
|
387
|
+
lines = []
|
|
388
|
+
for tup in stored:
|
|
389
|
+
if len(tup) == 3:
|
|
390
|
+
disp_name, _clean, disp_expr = tup
|
|
391
|
+
lines.append(f"{disp_name} = {disp_expr}")
|
|
392
|
+
else:
|
|
393
|
+
# backward-compat if older pairs (name, expr)
|
|
394
|
+
n, e = tup
|
|
395
|
+
lines.append(f"{n} = {e}")
|
|
396
|
+
old_text = top.info_label.toPlainText()
|
|
397
|
+
base = old_text.split("\n\nCustom variables:")[0]
|
|
398
|
+
top.info_label.setPlainText(
|
|
399
|
+
f"{base}\n\nCustom variables:\n" + "\n".join(lines)
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def _reapply_custom_vars(combo: QComboBox, D):
|
|
404
|
+
"""Recreate previously defined session custom vars on the new D, silently skipping failures."""
|
|
405
|
+
defs = list(combo.property("_cv_defs") or [])
|
|
406
|
+
if not defs:
|
|
407
|
+
return
|
|
408
|
+
for item in defs:
|
|
409
|
+
# Support both (name, expr) legacy pairs and (display_name, expr_clean, expr_display) triples
|
|
410
|
+
if len(item) == 3:
|
|
411
|
+
display_name, expr_clean, _expr_display = item
|
|
412
|
+
else:
|
|
413
|
+
display_name, expr_clean = item
|
|
414
|
+
hidden = display_name.startswith("!")
|
|
415
|
+
clean_name = display_name[1:] if hidden else display_name
|
|
416
|
+
|
|
417
|
+
try:
|
|
418
|
+
evaluate_custom_var(D, clean_name, expr_clean, assign=True)
|
|
419
|
+
except Exception:
|
|
420
|
+
continue # silent skip
|
|
421
|
+
|
|
422
|
+
if not hidden and not any(
|
|
423
|
+
combo.itemText(i) == clean_name for i in range(combo.count())
|
|
424
|
+
):
|
|
425
|
+
sent = next(
|
|
426
|
+
(
|
|
427
|
+
i
|
|
428
|
+
for i in range(combo.count())
|
|
429
|
+
if combo.itemText(i) == SENTINEL
|
|
430
|
+
),
|
|
431
|
+
-1,
|
|
432
|
+
)
|
|
433
|
+
pos = sent if sent != -1 else combo.count()
|
|
434
|
+
combo.insertItem(pos, clean_name)
|
|
435
|
+
combo.setItemData(pos, expr_clean, role=Qt.ItemDataRole.UserRole)
|
pyPLUTO/gui/globals.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Contains global variables for the GUI."""
|
|
2
|
+
|
|
3
|
+
import matplotlib.scale as mscale
|
|
4
|
+
from matplotlib import colormaps as cmaps
|
|
5
|
+
|
|
6
|
+
scale = list(mscale.get_scale_names())
|
|
7
|
+
scales = [scale[i] for i in [3, 4, 6, 0]]
|
|
8
|
+
vscales = ["linear", "log", "symlog", "2slope", "power", "asinh"]
|
|
9
|
+
|
|
10
|
+
cmaps_list = list(cmaps)
|
|
11
|
+
cmaps_avail0 = [cmaps_list[2], *cmaps_list[0:2], *cmaps_list[3:83]]
|
|
12
|
+
cmaps_avail = [cmap for cmap in cmaps_avail0 if cmap in cmaps_list]
|
|
13
|
+
|
|
14
|
+
cmaps_divided = {
|
|
15
|
+
"All": cmaps_avail,
|
|
16
|
+
"Uniform": ["plasma", "viridis", "inferno", "magma", "cividis"],
|
|
17
|
+
"Sequential": [
|
|
18
|
+
"Greys",
|
|
19
|
+
"Purples",
|
|
20
|
+
"Blues",
|
|
21
|
+
"Greens",
|
|
22
|
+
"Oranges",
|
|
23
|
+
"Reds",
|
|
24
|
+
"YlOrBr",
|
|
25
|
+
"YlOrRd",
|
|
26
|
+
"OrRd",
|
|
27
|
+
"PuRd",
|
|
28
|
+
"RdPu",
|
|
29
|
+
"BuPu",
|
|
30
|
+
"GnBu",
|
|
31
|
+
"PuBu",
|
|
32
|
+
"YlGnBu",
|
|
33
|
+
"PuBuGn",
|
|
34
|
+
"BuGn",
|
|
35
|
+
"YlGn",
|
|
36
|
+
],
|
|
37
|
+
"Sequential (2)": [
|
|
38
|
+
"binary",
|
|
39
|
+
"gist_yarg",
|
|
40
|
+
"gist_gray",
|
|
41
|
+
"gray",
|
|
42
|
+
"bone",
|
|
43
|
+
"pink",
|
|
44
|
+
"spring",
|
|
45
|
+
"summer",
|
|
46
|
+
"autumn",
|
|
47
|
+
"winter",
|
|
48
|
+
"cool",
|
|
49
|
+
"Wistia",
|
|
50
|
+
"hot",
|
|
51
|
+
"afmhot",
|
|
52
|
+
"gist_heat",
|
|
53
|
+
"copper",
|
|
54
|
+
],
|
|
55
|
+
"Diverging": [
|
|
56
|
+
"PiYG",
|
|
57
|
+
"PRGn",
|
|
58
|
+
"BrBG",
|
|
59
|
+
"PuOr",
|
|
60
|
+
"RdGy",
|
|
61
|
+
"RdBu",
|
|
62
|
+
"RdYlBu",
|
|
63
|
+
"RdYlGn",
|
|
64
|
+
"Spectral",
|
|
65
|
+
"coolwarm",
|
|
66
|
+
"bwr",
|
|
67
|
+
"seismic",
|
|
68
|
+
"berlin",
|
|
69
|
+
"managua",
|
|
70
|
+
"vanimo",
|
|
71
|
+
],
|
|
72
|
+
"Cyclic": ["twilight", "twilight_shifted", "hsv"],
|
|
73
|
+
"Qualitative": [
|
|
74
|
+
"Pastel1",
|
|
75
|
+
"Pastel2",
|
|
76
|
+
"Paired",
|
|
77
|
+
"Accent",
|
|
78
|
+
"Dark2",
|
|
79
|
+
"Set1",
|
|
80
|
+
"Set2",
|
|
81
|
+
"Set3",
|
|
82
|
+
"tab10",
|
|
83
|
+
"tab20",
|
|
84
|
+
"tab20b",
|
|
85
|
+
"tab20c",
|
|
86
|
+
],
|
|
87
|
+
"Miscellaneous": [
|
|
88
|
+
"flag",
|
|
89
|
+
"prism",
|
|
90
|
+
"ocean",
|
|
91
|
+
"gist_earth",
|
|
92
|
+
"terrain",
|
|
93
|
+
"gist_stern",
|
|
94
|
+
"gnuplot",
|
|
95
|
+
"gnuplot2",
|
|
96
|
+
"CMRmap",
|
|
97
|
+
"cubehelix",
|
|
98
|
+
"brg",
|
|
99
|
+
"gist_rainbow",
|
|
100
|
+
"rainbow",
|
|
101
|
+
"jet",
|
|
102
|
+
"turbo",
|
|
103
|
+
"nipy_spectral",
|
|
104
|
+
"gist_ncar",
|
|
105
|
+
],
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
format_avail = ["None", "dbl", "flt", "vtk", "dbl.h5", "flt.h5", "hdf5", "tab"]
|
pyPLUTO/gui/main.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
|
|
3
|
+
from PySide6.QtWidgets import QApplication
|
|
4
|
+
|
|
5
|
+
from .main_window import PyPLUTOApp
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def main():
|
|
9
|
+
app = QApplication(sys.argv)
|
|
10
|
+
window = PyPLUTOApp(code="PLUTO")
|
|
11
|
+
window.resize(1150, 720)
|
|
12
|
+
window.show()
|
|
13
|
+
sys.exit(app.exec())
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
if __name__ == "__main__":
|
|
17
|
+
main()
|