PyMHD 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.
- pymhd/__init__.py +31 -0
- pymhd/derivatives/TENO.py +278 -0
- pymhd/derivatives/WENO.py +323 -0
- pymhd/derivatives/__init__.py +24 -0
- pymhd/derivatives/compact.py +365 -0
- pymhd/derivatives/derivative.py +926 -0
- pymhd/numdiss.py +598 -0
- pymhd/plot/__init__.py +48 -0
- pymhd/plot/nd.py +1519 -0
- pymhd/plot/slc.py +648 -0
- pymhd/plot/spc.py +249 -0
- pymhd/preprocess/Athena.py +847 -0
- pymhd/preprocess/__init__.py +69 -0
- pymhd/preprocess/helper/NOTICE +42 -0
- pymhd/preprocess/helper/bin_convert.py +2000 -0
- pymhd/preprocess/helper/make_athdf.py +45 -0
- pymhd/spectra.py +376 -0
- pymhd/turbulence.py +917 -0
- pymhd-0.1.0.dist-info/METADATA +100 -0
- pymhd-0.1.0.dist-info/RECORD +22 -0
- pymhd-0.1.0.dist-info/WHEEL +4 -0
- pymhd-0.1.0.dist-info/licenses/LICENSE +21 -0
pymhd/plot/nd.py
ADDED
|
@@ -0,0 +1,1519 @@
|
|
|
1
|
+
# PyMHD: Python for Magnetohydrodynamic Turbulence.
|
|
2
|
+
# Copyright (c) 2026 Yuyang Hua (华宇阳)
|
|
3
|
+
# License: MIT
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
pymhd/plot/nd.py
|
|
7
|
+
----------------
|
|
8
|
+
|
|
9
|
+
Plotting tools for numerical dissipation analysis in (M)HD turbulence, including:
|
|
10
|
+
- 2D slices of numerical and physical dissipation terms
|
|
11
|
+
- Numerical and physical dissipation spectra
|
|
12
|
+
|
|
13
|
+
For 2D slices, similar to pymhd/plot/slc.py, currently:
|
|
14
|
+
- Shearing-box simulations: the box ratio is hard coded to be Lx : Ly : Lz = 2 : 4 : 1
|
|
15
|
+
- Forced turbulence: the box ratio is hard coded to be Lx : Ly : Lz = 1 : 1 : 1
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import numpy as np
|
|
19
|
+
|
|
20
|
+
import matplotlib.pyplot as plt
|
|
21
|
+
from matplotlib.axes import Axes
|
|
22
|
+
from matplotlib.colors import LogNorm
|
|
23
|
+
from matplotlib.layout_engine import ConstrainedLayoutEngine
|
|
24
|
+
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
import pickle
|
|
27
|
+
|
|
28
|
+
from typing import Callable, Literal
|
|
29
|
+
|
|
30
|
+
from functools import partial
|
|
31
|
+
|
|
32
|
+
from KDEpy import FFTKDE
|
|
33
|
+
|
|
34
|
+
from scipy.ndimage import gaussian_filter1d as Gaussian
|
|
35
|
+
|
|
36
|
+
from ..turbulence import ScalarField, VectorField
|
|
37
|
+
from ..spectra import Spectrum, Spectrum1D, EnergySpectra, get1D
|
|
38
|
+
from ..numdiss import NumericalDissipation, DissipationSpectra
|
|
39
|
+
|
|
40
|
+
# Set flush=True to avoid buffer output
|
|
41
|
+
print = partial(print, flush=True)
|
|
42
|
+
|
|
43
|
+
# Font settings
|
|
44
|
+
plt.rcParams['font.family'] = 'serif'
|
|
45
|
+
plt.rcParams['font.serif' ] = ['cmr10']
|
|
46
|
+
|
|
47
|
+
def float2LaTeX(value: float, ndigits: int = 2) -> str:
|
|
48
|
+
r"""Format a float as LaTeX scientific notation (no outer $), e.g. 3.1\times 10^{-4}."""
|
|
49
|
+
if not np.isfinite(value):
|
|
50
|
+
return str(value)
|
|
51
|
+
if value == 0.0:
|
|
52
|
+
return "0"
|
|
53
|
+
mantissa, exp = f"{value:.{ndigits}e}".split("e")
|
|
54
|
+
exp = int(exp)
|
|
55
|
+
return rf"{mantissa}\times 10^{{{exp}}}"
|
|
56
|
+
|
|
57
|
+
class Curve:
|
|
58
|
+
"""1D spectrum curve for dissipation spectra plots.
|
|
59
|
+
|
|
60
|
+
Attributes
|
|
61
|
+
----------
|
|
62
|
+
spc1d : Spectrum1D, the 1D spectrum object.
|
|
63
|
+
color : str, the color of the curve.
|
|
64
|
+
label : str, the label of the curve.
|
|
65
|
+
peak : float | None, the peak wavenumber of the curve, or None if not plotted.
|
|
66
|
+
lw : float, line width for plotDissipation (defaults to 2.0).
|
|
67
|
+
dashed: bool, dashed linestyle for plotDissipation (default False).
|
|
68
|
+
|
|
69
|
+
Methods
|
|
70
|
+
-------
|
|
71
|
+
getPeak : peak wavenumber from smoothed |E(k)|.
|
|
72
|
+
plotDissipation : Plot dissipation spectrum.
|
|
73
|
+
plotEnergy : Plot energy spectrum.
|
|
74
|
+
"""
|
|
75
|
+
def __init__(
|
|
76
|
+
self,
|
|
77
|
+
spc1d : Spectrum1D,
|
|
78
|
+
color : str,
|
|
79
|
+
label : str,
|
|
80
|
+
peak : bool = False,
|
|
81
|
+
mask : bool = False,
|
|
82
|
+
lw : float = 2.0,
|
|
83
|
+
dashed: bool = False,
|
|
84
|
+
) -> None:
|
|
85
|
+
self.spc1d = spc1d
|
|
86
|
+
self.color = color
|
|
87
|
+
self.label = label
|
|
88
|
+
self.lw = lw
|
|
89
|
+
self.dashed = dashed
|
|
90
|
+
|
|
91
|
+
if peak:
|
|
92
|
+
self.peak = Curve.getPeak(spc1d, mask=mask)
|
|
93
|
+
else:
|
|
94
|
+
self.peak = None
|
|
95
|
+
|
|
96
|
+
@staticmethod
|
|
97
|
+
def getPeak(spc1d: Spectrum1D, mask: bool = False) -> float | None:
|
|
98
|
+
"""Peak wavenumber of smoothed |E(k)|, or None if unavailable."""
|
|
99
|
+
|
|
100
|
+
k, Ek = spc1d.k, np.abs(spc1d.Ek)
|
|
101
|
+
sigma = 2.0 # kernel scale: radius 2*dk in units of dk-spaced samples
|
|
102
|
+
smoothEk = Gaussian(Ek, sigma=sigma, mode="nearest")
|
|
103
|
+
if mask:
|
|
104
|
+
valid = (k > 5.0) & np.isfinite(Ek)
|
|
105
|
+
else:
|
|
106
|
+
valid = np.isfinite(Ek)
|
|
107
|
+
if not np.any(valid):
|
|
108
|
+
return None
|
|
109
|
+
k, smoothEk = k[valid], smoothEk[valid]
|
|
110
|
+
idx = int(np.argmax(smoothEk))
|
|
111
|
+
kpeak = float(k[idx])
|
|
112
|
+
kmin = float(np.min(k))
|
|
113
|
+
if np.isclose(kpeak, kmin, rtol=1e-12, atol=1e-12 * max(1.0, abs(kmin))):
|
|
114
|
+
return None
|
|
115
|
+
return kpeak
|
|
116
|
+
|
|
117
|
+
def plotDissipation(self, ax: Axes) -> None:
|
|
118
|
+
if self.peak is not None:
|
|
119
|
+
ax.axvline(self.peak, color=self.color, ls="--", lw=1.8, zorder=1)
|
|
120
|
+
k, Ek = self.spc1d.k, self.spc1d.Ek
|
|
121
|
+
ls = "--" if self.dashed else "-"
|
|
122
|
+
ax.semilogx(k, Ek, color=self.color, lw=self.lw, ls=ls, label=self.label, zorder=3)
|
|
123
|
+
|
|
124
|
+
def plotEnergy(self, ax: Axes, slope: float = 5.0 / 3.0) -> None:
|
|
125
|
+
k = self.spc1d.k
|
|
126
|
+
y = self.spc1d.Ek * k**slope
|
|
127
|
+
ax.loglog(k, y, color=self.color, ls="--", lw=1.8, alpha=0.8, label=self.label)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def plotSlices(nd: NumericalDissipation, fraction: float = 1.0) -> None:
|
|
131
|
+
"""Plot 2D slices of dissipation terms
|
|
132
|
+
|
|
133
|
+
Outputs to nd.outputdir/slices/:
|
|
134
|
+
- For type in ('SSD', 'Bx', 'Bz'):
|
|
135
|
+
slice.phy.res.pdf, slice.phy.vis.pdf,
|
|
136
|
+
slice.num.res.pdf, slice.num.vis.pdf
|
|
137
|
+
all.slice.pdf, all.slice.pdf (for 'Bx' and 'Bz')
|
|
138
|
+
- For type 'hydro':
|
|
139
|
+
slice.phy.vis.pdf, slice.num.vis.pdf,
|
|
140
|
+
slice.rate.phy.vis.pdf, slice.rate.num.vis.pdf
|
|
141
|
+
- For type 'MRI':
|
|
142
|
+
'term.{num,phy}.{res,vis}.{x,y,z}.pdf' (12 files)
|
|
143
|
+
|
|
144
|
+
Parameters
|
|
145
|
+
----------
|
|
146
|
+
nd : NumericalDissipation object
|
|
147
|
+
fraction : float in (0, 1], colormap parameter
|
|
148
|
+
"""
|
|
149
|
+
if not (0 < fraction <= 1.0):
|
|
150
|
+
raise ValueError("fraction must be in (0, 1]")
|
|
151
|
+
|
|
152
|
+
path = Path(nd.outputdir) / "slices"
|
|
153
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
154
|
+
|
|
155
|
+
if nd.type == 'hydro':
|
|
156
|
+
terms = [
|
|
157
|
+
('term.phy.vis', nd.phyVisTerm if nd.nu != 0 else nd.divStressT),
|
|
158
|
+
('term.num.vis', nd.numVisTerm),
|
|
159
|
+
('rate.phy.vis', nd.phyVisRate if nd.nu != 0 else nd.VdotStress), # ScalarField
|
|
160
|
+
('rate.num.vis', nd.numVisRate), # ScalarField
|
|
161
|
+
]
|
|
162
|
+
else:
|
|
163
|
+
terms = [
|
|
164
|
+
('term.phy.res', nd.phyResTerm if nd.eta != 0 else nd.LaplacianB),
|
|
165
|
+
('term.phy.vis', nd.phyVisTerm if nd.nu != 0 else nd.divStressT),
|
|
166
|
+
('term.num.res', nd.numResTerm),
|
|
167
|
+
('term.num.vis', nd.numVisTerm),
|
|
168
|
+
('rate.phy.res', nd.phyResRate if nd.eta != 0 else nd.BdotLaplaB), # ScalarField
|
|
169
|
+
('rate.num.res', nd.numResRate), # ScalarField
|
|
170
|
+
('rate.phy.vis', nd.phyVisRate if nd.nu != 0 else nd.VdotStress), # ScalarField
|
|
171
|
+
('rate.num.vis', nd.numVisRate), # ScalarField
|
|
172
|
+
]
|
|
173
|
+
|
|
174
|
+
Nx, Ny, Nz = nd.Nx, nd.Ny, nd.Nz
|
|
175
|
+
Lx, Ly, Lz = nd.Lx, nd.Ly, nd.Lz
|
|
176
|
+
|
|
177
|
+
cmap = plt.colormaps['RdBu']
|
|
178
|
+
|
|
179
|
+
def getRange(data: np.ndarray, frac: float | None = None) -> tuple[float, float]:
|
|
180
|
+
|
|
181
|
+
f = fraction if frac is None else frac
|
|
182
|
+
valid = data[~np.isnan(data)]
|
|
183
|
+
if len(valid) == 0:
|
|
184
|
+
return -1.0, 1.0
|
|
185
|
+
r = float(np.percentile(np.abs(valid), f * 100))
|
|
186
|
+
return -r, r
|
|
187
|
+
|
|
188
|
+
for name, term in terms:
|
|
189
|
+
if term is None:
|
|
190
|
+
continue
|
|
191
|
+
|
|
192
|
+
if isinstance(term, VectorField):
|
|
193
|
+
slices = [
|
|
194
|
+
term.x[Nx // 2, :, :],
|
|
195
|
+
term.y[:, Ny // 2, :],
|
|
196
|
+
term.z[:, :, Nz // 2],
|
|
197
|
+
]
|
|
198
|
+
elif isinstance(term, ScalarField):
|
|
199
|
+
slices = [
|
|
200
|
+
term.data[Nx // 2, :, :],
|
|
201
|
+
term.data[:, Ny // 2, :],
|
|
202
|
+
term.data[:, :, Nz // 2],
|
|
203
|
+
]
|
|
204
|
+
else:
|
|
205
|
+
continue
|
|
206
|
+
|
|
207
|
+
if nd.type == 'MRI':
|
|
208
|
+
if not isinstance(term, VectorField) or not name.startswith('term.'):
|
|
209
|
+
continue
|
|
210
|
+
|
|
211
|
+
nu = r'\nu ' if nd.nu != 0 else ''
|
|
212
|
+
eta = r'\eta ' if nd.eta != 0 else ''
|
|
213
|
+
|
|
214
|
+
linewidth = 1.5
|
|
215
|
+
pad = 7
|
|
216
|
+
|
|
217
|
+
for comp, vardata in (
|
|
218
|
+
('x', term.x),
|
|
219
|
+
('y', term.y),
|
|
220
|
+
('z', term.z),
|
|
221
|
+
):
|
|
222
|
+
merged = np.concatenate(
|
|
223
|
+
[
|
|
224
|
+
vardata[:, :, Nz // 2].ravel(),
|
|
225
|
+
vardata[:, Ny // 2, :].ravel(),
|
|
226
|
+
vardata[Nx // 2, :, :].ravel(),
|
|
227
|
+
]
|
|
228
|
+
)
|
|
229
|
+
vmin, vmax = getRange(merged)
|
|
230
|
+
|
|
231
|
+
fig = plt.figure(figsize=(14, 8), constrained_layout=False)
|
|
232
|
+
gs = plt.GridSpec(
|
|
233
|
+
2, 3,
|
|
234
|
+
figure=fig,
|
|
235
|
+
width_ratios=[4, 1, 0.2],
|
|
236
|
+
height_ratios=[2, 1],
|
|
237
|
+
left=0.1,
|
|
238
|
+
right=0.9,
|
|
239
|
+
top=0.9,
|
|
240
|
+
bottom=0.1,
|
|
241
|
+
wspace=0.08,
|
|
242
|
+
hspace=0.08,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
ax1 = fig.add_subplot(gs[0, 0])
|
|
246
|
+
ax2 = fig.add_subplot(gs[0, 1])
|
|
247
|
+
ax3 = fig.add_subplot(gs[1, 0])
|
|
248
|
+
cax = fig.add_subplot(gs[:, 2])
|
|
249
|
+
|
|
250
|
+
im1 = ax1.imshow(
|
|
251
|
+
vardata[:, :, Nz // 2],
|
|
252
|
+
origin='upper',
|
|
253
|
+
cmap=cmap, vmin=vmin, vmax=vmax,
|
|
254
|
+
extent=(-Ly / 2, Ly / 2, Lx / 2, -Lx / 2),
|
|
255
|
+
)
|
|
256
|
+
im2 = ax2.imshow(
|
|
257
|
+
vardata[:, Ny // 2, :],
|
|
258
|
+
origin='upper',
|
|
259
|
+
cmap=cmap, vmin=vmin, vmax=vmax,
|
|
260
|
+
extent=(-Lz / 2, Lz / 2, Lx / 2, -Lx / 2),
|
|
261
|
+
)
|
|
262
|
+
im3 = ax3.imshow(
|
|
263
|
+
vardata[Nx // 2, :, :].T,
|
|
264
|
+
origin='lower',
|
|
265
|
+
cmap=cmap, vmin=vmin, vmax=vmax,
|
|
266
|
+
extent=(-Ly / 2, Ly / 2, -Lz / 2, Lz / 2),
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
ax1.set_aspect('equal')
|
|
270
|
+
ax2.set_aspect('equal')
|
|
271
|
+
ax3.set_aspect('equal')
|
|
272
|
+
|
|
273
|
+
ax1.xaxis.set_ticklabels([])
|
|
274
|
+
|
|
275
|
+
ax2.yaxis.set_ticklabels([])
|
|
276
|
+
ax2.set_ylabel('')
|
|
277
|
+
|
|
278
|
+
ax1.tick_params(direction='in')
|
|
279
|
+
ax2.tick_params(direction='in')
|
|
280
|
+
ax3.tick_params(direction='in')
|
|
281
|
+
|
|
282
|
+
ax1.set_ylabel(r'$x/H$', labelpad=0)
|
|
283
|
+
ax2.set_xlabel(r'$z/H$', labelpad=10)
|
|
284
|
+
ax3.set_xlabel(r'$y/H$', labelpad=10)
|
|
285
|
+
ax3.set_ylabel(r'$z/H$', labelpad=0)
|
|
286
|
+
|
|
287
|
+
ax1.tick_params(width=linewidth, pad=pad)
|
|
288
|
+
ax2.tick_params(width=linewidth, pad=pad)
|
|
289
|
+
ax3.tick_params(width=linewidth, pad=pad)
|
|
290
|
+
|
|
291
|
+
for ax in (ax1, ax2, ax3):
|
|
292
|
+
for spine in ax.spines.values():
|
|
293
|
+
spine.set_linewidth(linewidth)
|
|
294
|
+
|
|
295
|
+
cbar = fig.colorbar(im1, cax=cax, shrink=0.9)
|
|
296
|
+
|
|
297
|
+
# Physical terms: same notation as vislabel / reslabel (Bx/Bz composite).
|
|
298
|
+
# Numerical terms: D^{\mathrm{num}}_{\mathrm{vis/res}, comp}.
|
|
299
|
+
if name == 'term.phy.res':
|
|
300
|
+
cbarlabel = r'$' + eta + r'\nabla^2 B_' + comp + r'$'
|
|
301
|
+
elif name == 'term.num.res':
|
|
302
|
+
cbarlabel = (r'$D^{\mathrm{num}}_{\mathrm{res}, ' + comp + r'}$')
|
|
303
|
+
elif name == 'term.phy.vis':
|
|
304
|
+
cbarlabel = r'$' + nu + r'(\nabla \cdot \mathbb{T})_' + comp + r'$'
|
|
305
|
+
elif name == 'term.num.vis':
|
|
306
|
+
cbarlabel = r'$D^{\mathrm{num}}_{\mathrm{vis}, ' + comp + r'}$'
|
|
307
|
+
else:
|
|
308
|
+
raise ValueError(f'Unexpected MRI term name: {name!r}')
|
|
309
|
+
cbar.set_label(cbarlabel, labelpad=10)
|
|
310
|
+
|
|
311
|
+
outline_spine = cbar.ax.spines.get('outline')
|
|
312
|
+
if outline_spine is not None:
|
|
313
|
+
outline_spine.set_linewidth(linewidth)
|
|
314
|
+
|
|
315
|
+
plt.savefig(path / f'{name}.{comp}.pdf', bbox_inches='tight')
|
|
316
|
+
plt.close()
|
|
317
|
+
|
|
318
|
+
continue
|
|
319
|
+
|
|
320
|
+
elif nd.type in ('SSD', 'Bx', 'Bz', 'hydro'):
|
|
321
|
+
merged = np.concatenate([arr.flatten() for arr in slices])
|
|
322
|
+
vmin, vmax = getRange(merged)
|
|
323
|
+
|
|
324
|
+
fig, axes = plt.subplots(1, 3, figsize=(16, 6), constrained_layout=True)
|
|
325
|
+
ax1, ax2, ax3 = axes
|
|
326
|
+
|
|
327
|
+
im1 = ax1.imshow(
|
|
328
|
+
slices[0].T, origin='lower',
|
|
329
|
+
cmap=cmap, vmin=vmin, vmax=vmax,
|
|
330
|
+
extent=(-Ly / 2, Ly / 2, -Lz / 2, Lz / 2), aspect='auto'
|
|
331
|
+
)
|
|
332
|
+
im2 = ax2.imshow(
|
|
333
|
+
slices[1].T, origin='lower',
|
|
334
|
+
cmap=cmap, vmin=vmin, vmax=vmax,
|
|
335
|
+
extent=(-Lz / 2, Lz / 2, -Lx / 2, Lx / 2), aspect='auto'
|
|
336
|
+
)
|
|
337
|
+
im3 = ax3.imshow(
|
|
338
|
+
slices[2].T, origin='lower',
|
|
339
|
+
cmap=cmap, vmin=vmin, vmax=vmax,
|
|
340
|
+
extent=(-Lx / 2, Lx / 2, -Ly / 2, Ly / 2), aspect='auto'
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
ax1.set_xlabel(r'$y/H$')
|
|
344
|
+
ax1.set_ylabel(r'$z/H$')
|
|
345
|
+
ax2.set_xlabel(r'$z/H$')
|
|
346
|
+
ax2.set_ylabel(r'$x/H$')
|
|
347
|
+
ax3.set_xlabel(r'$x/H$')
|
|
348
|
+
ax3.set_ylabel(r'$y/H$')
|
|
349
|
+
|
|
350
|
+
for ax in [ax1, ax2, ax3]:
|
|
351
|
+
ax.tick_params(direction='in', width=1.5, pad=7)
|
|
352
|
+
ax.set_box_aspect(1)
|
|
353
|
+
for spine in ax.spines.values():
|
|
354
|
+
spine.set_linewidth(1.5)
|
|
355
|
+
|
|
356
|
+
cbar_bottom = 1.035
|
|
357
|
+
cbar_height = 0.06
|
|
358
|
+
for ax, im in [(ax1, im1), (ax2, im2), (ax3, im3)]:
|
|
359
|
+
cax = ax.inset_axes((0, cbar_bottom, 1, cbar_height))
|
|
360
|
+
cbar = fig.colorbar(im, cax=cax, orientation='horizontal')
|
|
361
|
+
cbar.ax.xaxis.set_ticks_position('top')
|
|
362
|
+
cbar.ax.xaxis.set_label_position('top')
|
|
363
|
+
cbar.ax.tick_params(labelsize=12, pad=2)
|
|
364
|
+
outline_spine = cbar.ax.spines.get("outline")
|
|
365
|
+
if outline_spine is not None:
|
|
366
|
+
outline_spine.set_linewidth(1.5)
|
|
367
|
+
|
|
368
|
+
filename = f'{name}.pdf'
|
|
369
|
+
plt.savefig(path / filename, bbox_inches='tight')
|
|
370
|
+
plt.close()
|
|
371
|
+
|
|
372
|
+
else:
|
|
373
|
+
raise ValueError(
|
|
374
|
+
f"Unsupported turbulence {nd.type!r}; expected 'SSD', 'Bx', 'Bz', 'MRI', or 'hydro'."
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
# ===== Composite slice plots: all.vis.pdf and all.res.pdf =====
|
|
378
|
+
# Each figure is a 2x3 grid. Rows are (plane, component) pairs.
|
|
379
|
+
# all.vis.pdf columns: velocity, physical viscosity, numerical viscosity
|
|
380
|
+
# all.res.pdf columns: magnetic field, physical resistivity, numerical resistivity
|
|
381
|
+
# For type = 'Bz', row 1 = z-plane & z-component; row 2 = x-plane & x-component.
|
|
382
|
+
# For type = 'Bx', row 1 = x-plane & x-component; row 2 = z-plane & z-component.
|
|
383
|
+
if nd.type in ('Bx', 'Bz'):
|
|
384
|
+
|
|
385
|
+
phyVisField = nd.phyVisTerm if nd.nu != 0 else nd.divStressT
|
|
386
|
+
phyResField = nd.phyResTerm if nd.eta != 0 else nd.LaplacianB
|
|
387
|
+
assert phyVisField is not None and phyResField is not None
|
|
388
|
+
assert nd.numVisTerm is not None and nd.numResTerm is not None
|
|
389
|
+
assert nd.V is not None and nd.B is not None
|
|
390
|
+
|
|
391
|
+
def getslice(arr: np.ndarray, plane: str) -> np.ndarray:
|
|
392
|
+
if plane == 'x':
|
|
393
|
+
return arr[Nx // 2, :, :]
|
|
394
|
+
elif plane == 'z':
|
|
395
|
+
return arr[:, :, Nz // 2]
|
|
396
|
+
else:
|
|
397
|
+
raise ValueError(f"Unsupported plane: {plane!r}; expected 'x' or 'z'.")
|
|
398
|
+
|
|
399
|
+
def getextent(plane: str) -> tuple[float, float, float, float]:
|
|
400
|
+
if plane == 'x':
|
|
401
|
+
return (-Ly / 2, Ly / 2, -Lz / 2, Lz / 2)
|
|
402
|
+
elif plane == 'z':
|
|
403
|
+
return (-Lx / 2, Lx / 2, -Ly / 2, Ly / 2)
|
|
404
|
+
else:
|
|
405
|
+
raise ValueError(f"Unsupported plane: {plane!r}; expected 'x' or 'z'.")
|
|
406
|
+
|
|
407
|
+
def getlabels(plane: str) -> tuple[str, str]:
|
|
408
|
+
if plane == 'x':
|
|
409
|
+
return r'$y$', r'$z$'
|
|
410
|
+
elif plane == 'z':
|
|
411
|
+
return r'$x$', r'$y$'
|
|
412
|
+
else:
|
|
413
|
+
raise ValueError(f"Unsupported plane: {plane!r}; expected 'x' or 'z'.")
|
|
414
|
+
|
|
415
|
+
planes = {
|
|
416
|
+
'Bz': [('z', 'z'), ('x', 'x')],
|
|
417
|
+
'Bx': [('x', 'x'), ('z', 'z')],
|
|
418
|
+
}[nd.type]
|
|
419
|
+
|
|
420
|
+
nu = r'\nu ' if nd.nu != 0 else ''
|
|
421
|
+
eta = r'\eta ' if nd.eta != 0 else ''
|
|
422
|
+
|
|
423
|
+
def vislabel(kind: str, comp: str) -> str:
|
|
424
|
+
return {
|
|
425
|
+
'V' : r'$u_' + comp + r'$',
|
|
426
|
+
'phyVis': r'$' + nu + r'(\nabla \cdot \mathbb{T})_' + comp + r'$',
|
|
427
|
+
'numVis': r'$D^{\mathrm{num}}_{\mathrm{vis}, ' + comp + r'}$',
|
|
428
|
+
}[kind]
|
|
429
|
+
|
|
430
|
+
def reslabel(kind: str, comp: str) -> str:
|
|
431
|
+
return {
|
|
432
|
+
'B' : r'$B_' + comp + r'$',
|
|
433
|
+
'phyRes': r'$' + eta + r'\nabla^2 B_' + comp + r'$',
|
|
434
|
+
'numRes': r'$D^{\mathrm{num}}_{\mathrm{res}, ' + comp + r'}$',
|
|
435
|
+
}[kind]
|
|
436
|
+
|
|
437
|
+
visCols = [('V', nd.V), ('phyVis', phyVisField), ('numVis', nd.numVisTerm)]
|
|
438
|
+
resCols = [('B', nd.B), ('phyRes', phyResField), ('numRes', nd.numResTerm)]
|
|
439
|
+
|
|
440
|
+
def plot(
|
|
441
|
+
filename: str,
|
|
442
|
+
columns : list[tuple[str, VectorField]],
|
|
443
|
+
labelFn : Callable[[str, str], str],
|
|
444
|
+
) -> None:
|
|
445
|
+
nrows, ncols = 2, 3
|
|
446
|
+
fig = plt.figure(figsize=(15.0, 11))
|
|
447
|
+
gs = fig.add_gridspec(
|
|
448
|
+
nrows, ncols,
|
|
449
|
+
wspace=0.24, hspace=0.04,
|
|
450
|
+
left=0.0, right=1.0, top=0.94, bottom=0.06,
|
|
451
|
+
)
|
|
452
|
+
axes = np.empty((nrows, ncols), dtype=object)
|
|
453
|
+
for r in range(nrows):
|
|
454
|
+
for c in range(ncols):
|
|
455
|
+
axes[r, c] = fig.add_subplot(gs[r, c])
|
|
456
|
+
|
|
457
|
+
for r, (plane, comp) in enumerate(planes):
|
|
458
|
+
extent = getextent(plane)
|
|
459
|
+
xlabel, ylabel = getlabels(plane)
|
|
460
|
+
for c, (kind, field) in enumerate(columns):
|
|
461
|
+
ax = axes[r, c]
|
|
462
|
+
data = getslice(getattr(field, comp), plane)
|
|
463
|
+
vmin, vmax = getRange(data, frac=1.0 if c == 0 else None)
|
|
464
|
+
|
|
465
|
+
im = ax.imshow(
|
|
466
|
+
data.T, origin='lower',
|
|
467
|
+
cmap=cmap, vmin=vmin, vmax=vmax,
|
|
468
|
+
extent=extent, aspect='auto'
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
isLeft = (c == 0)
|
|
472
|
+
ax.set_xlabel(xlabel)
|
|
473
|
+
if isLeft:
|
|
474
|
+
ax.set_ylabel(ylabel, labelpad=0)
|
|
475
|
+
ax.tick_params(direction='in', width=1.5, pad=5, labelleft=isLeft, labelsize=14)
|
|
476
|
+
ax.set_box_aspect(1)
|
|
477
|
+
for spine in ax.spines.values():
|
|
478
|
+
spine.set_linewidth(1.5)
|
|
479
|
+
|
|
480
|
+
ax.text(
|
|
481
|
+
0.06, 0.94, labelFn(kind, comp),
|
|
482
|
+
transform=ax.transAxes,
|
|
483
|
+
ha='left', va='top',
|
|
484
|
+
fontsize=18,
|
|
485
|
+
bbox=dict(
|
|
486
|
+
boxstyle='round,pad=0.5',
|
|
487
|
+
facecolor='white',
|
|
488
|
+
edgecolor='black',
|
|
489
|
+
linewidth=1.2,
|
|
490
|
+
),
|
|
491
|
+
zorder=10,
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
cax = ax.inset_axes((1.04, 0.0, 0.06, 1.0))
|
|
495
|
+
cbar = fig.colorbar(im, cax=cax, orientation='vertical')
|
|
496
|
+
cbar.ax.yaxis.set_ticks_position('right')
|
|
497
|
+
cbar.ax.yaxis.set_label_position('right')
|
|
498
|
+
cbar.ax.tick_params(labelsize=14, pad=4)
|
|
499
|
+
outline_spine = cbar.ax.spines.get("outline")
|
|
500
|
+
if outline_spine is not None:
|
|
501
|
+
outline_spine.set_linewidth(1.5)
|
|
502
|
+
|
|
503
|
+
plt.savefig(path / filename, bbox_inches='tight')
|
|
504
|
+
plt.close()
|
|
505
|
+
|
|
506
|
+
plot('all.vis.pdf', visCols, vislabel)
|
|
507
|
+
plot('all.res.pdf', resCols, reslabel)
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def plotShellSpectra(ds: DissipationSpectra, spc: EnergySpectra, outdir: Path) -> None:
|
|
511
|
+
"""Plot shell-integrated dissipation and energy spectra
|
|
512
|
+
|
|
513
|
+
Parameters
|
|
514
|
+
----------
|
|
515
|
+
ds : DissipationSpectra
|
|
516
|
+
spc : EnergySpectra
|
|
517
|
+
outdir: Path, the output directory.
|
|
518
|
+
"""
|
|
519
|
+
totEkin = spc.totEkin
|
|
520
|
+
totEmag = spc.totEmag
|
|
521
|
+
|
|
522
|
+
Ekin1D = get1D(totEkin, mode="shell")
|
|
523
|
+
Emag1D = get1D(totEmag, mode="shell") if totEmag is not None else None
|
|
524
|
+
|
|
525
|
+
def plotCurves(
|
|
526
|
+
filename: str,
|
|
527
|
+
curves1 : list[Curve],
|
|
528
|
+
curves2 : list[Curve],
|
|
529
|
+
ylabel : str,
|
|
530
|
+
) -> None:
|
|
531
|
+
|
|
532
|
+
fig, ax1 = plt.subplots(figsize=(9.0, 6.0))
|
|
533
|
+
ax2 = ax1.twinx()
|
|
534
|
+
|
|
535
|
+
# Draw left-axis lines above right-axis lines
|
|
536
|
+
ax2.set_zorder(0)
|
|
537
|
+
ax1.set_zorder(1)
|
|
538
|
+
ax1.patch.set_visible(False)
|
|
539
|
+
|
|
540
|
+
for curve in curves1:
|
|
541
|
+
curve.plotDissipation(ax1)
|
|
542
|
+
|
|
543
|
+
for curve in curves2:
|
|
544
|
+
curve.plotEnergy(ax2)
|
|
545
|
+
|
|
546
|
+
# Keep low-k forcing from dominating y-max by using only k > 10 for the upper bound.
|
|
547
|
+
all = np.concatenate([curve.spc1d.Ek for curve in curves1])
|
|
548
|
+
high = np.concatenate([curve.spc1d.Ek[curve.spc1d.k > 10.0] for curve in curves1])
|
|
549
|
+
|
|
550
|
+
ymin = float(np.min(all))
|
|
551
|
+
ymax = float(np.max(high))
|
|
552
|
+
|
|
553
|
+
# Auto margin following the matplotlib default.
|
|
554
|
+
ymargin = float(plt.rcParams.get("axes.ymargin", 0.05))
|
|
555
|
+
pad = ymargin * (ymax - ymin)
|
|
556
|
+
ax1.set_ylim(ymin - pad, ymax + pad)
|
|
557
|
+
ax1.axhline(0.0, color="k", ls="-", lw=1.0, zorder=-10)
|
|
558
|
+
|
|
559
|
+
labelsize = 16
|
|
560
|
+
ticksize = 12
|
|
561
|
+
|
|
562
|
+
ax1.set_xlabel(r"$k$", fontsize=labelsize)
|
|
563
|
+
ax1.set_ylabel(ylabel, fontsize=labelsize)
|
|
564
|
+
ax2.set_ylabel(r"$k^{5/3}E(k)$", fontsize=labelsize)
|
|
565
|
+
|
|
566
|
+
ax1.tick_params(axis="both", direction="in", which="both", labelsize=ticksize, pad=5)
|
|
567
|
+
ax2.tick_params(axis="y", direction="in", which="both", labelsize=ticksize, pad=5)
|
|
568
|
+
|
|
569
|
+
ax1.grid(True, which="both", ls="--", alpha=0.3)
|
|
570
|
+
handles1, labels1 = ax1.get_legend_handles_labels()
|
|
571
|
+
handles2, labels2 = ax2.get_legend_handles_labels()
|
|
572
|
+
handles, labels = handles1 + handles2, labels1 + labels2
|
|
573
|
+
ax1.legend(handles, labels, loc="lower left", fontsize=14, framealpha=1.0)
|
|
574
|
+
|
|
575
|
+
fig.tight_layout()
|
|
576
|
+
fig.savefig(outdir / filename, bbox_inches="tight")
|
|
577
|
+
plt.close(fig)
|
|
578
|
+
|
|
579
|
+
# ===== shell.num.pdf =====
|
|
580
|
+
# numerical viscous + numerical resistive dissipation spectra
|
|
581
|
+
num1: list[Curve] = []
|
|
582
|
+
spectrum1d = get1D(ds.eNumVis, mode="shell", negative=True)
|
|
583
|
+
num1.append(
|
|
584
|
+
Curve(spectrum1d, "k", r"$\varepsilon_{\mathrm{vis}}^{\mathrm{num}}$", peak=True, mask=True)
|
|
585
|
+
)
|
|
586
|
+
if spc.totEmag is not None:
|
|
587
|
+
spectrum1d = get1D(ds.eNumRes, mode="shell", negative=True)
|
|
588
|
+
num1.append(
|
|
589
|
+
Curve(spectrum1d, "r", r"$\varepsilon_{\mathrm{res}}^{\mathrm{num}}$", peak=True, mask=True)
|
|
590
|
+
)
|
|
591
|
+
if num1:
|
|
592
|
+
num2: list[Curve] = []
|
|
593
|
+
curve = Curve(Ekin1D, "k", r"$E_{\mathrm{kin}}$")
|
|
594
|
+
num2.append(curve)
|
|
595
|
+
if Emag1D is not None:
|
|
596
|
+
curve = Curve(Emag1D, "r", r"$E_{\mathrm{mag}}$")
|
|
597
|
+
num2.append(curve)
|
|
598
|
+
ylabel = r"$-\varepsilon_{\mathrm{diss}}^{\mathrm{num}}(k)$"
|
|
599
|
+
plotCurves("shell.num.pdf", curves1 = num1, curves2 = num2, ylabel = ylabel)
|
|
600
|
+
|
|
601
|
+
# ===== shell.all.pdf =====
|
|
602
|
+
if Emag1D is not None and spc.totEmag is not None:
|
|
603
|
+
|
|
604
|
+
# vis: numerical/physical/total viscous dissipation spectrum
|
|
605
|
+
vis: list[Curve] = []
|
|
606
|
+
|
|
607
|
+
# numerical viscous dissipation spectrum
|
|
608
|
+
spectrum1d = get1D(ds.eNumVis, mode="shell", negative=True)
|
|
609
|
+
label = r"$\varepsilon_{\mathrm{vis}}^{\mathrm{num}}$"
|
|
610
|
+
vis.append(Curve(spectrum1d, "b", label, peak=True, mask=True, lw=2.5, dashed=True))
|
|
611
|
+
|
|
612
|
+
# physical viscous dissipation spectrum
|
|
613
|
+
if ds.nu != 0.0:
|
|
614
|
+
spectrum1d = get1D(ds.ePhyVis, mode="shell", negative=True)
|
|
615
|
+
label = r"$\varepsilon_{\mathrm{vis}}^{\mathrm{phy}}$"
|
|
616
|
+
vis.append(Curve(spectrum1d, "r", label, peak=True, mask=True, lw=2.5, dashed=True))
|
|
617
|
+
|
|
618
|
+
# total viscous dissipation spectrum
|
|
619
|
+
spectrum1d = get1D(ds.eTotVis, mode="shell", negative=True)
|
|
620
|
+
label = r"$\varepsilon_{\mathrm{vis}}^{\mathrm{tot}}$"
|
|
621
|
+
vis.append(Curve(spectrum1d, "k", label, peak=True, mask=True, lw=2.5, dashed=False))
|
|
622
|
+
|
|
623
|
+
# kin: kinetic energy spectrum
|
|
624
|
+
kin: list[Curve] = [Curve(Ekin1D, "k", r"$E_{\mathrm{kin}}$")]
|
|
625
|
+
|
|
626
|
+
# res: numerical/physical/total resistive dissipation spectrum
|
|
627
|
+
res: list[Curve] = []
|
|
628
|
+
|
|
629
|
+
# numerical resistive dissipation spectrum
|
|
630
|
+
spectrum1d = get1D(ds.eNumRes, mode="shell", negative=True)
|
|
631
|
+
label = r"$\varepsilon_{\mathrm{res}}^{\mathrm{num}}$"
|
|
632
|
+
res.append(Curve(spectrum1d, "b", label, peak=True, mask=True, lw=2.5, dashed=True))
|
|
633
|
+
|
|
634
|
+
# physical resistive dissipation spectrum
|
|
635
|
+
if ds.eta != 0.0:
|
|
636
|
+
spectrum1d = get1D(ds.ePhyRes, mode="shell", negative=True)
|
|
637
|
+
label = r"$\varepsilon_{\mathrm{res}}^{\mathrm{phy}}$"
|
|
638
|
+
res.append(Curve(spectrum1d, "r", label, peak=True, mask=True, lw=2.5, dashed=True))
|
|
639
|
+
|
|
640
|
+
# total resistive dissipation spectrum
|
|
641
|
+
spectrum1d = get1D(ds.eTotRes, mode="shell", negative=True)
|
|
642
|
+
label = r"$\varepsilon_{\mathrm{res}}^{\mathrm{tot}}$"
|
|
643
|
+
res.append(Curve(spectrum1d, "k", label, peak=True, mask=True, lw=2.5, dashed=False))
|
|
644
|
+
|
|
645
|
+
# mag: magnetic energy spectrum
|
|
646
|
+
mag: list[Curve] = [Curve(Emag1D, "k", r"$E_{\mathrm{mag}}$")]
|
|
647
|
+
|
|
648
|
+
yleftlabel = r"$-\varepsilon(k)$"
|
|
649
|
+
yrightlabel = r"$k^{5/3}E(k)$"
|
|
650
|
+
|
|
651
|
+
xlabelsize = 16
|
|
652
|
+
ylabelsize = 14
|
|
653
|
+
ticksize = 12
|
|
654
|
+
pad = 5.5
|
|
655
|
+
|
|
656
|
+
fig, (ax1vis, ax1res) = plt.subplots(1, 2, figsize=(12.0, 5.5), sharey=True)
|
|
657
|
+
ax2vis = ax1vis.twinx()
|
|
658
|
+
ax2res = ax1res.twinx()
|
|
659
|
+
ax2res.sharey(ax2vis)
|
|
660
|
+
|
|
661
|
+
for ax1, ax2 in ((ax1vis, ax2vis), (ax1res, ax2res)):
|
|
662
|
+
ax2.set_zorder(0)
|
|
663
|
+
ax1.set_zorder(1)
|
|
664
|
+
ax1.patch.set_visible(False)
|
|
665
|
+
|
|
666
|
+
for curve in vis:
|
|
667
|
+
curve.plotDissipation(ax1vis)
|
|
668
|
+
for curve in kin:
|
|
669
|
+
curve.plotEnergy(ax2vis)
|
|
670
|
+
for curve in res:
|
|
671
|
+
curve.plotDissipation(ax1res)
|
|
672
|
+
for curve in mag:
|
|
673
|
+
curve.plotEnergy(ax2res)
|
|
674
|
+
|
|
675
|
+
totvis1d = get1D(ds.eTotVis, mode="shell", negative=True)
|
|
676
|
+
totres1d = get1D(ds.eTotRes, mode="shell", negative=True)
|
|
677
|
+
diss_Ek = np.concatenate([totvis1d.Ek, totres1d.Ek])
|
|
678
|
+
diss_Ek_highk = np.concatenate(
|
|
679
|
+
[
|
|
680
|
+
totvis1d.Ek[totvis1d.k > 10.0],
|
|
681
|
+
totres1d.Ek[totres1d.k > 10.0],
|
|
682
|
+
]
|
|
683
|
+
)
|
|
684
|
+
ymin = float(np.min(diss_Ek))
|
|
685
|
+
ymax = float(np.max(diss_Ek_highk))
|
|
686
|
+
ymargin = float(plt.rcParams.get("axes.ymargin", 0.05))
|
|
687
|
+
ypad = ymargin * (ymax - ymin)
|
|
688
|
+
ylow, yhigh = ymin - ypad, ymax + ypad
|
|
689
|
+
ax1vis.set_ylim(ylow, yhigh)
|
|
690
|
+
ax1vis.axhline(0.0, color="k", ls="-", lw=1.0, zorder=-10)
|
|
691
|
+
ax1res.axhline(0.0, color="k", ls="-", lw=1.0, zorder=-10)
|
|
692
|
+
|
|
693
|
+
ax2vis.relim()
|
|
694
|
+
ax2res.relim()
|
|
695
|
+
ax2vis.autoscale_view()
|
|
696
|
+
|
|
697
|
+
ax1vis.set_xlabel(r"$k$", fontsize=xlabelsize)
|
|
698
|
+
ax1res.set_xlabel(r"$k$", fontsize=xlabelsize)
|
|
699
|
+
ax1vis.set_ylabel(yleftlabel, fontsize=ylabelsize)
|
|
700
|
+
ax2res.set_ylabel(yrightlabel, fontsize=ylabelsize)
|
|
701
|
+
|
|
702
|
+
ax1vis.tick_params(axis="both", direction="in", which="both", labelsize=ticksize, pad=pad)
|
|
703
|
+
ax1res.tick_params(axis="x" , direction="in", which="both", labelsize=ticksize, pad=pad)
|
|
704
|
+
ax1res.tick_params(axis="y" , direction="in", which="both", labelsize=ticksize, pad=pad, labelleft=False)
|
|
705
|
+
ax2vis.tick_params(axis="y" , direction="in", which="both", labelsize=ticksize, pad=pad, labelright=False)
|
|
706
|
+
ax2res.tick_params(axis="y" , direction="in", which="both", labelsize=ticksize, pad=pad)
|
|
707
|
+
|
|
708
|
+
ax1vis.grid(True, which="both", ls="--", alpha=0.3)
|
|
709
|
+
ax1res.grid(True, which="both", ls="--", alpha=0.3)
|
|
710
|
+
|
|
711
|
+
handles1, labels1 = ax1vis.get_legend_handles_labels()
|
|
712
|
+
handles2, labels2 = ax2vis.get_legend_handles_labels()
|
|
713
|
+
frame = ax1vis.legend(
|
|
714
|
+
handles1 + handles2, labels1 + labels2, loc="lower left", bbox_to_anchor=(0.007, 0.007), fontsize=14,
|
|
715
|
+
framealpha=1.0, fancybox=True, facecolor="white", edgecolor="k",
|
|
716
|
+
).get_frame()
|
|
717
|
+
frame.set_edgecolor("k")
|
|
718
|
+
frame.set_linewidth(0.9)
|
|
719
|
+
frame.set_boxstyle("round", pad=0.15, rounding_size=0.7)
|
|
720
|
+
|
|
721
|
+
handles1, labels1 = ax1res.get_legend_handles_labels()
|
|
722
|
+
handles2, labels2 = ax2res.get_legend_handles_labels()
|
|
723
|
+
frame = ax1res.legend(
|
|
724
|
+
handles1 + handles2, labels1 + labels2, loc="lower left", bbox_to_anchor=(0.007, 0.007), fontsize=14,
|
|
725
|
+
framealpha=1.0, fancybox=True, facecolor="white", edgecolor="k",
|
|
726
|
+
).get_frame()
|
|
727
|
+
frame.set_edgecolor("k")
|
|
728
|
+
frame.set_linewidth(0.9)
|
|
729
|
+
frame.set_boxstyle("round", pad=0.15, rounding_size=0.7)
|
|
730
|
+
|
|
731
|
+
bbox = dict(
|
|
732
|
+
boxstyle="round,pad=0.45",
|
|
733
|
+
facecolor="white",
|
|
734
|
+
edgecolor="1.0",
|
|
735
|
+
linewidth=0.8,
|
|
736
|
+
alpha=0.0,
|
|
737
|
+
)
|
|
738
|
+
|
|
739
|
+
nu = float2LaTeX(ds.nu, ndigits=1)
|
|
740
|
+
eta = float2LaTeX(ds.eta, ndigits=1)
|
|
741
|
+
ax1vis.text(
|
|
742
|
+
0.98, 0.97, rf"$\nu = {nu}$", transform=ax1vis.transAxes,
|
|
743
|
+
ha="right", va="top", fontsize=14, bbox=bbox,
|
|
744
|
+
)
|
|
745
|
+
ax1res.text(
|
|
746
|
+
0.98, 0.97, rf"$\eta = {eta}$", transform=ax1res.transAxes,
|
|
747
|
+
ha="right", va="top", fontsize=14, bbox=bbox,
|
|
748
|
+
)
|
|
749
|
+
|
|
750
|
+
fig.tight_layout()
|
|
751
|
+
fig.savefig(outdir / "shell.all.pdf", bbox_inches="tight")
|
|
752
|
+
plt.close(fig)
|
|
753
|
+
|
|
754
|
+
|
|
755
|
+
def plotAxisymmetricSpectra(ds: DissipationSpectra, spc: EnergySpectra, outdir: Path) -> None:
|
|
756
|
+
"""Plot axisymmetric dissipation and energy spectra for Bx/Bz turbulence.
|
|
757
|
+
|
|
758
|
+
Parameters
|
|
759
|
+
----------
|
|
760
|
+
ds : DissipationSpectra
|
|
761
|
+
spc : EnergySpectra
|
|
762
|
+
outdir: Path, the output directory.
|
|
763
|
+
"""
|
|
764
|
+
if spc.type == "Bx":
|
|
765
|
+
axis = "x"
|
|
766
|
+
elif spc.type == "Bz":
|
|
767
|
+
axis = "z"
|
|
768
|
+
else:
|
|
769
|
+
raise ValueError(f"plotAxisymmetricSpectra requires type 'Bx' or 'Bz', got '{spc.type}'")
|
|
770
|
+
|
|
771
|
+
if spc.totEmag is None:
|
|
772
|
+
raise ValueError("Magnetic energy spectrum cache is required for axisymmetric spectrum plotting.")
|
|
773
|
+
|
|
774
|
+
totEkin = ds.Ek.totEkin
|
|
775
|
+
totEmag = ds.Ek.totEmag
|
|
776
|
+
assert totEmag is not None
|
|
777
|
+
perpEkin1D = get1D(totEkin, mode="perp", axis=axis)
|
|
778
|
+
paraEkin1D = get1D(totEkin, mode="para", axis=axis)
|
|
779
|
+
perpEmag1D = get1D(totEmag, mode="perp", axis=axis)
|
|
780
|
+
paraEmag1D = get1D(totEmag, mode="para", axis=axis)
|
|
781
|
+
|
|
782
|
+
def getDissipationCurves(
|
|
783
|
+
phy : Spectrum,
|
|
784
|
+
num : Spectrum,
|
|
785
|
+
tot : Spectrum,
|
|
786
|
+
mode : Literal["perp", "para"],
|
|
787
|
+
diss : Literal["vis", "res"],
|
|
788
|
+
) -> list[Curve]:
|
|
789
|
+
|
|
790
|
+
# Order: numerical, physical, total (legend: num on top). Num = blue dashed, phy = red dashed.
|
|
791
|
+
return [
|
|
792
|
+
Curve(
|
|
793
|
+
get1D(num, mode=mode, axis=axis, negative=True),
|
|
794
|
+
"b", rf"$\varepsilon_{{\mathrm{{{diss}}}}}^{{\mathrm{{num}}}}$", lw=2.5, dashed=True,
|
|
795
|
+
),
|
|
796
|
+
Curve(
|
|
797
|
+
get1D(phy, mode=mode, axis=axis, negative=True),
|
|
798
|
+
"r", rf"$\varepsilon_{{\mathrm{{{diss}}}}}^{{\mathrm{{phy}}}}$", lw=2.5, dashed=True,
|
|
799
|
+
),
|
|
800
|
+
Curve(
|
|
801
|
+
get1D(tot, mode=mode, axis=axis, negative=True),
|
|
802
|
+
"k", rf"$\varepsilon_{{\mathrm{{{diss}}}}}^{{\mathrm{{tot}}}}$", lw=2.5, dashed=False,
|
|
803
|
+
),
|
|
804
|
+
]
|
|
805
|
+
|
|
806
|
+
def getShellDissipationCurves(
|
|
807
|
+
phy : Spectrum,
|
|
808
|
+
num : Spectrum,
|
|
809
|
+
tot : Spectrum,
|
|
810
|
+
diss : Literal["vis", "res"],
|
|
811
|
+
direction: Literal["perp", "para"],
|
|
812
|
+
) -> list[Curve]:
|
|
813
|
+
|
|
814
|
+
directionLabel = r"\perp" if direction == "perp" else r"\parallel"
|
|
815
|
+
curves: list[Curve] = []
|
|
816
|
+
# Plot / legend order: num, phy, tot. Num = blue dashed, phy = red dashed.
|
|
817
|
+
for spectrum, color, label, dashed, lw, use_peak in (
|
|
818
|
+
(num, "b", "num", True , 2.5, True ),
|
|
819
|
+
(phy, "r", "phy", True , 2.5, True ),
|
|
820
|
+
(tot, "k", "tot", False, 2.5, False),
|
|
821
|
+
):
|
|
822
|
+
spectrum1D = get1D(spectrum, mode="shell", negative=True)
|
|
823
|
+
curveLabel = rf"$\mathscr{{D}}_{{\mathrm{{{diss}}},{directionLabel}}}^{{\mathrm{{{label}}}}}$"
|
|
824
|
+
curves.append(
|
|
825
|
+
Curve(
|
|
826
|
+
spectrum1D, color, curveLabel,
|
|
827
|
+
lw=lw, dashed=dashed, peak=use_peak, mask=use_peak,
|
|
828
|
+
),
|
|
829
|
+
)
|
|
830
|
+
return curves
|
|
831
|
+
|
|
832
|
+
def plotCurves(
|
|
833
|
+
filename: str,
|
|
834
|
+
panels : list[tuple[list[Curve], list[Curve], float, str, str, str]],
|
|
835
|
+
xlim : tuple[float, float] | None = None,
|
|
836
|
+
) -> None:
|
|
837
|
+
|
|
838
|
+
fig, axs = plt.subplots(2, 2, figsize=(12.0, 9.5), sharey="row")
|
|
839
|
+
ax2s = [[axs[i, j].twinx() for j in range(2)] for i in range(2)]
|
|
840
|
+
|
|
841
|
+
ax2s[0][1].sharey(ax2s[0][0])
|
|
842
|
+
ax2s[1][1].sharey(ax2s[1][0])
|
|
843
|
+
|
|
844
|
+
labelsize = 14
|
|
845
|
+
ticksize = 12
|
|
846
|
+
pad = 5.5
|
|
847
|
+
|
|
848
|
+
for i, (curves1, curves2, slope, xlabel, ylabelLeft, ylabelRight) in enumerate(panels):
|
|
849
|
+
row, col = divmod(i, 2)
|
|
850
|
+
ax1 = axs[row, col]
|
|
851
|
+
ax2 = ax2s[row][col]
|
|
852
|
+
|
|
853
|
+
ax2.set_zorder(0)
|
|
854
|
+
ax1.set_zorder(1)
|
|
855
|
+
ax1.patch.set_visible(False)
|
|
856
|
+
|
|
857
|
+
for curve in curves1:
|
|
858
|
+
curve.plotDissipation(ax1)
|
|
859
|
+
for curve in curves2:
|
|
860
|
+
curve.plotEnergy(ax2, slope=slope)
|
|
861
|
+
|
|
862
|
+
ax1.axhline(0.0, color="k", ls="-", lw=1.0, zorder=-10)
|
|
863
|
+
# ax1.grid(True, which="both", ls="--", alpha=0.3)
|
|
864
|
+
|
|
865
|
+
# Omit bottom-row x labels on the first row when x matches the panel below (same column).
|
|
866
|
+
samexlabel = panels[col][3] == panels[2 + col][3]
|
|
867
|
+
showbottom = (row == 1) or not samexlabel
|
|
868
|
+
if showbottom:
|
|
869
|
+
ax1.set_xlabel(xlabel, fontsize=labelsize)
|
|
870
|
+
|
|
871
|
+
if col == 0:
|
|
872
|
+
ax1.set_ylabel(ylabelLeft, fontsize=labelsize)
|
|
873
|
+
else:
|
|
874
|
+
ax1.tick_params(axis="y", labelleft=False)
|
|
875
|
+
ax2.set_ylabel(ylabelRight, fontsize=labelsize)
|
|
876
|
+
|
|
877
|
+
ax1.tick_params(
|
|
878
|
+
axis="both", direction="in", which="both",
|
|
879
|
+
labelsize=ticksize, pad=pad, labelbottom=showbottom,
|
|
880
|
+
)
|
|
881
|
+
ax2.tick_params(axis="y" , direction="in", which="both", labelsize=ticksize, pad=pad)
|
|
882
|
+
|
|
883
|
+
if col == 0:
|
|
884
|
+
ax2.tick_params(axis="y", labelright=False)
|
|
885
|
+
|
|
886
|
+
handles1, labels1 = ax1.get_legend_handles_labels()
|
|
887
|
+
handles2, labels2 = ax2.get_legend_handles_labels()
|
|
888
|
+
frame = ax1.legend(
|
|
889
|
+
handles1 + handles2, labels1 + labels2,
|
|
890
|
+
loc="lower left", bbox_to_anchor=(0.007, 0.007), fontsize=12,
|
|
891
|
+
framealpha=1.0, fancybox=True, facecolor="white", edgecolor="k",
|
|
892
|
+
).get_frame()
|
|
893
|
+
frame.set_edgecolor("k")
|
|
894
|
+
frame.set_linewidth(0.9)
|
|
895
|
+
frame.set_boxstyle("round", pad=0.15, rounding_size=0.5)
|
|
896
|
+
|
|
897
|
+
if xlim is not None:
|
|
898
|
+
k0, k1 = xlim
|
|
899
|
+
ym = float(plt.rcParams.get("axes.ymargin", 0.05))
|
|
900
|
+
for row in (0, 1):
|
|
901
|
+
parts: list[np.ndarray] = []
|
|
902
|
+
for j in (0, 1):
|
|
903
|
+
for c in panels[2 * row + j][0]:
|
|
904
|
+
k, ek = c.spc1d.k, c.spc1d.Ek
|
|
905
|
+
m = (k >= k0) & (k <= k1) & np.isfinite(ek)
|
|
906
|
+
if np.any(m):
|
|
907
|
+
parts.append(ek[m])
|
|
908
|
+
if parts:
|
|
909
|
+
y = np.concatenate(parts)
|
|
910
|
+
lo, hi = float(np.min(y)), float(np.max(y))
|
|
911
|
+
p = ym * ((hi - lo) if hi > lo else max(abs(hi), abs(lo), 1.0))
|
|
912
|
+
axs[row, 0].set_ylim(lo - p, hi + p)
|
|
913
|
+
for ax1 in axs.flat:
|
|
914
|
+
ax1.set_xlim(xlim)
|
|
915
|
+
|
|
916
|
+
# Semi-transparent band for energy injection scales in [k_min, k_inj]. Draw on twin ax2 only
|
|
917
|
+
# (lower axes zorder) so axvspan stays below energy curves and ax1 dissipation draws on top.
|
|
918
|
+
injection_k_hi = 6.5
|
|
919
|
+
span_kw = dict(facecolor="0.5", alpha=0.24, zorder=-15, linewidth=0)
|
|
920
|
+
for row in range(2):
|
|
921
|
+
for col in range(2):
|
|
922
|
+
ax2 = ax2s[row][col]
|
|
923
|
+
k_lo, k_hi_axis = ax2.get_xlim()
|
|
924
|
+
k_inj_right = min(injection_k_hi, k_hi_axis)
|
|
925
|
+
if k_inj_right > k_lo:
|
|
926
|
+
ax2.axvspan(k_lo, k_inj_right, **span_kw)
|
|
927
|
+
|
|
928
|
+
fig.tight_layout()
|
|
929
|
+
fig.subplots_adjust(hspace=0.07, wspace=0.06)
|
|
930
|
+
fig.savefig(outdir / filename, bbox_inches="tight")
|
|
931
|
+
plt.close(fig)
|
|
932
|
+
|
|
933
|
+
# ===== axisymmetric.pdf =====
|
|
934
|
+
# Dissipation spectra: \varepsilon_{vis,res}(k_{\perp,\parallel})
|
|
935
|
+
# Energy spectra: E_{kin,mag}(k_{\perp,\parallel})
|
|
936
|
+
perpVis: list[Curve] = getDissipationCurves(
|
|
937
|
+
ds.ePhyVis, ds.eNumVis, ds.eTotVis, mode="perp", diss="vis",
|
|
938
|
+
)
|
|
939
|
+
perpRes: list[Curve] = getDissipationCurves(
|
|
940
|
+
ds.ePhyRes, ds.eNumRes, ds.eTotRes, mode="perp", diss="res",
|
|
941
|
+
)
|
|
942
|
+
paraVis: list[Curve] = getDissipationCurves(
|
|
943
|
+
ds.ePhyVis, ds.eNumVis, ds.eTotVis, mode="para", diss="vis",
|
|
944
|
+
)
|
|
945
|
+
paraRes: list[Curve] = getDissipationCurves(
|
|
946
|
+
ds.ePhyRes, ds.eNumRes, ds.eTotRes, mode="para", diss="res",
|
|
947
|
+
)
|
|
948
|
+
|
|
949
|
+
perpKin: list[Curve] = [Curve(perpEkin1D, "0.5", r"$E_{\mathrm{kin}}$")]
|
|
950
|
+
perpMag: list[Curve] = [Curve(perpEmag1D, "0.5", r"$E_{\mathrm{mag}}$")]
|
|
951
|
+
paraKin: list[Curve] = [Curve(paraEkin1D, "0.5", r"$E_{\mathrm{kin}}$")]
|
|
952
|
+
paraMag: list[Curve] = [Curve(paraEmag1D, "0.5", r"$E_{\mathrm{mag}}$")]
|
|
953
|
+
|
|
954
|
+
panels = [
|
|
955
|
+
# top-left panel: \varepsilon_{vis}(k_{\perp})
|
|
956
|
+
(
|
|
957
|
+
perpVis, perpKin, 5.0 / 3.0,
|
|
958
|
+
r"$k_{\perp}$", r"$-\varepsilon(k_{\perp})$", r"$k_{\perp}^{5/3}E(k_{\perp})$",
|
|
959
|
+
),
|
|
960
|
+
# top-right panel: \varepsilon_{res}(k_{\perp})
|
|
961
|
+
(
|
|
962
|
+
perpRes, perpMag, 5.0 / 3.0,
|
|
963
|
+
r"$k_{\perp}$", r"$-\varepsilon(k_{\perp})$", r"$k_{\perp}^{5/3}E(k_{\perp})$",
|
|
964
|
+
),
|
|
965
|
+
# bottom-left panel: \varepsilon_{vis}(k_{\parallel})
|
|
966
|
+
(
|
|
967
|
+
paraVis, paraKin, 2.0,
|
|
968
|
+
r"$k_{\parallel}$", r"$-\varepsilon(k_{\parallel})$", r"$k_{\parallel}^{2}E(k_{\parallel})$",
|
|
969
|
+
),
|
|
970
|
+
# bottom-right panel: \varepsilon_{res}(k_{\parallel})
|
|
971
|
+
(
|
|
972
|
+
paraRes, paraMag, 2.0,
|
|
973
|
+
r"$k_{\parallel}$", r"$-\varepsilon(k_{\parallel})$", r"$k_{\parallel}^{2}E(k_{\parallel})$",
|
|
974
|
+
),
|
|
975
|
+
]
|
|
976
|
+
plotCurves("axisymmetric.pdf", panels)
|
|
977
|
+
|
|
978
|
+
# ===== components.pdf =====
|
|
979
|
+
# Shell-integrated component-wise dissipation spectra: \mathscr{D}_{vis,res}(k)
|
|
980
|
+
# Shell-integrated component-wise energy spectra: E_{kin,mag}(k)
|
|
981
|
+
Ek = ds.Ek
|
|
982
|
+
|
|
983
|
+
if axis == "z":
|
|
984
|
+
perpEkin, paraEkin = Ek.xEkin + Ek.yEkin, Ek.zEkin
|
|
985
|
+
if Ek.xEmag is None or Ek.yEmag is None or Ek.zEmag is None:
|
|
986
|
+
raise ValueError("Per-component magnetic energy spectra are required for components.pdf.")
|
|
987
|
+
perpEmag, paraEmag = Ek.xEmag + Ek.yEmag, Ek.zEmag
|
|
988
|
+
|
|
989
|
+
numPerpVis, numParaVis = ds.xNumVis + ds.yNumVis, ds.zNumVis
|
|
990
|
+
numPerpRes, numParaRes = ds.xNumRes + ds.yNumRes, ds.zNumRes
|
|
991
|
+
phyPerpVis, phyParaVis = ds.xPhyVis + ds.yPhyVis, ds.zPhyVis
|
|
992
|
+
phyPerpRes, phyParaRes = ds.xPhyRes + ds.yPhyRes, ds.zPhyRes
|
|
993
|
+
else:
|
|
994
|
+
perpEkin, paraEkin = Ek.yEkin + Ek.zEkin, Ek.xEkin
|
|
995
|
+
if Ek.xEmag is None or Ek.yEmag is None or Ek.zEmag is None:
|
|
996
|
+
raise ValueError("Per-component magnetic energy spectra are required for components.pdf.")
|
|
997
|
+
perpEmag, paraEmag = Ek.yEmag + Ek.zEmag, Ek.xEmag
|
|
998
|
+
|
|
999
|
+
numPerpVis, numParaVis = ds.yNumVis + ds.zNumVis, ds.xNumVis
|
|
1000
|
+
numPerpRes, numParaRes = ds.yNumRes + ds.zNumRes, ds.xNumRes
|
|
1001
|
+
phyPerpVis, phyParaVis = ds.yPhyVis + ds.zPhyVis, ds.xPhyVis
|
|
1002
|
+
phyPerpRes, phyParaRes = ds.yPhyRes + ds.zPhyRes, ds.xPhyRes
|
|
1003
|
+
|
|
1004
|
+
totPerpVis = phyPerpVis + numPerpVis
|
|
1005
|
+
totParaVis = phyParaVis + numParaVis
|
|
1006
|
+
totPerpRes = phyPerpRes + numPerpRes
|
|
1007
|
+
totParaRes = phyParaRes + numParaRes
|
|
1008
|
+
|
|
1009
|
+
perpVis = getShellDissipationCurves(phyPerpVis, numPerpVis, totPerpVis, diss="vis", direction="perp")
|
|
1010
|
+
perpRes = getShellDissipationCurves(phyPerpRes, numPerpRes, totPerpRes, diss="res", direction="perp")
|
|
1011
|
+
paraVis = getShellDissipationCurves(phyParaVis, numParaVis, totParaVis, diss="vis", direction="para")
|
|
1012
|
+
paraRes = getShellDissipationCurves(phyParaRes, numParaRes, totParaRes, diss="res", direction="para")
|
|
1013
|
+
|
|
1014
|
+
perpKin = [Curve(get1D(perpEkin, mode="shell"), "0.5", r"$E_{\mathrm{kin},\perp}$")]
|
|
1015
|
+
perpMag = [Curve(get1D(perpEmag, mode="shell"), "0.5", r"$E_{\mathrm{mag},\perp}$")]
|
|
1016
|
+
paraKin = [Curve(get1D(paraEkin, mode="shell"), "0.5", r"$E_{\mathrm{kin},\parallel}$")]
|
|
1017
|
+
paraMag = [Curve(get1D(paraEmag, mode="shell"), "0.5", r"$E_{\mathrm{mag},\parallel}$")]
|
|
1018
|
+
|
|
1019
|
+
panels = [
|
|
1020
|
+
(
|
|
1021
|
+
perpVis, perpKin, 5.0 / 3.0,
|
|
1022
|
+
r"$k$", r"$-\mathscr{D}_{\perp}(k)$", r"$k^{5/3}E_{\perp}(k)$",
|
|
1023
|
+
),
|
|
1024
|
+
(
|
|
1025
|
+
perpRes, perpMag, 5.0 / 3.0,
|
|
1026
|
+
r"$k$", r"$-\mathscr{D}_{\perp}(k)$", r"$k^{5/3}E_{\perp}(k)$",
|
|
1027
|
+
),
|
|
1028
|
+
(
|
|
1029
|
+
paraVis, paraKin, 5.0 / 3.0,
|
|
1030
|
+
r"$k$", r"$-\mathscr{D}_{\parallel}(k)$", r"$k^{5/3}E_{\parallel}(k)$",
|
|
1031
|
+
),
|
|
1032
|
+
(
|
|
1033
|
+
paraRes, paraMag, 5.0 / 3.0,
|
|
1034
|
+
r"$k$", r"$-\mathscr{D}_{\parallel}(k)$", r"$k^{5/3}E_{\parallel}(k)$",
|
|
1035
|
+
),
|
|
1036
|
+
]
|
|
1037
|
+
kmax = float(np.max(perpKin[0].spc1d.k))
|
|
1038
|
+
plotCurves("components.pdf", panels, xlim=(2.0, kmax))
|
|
1039
|
+
|
|
1040
|
+
|
|
1041
|
+
def plotAnisotropicSpectra(ds: DissipationSpectra, spc: EnergySpectra, outdir: Path) -> None:
|
|
1042
|
+
"""Plot anisotropic dissipation and energy spectra for x/y/z components.
|
|
1043
|
+
|
|
1044
|
+
Outputs components.pdf to nd.outputdir.
|
|
1045
|
+
|
|
1046
|
+
Three panels (left to right: x, y, z component), shared y-axis.
|
|
1047
|
+
Left axis : physical, numerical, and total resistive dissipation spectra.
|
|
1048
|
+
Right axis: k^{3/2}-compensated component-wise kinetic and magnetic energy spectra.
|
|
1049
|
+
|
|
1050
|
+
Parameters
|
|
1051
|
+
----------
|
|
1052
|
+
ds : DissipationSpectra, the precomputed dissipation spectra.
|
|
1053
|
+
spc : EnergySpectra, the cached energy spectra.
|
|
1054
|
+
outdir: Path, the output directory.
|
|
1055
|
+
"""
|
|
1056
|
+
Ek = ds.Ek
|
|
1057
|
+
if Ek.xEmag is None or Ek.yEmag is None or Ek.zEmag is None:
|
|
1058
|
+
raise ValueError("Per-component magnetic energy spectra are required for components.pdf.")
|
|
1059
|
+
|
|
1060
|
+
directions = ("x", "y", "z")
|
|
1061
|
+
|
|
1062
|
+
# Shell-integrated component-wise resistive dissipation spectra
|
|
1063
|
+
phyRes = {
|
|
1064
|
+
"x": get1D(ds.xPhyRes, mode="shell", negative=True),
|
|
1065
|
+
"y": get1D(ds.yPhyRes, mode="shell", negative=True),
|
|
1066
|
+
"z": get1D(ds.zPhyRes, mode="shell", negative=True),
|
|
1067
|
+
}
|
|
1068
|
+
numRes = {
|
|
1069
|
+
"x": get1D(ds.xNumRes, mode="shell", negative=True),
|
|
1070
|
+
"y": get1D(ds.yNumRes, mode="shell", negative=True),
|
|
1071
|
+
"z": get1D(ds.zNumRes, mode="shell", negative=True),
|
|
1072
|
+
}
|
|
1073
|
+
totRes = {
|
|
1074
|
+
"x": get1D(ds.xTotRes, mode="shell", negative=True),
|
|
1075
|
+
"y": get1D(ds.yTotRes, mode="shell", negative=True),
|
|
1076
|
+
"z": get1D(ds.zTotRes, mode="shell", negative=True),
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
# Shell-integrated component-wise energy spectra
|
|
1080
|
+
ekin = {
|
|
1081
|
+
"x": get1D(Ek.xEkin, mode="shell"),
|
|
1082
|
+
"y": get1D(Ek.yEkin, mode="shell"),
|
|
1083
|
+
"z": get1D(Ek.zEkin, mode="shell"),
|
|
1084
|
+
}
|
|
1085
|
+
emag = {
|
|
1086
|
+
"x": get1D(Ek.xEmag, mode="shell"),
|
|
1087
|
+
"y": get1D(Ek.yEmag, mode="shell"),
|
|
1088
|
+
"z": get1D(Ek.zEmag, mode="shell"),
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
# ===== components.pdf =====
|
|
1092
|
+
fig, ax1s = plt.subplots(1, 3, figsize=(14, 5), sharey=True)
|
|
1093
|
+
ax2s: list[Axes] = []
|
|
1094
|
+
for ax1 in ax1s:
|
|
1095
|
+
ax2 = ax1.twinx()
|
|
1096
|
+
ax2.set_zorder(0)
|
|
1097
|
+
ax1.set_zorder(1)
|
|
1098
|
+
ax1.patch.set_visible(False)
|
|
1099
|
+
ax2s.append(ax2)
|
|
1100
|
+
ax2s[1].sharey(ax2s[0])
|
|
1101
|
+
ax2s[2].sharey(ax2s[0])
|
|
1102
|
+
|
|
1103
|
+
for i, direction in enumerate(directions):
|
|
1104
|
+
ax1 = ax1s[i]
|
|
1105
|
+
ax2 = ax2s[i]
|
|
1106
|
+
|
|
1107
|
+
# Left axis: numerical / physical / total (legend: num on top). Num = blue dashed, phy = red dashed.
|
|
1108
|
+
ax1.semilogx(
|
|
1109
|
+
numRes[direction].k, numRes[direction].Ek,
|
|
1110
|
+
color="b", ls="--", lw=2.5,
|
|
1111
|
+
label=r"${\mathscr{D}}^{\mathrm{num}}_{\mathrm{res},%s}$" % direction,
|
|
1112
|
+
)
|
|
1113
|
+
ax1.semilogx(
|
|
1114
|
+
phyRes[direction].k, phyRes[direction].Ek,
|
|
1115
|
+
color="r", ls="--", lw=2.5,
|
|
1116
|
+
label=r"${\mathscr{D}}^{\mathrm{phy}}_{\mathrm{res},%s}$" % direction,
|
|
1117
|
+
)
|
|
1118
|
+
ax1.semilogx(
|
|
1119
|
+
totRes[direction].k, totRes[direction].Ek,
|
|
1120
|
+
color="k", ls="-", lw=2.5,
|
|
1121
|
+
label=r"${\mathscr{D}}^{\mathrm{tot}}_{\mathrm{res},%s}$" % direction,
|
|
1122
|
+
)
|
|
1123
|
+
|
|
1124
|
+
# Right axis: component kinetic and magnetic energy spectra
|
|
1125
|
+
k = ekin[direction].k
|
|
1126
|
+
slope = 3.0 / 2.0
|
|
1127
|
+
ax2.loglog(
|
|
1128
|
+
k, ekin[direction].Ek * k**slope,
|
|
1129
|
+
color="k", ls="--", lw=1.5, alpha=0.8,
|
|
1130
|
+
label=r"$E_{\mathrm{kin},%s}$" % direction,
|
|
1131
|
+
)
|
|
1132
|
+
ax2.loglog(
|
|
1133
|
+
k, emag[direction].Ek * k**slope,
|
|
1134
|
+
color="r", ls="--", lw=1.5, alpha=0.8,
|
|
1135
|
+
label=r"$E_{\mathrm{mag},%s}$" % direction,
|
|
1136
|
+
)
|
|
1137
|
+
|
|
1138
|
+
ks = (
|
|
1139
|
+
phyRes[direction].k,
|
|
1140
|
+
numRes[direction].k,
|
|
1141
|
+
totRes[direction].k,
|
|
1142
|
+
ekin[direction].k,
|
|
1143
|
+
emag[direction].k,
|
|
1144
|
+
)
|
|
1145
|
+
kmin = float(min(float(np.min(ka)) for ka in ks))
|
|
1146
|
+
kmax = float(max(float(np.max(ka)) for ka in ks))
|
|
1147
|
+
ax1.set_xlim(kmin, kmax)
|
|
1148
|
+
|
|
1149
|
+
ax1.set_xlabel(r"$k$", fontsize=14)
|
|
1150
|
+
ax1.tick_params(axis="x", direction="in", which="both", labelsize=12, pad=5)
|
|
1151
|
+
ax1.tick_params(
|
|
1152
|
+
axis="y", direction="in", which="both", labelsize=12, pad=5,
|
|
1153
|
+
labelleft=(i == 0),
|
|
1154
|
+
)
|
|
1155
|
+
ax2.tick_params(
|
|
1156
|
+
axis="y", direction="in", which="both", labelsize=12, pad=5,
|
|
1157
|
+
labelright=(i == 2),
|
|
1158
|
+
)
|
|
1159
|
+
|
|
1160
|
+
# ax1.grid(True, which="both", ls="--", alpha=0.3)
|
|
1161
|
+
h1, l1 = ax1.get_legend_handles_labels()
|
|
1162
|
+
h2, l2 = ax2.get_legend_handles_labels()
|
|
1163
|
+
frame = ax1.legend(
|
|
1164
|
+
h1 + h2, l1 + l2,
|
|
1165
|
+
loc="lower left", bbox_to_anchor=(0.007, 0.007), fontsize=12,
|
|
1166
|
+
framealpha=1.0, fancybox=True, facecolor="white", edgecolor="k",
|
|
1167
|
+
).get_frame()
|
|
1168
|
+
frame.set_edgecolor("k")
|
|
1169
|
+
frame.set_linewidth(0.9)
|
|
1170
|
+
frame.set_boxstyle("round", pad=0.15, rounding_size=0.5)
|
|
1171
|
+
|
|
1172
|
+
_, ytop = ax1s[0].get_ylim()
|
|
1173
|
+
ax1s[0].set_ylim(0.0, ytop)
|
|
1174
|
+
|
|
1175
|
+
ax1s[0].set_ylabel(r"$-{\mathscr{D}}_{\mathrm{res}}(k)$", fontsize=16)
|
|
1176
|
+
ax2s[2].set_ylabel(r"$k^{3/2}E_i(k)$", fontsize=14)
|
|
1177
|
+
|
|
1178
|
+
fig.tight_layout()
|
|
1179
|
+
fig.subplots_adjust(wspace=0.07)
|
|
1180
|
+
fig.savefig(outdir / "components.pdf", bbox_inches="tight")
|
|
1181
|
+
plt.close(fig)
|
|
1182
|
+
|
|
1183
|
+
|
|
1184
|
+
def plotSpectra(nd: NumericalDissipation, spc: EnergySpectra) -> None:
|
|
1185
|
+
"""Plot dissipation spectra.
|
|
1186
|
+
|
|
1187
|
+
Extracts the energy spectra and computes the dissipation spectra only once,
|
|
1188
|
+
then shares them across all dissipation spectrum plotting functions.
|
|
1189
|
+
|
|
1190
|
+
Parameters
|
|
1191
|
+
----------
|
|
1192
|
+
nd : NumericalDissipation, the numerical dissipation object.
|
|
1193
|
+
spc: EnergySpectra, the cached energy spectra.
|
|
1194
|
+
"""
|
|
1195
|
+
ds = DissipationSpectra(nd, spc)
|
|
1196
|
+
outdir = Path(nd.outputdir)
|
|
1197
|
+
outdir.mkdir(parents=True, exist_ok=True)
|
|
1198
|
+
|
|
1199
|
+
plotShellSpectra(ds, spc, outdir)
|
|
1200
|
+
|
|
1201
|
+
if nd.type in ("Bx", "Bz"):
|
|
1202
|
+
plotAxisymmetricSpectra(ds, spc, outdir)
|
|
1203
|
+
|
|
1204
|
+
if nd.type == "MRI":
|
|
1205
|
+
plotAnisotropicSpectra(ds, spc, outdir)
|
|
1206
|
+
|
|
1207
|
+
|
|
1208
|
+
def plotHistogram(
|
|
1209
|
+
nd : NumericalDissipation,
|
|
1210
|
+
xcoverage : float = 0.95,
|
|
1211
|
+
ycoverage : float = 0.99,
|
|
1212
|
+
resolution: int = 400
|
|
1213
|
+
) -> None:
|
|
1214
|
+
"""Plot 2D histograms of dissipation terms (combined xyz only)
|
|
1215
|
+
|
|
1216
|
+
Outputs to nd.outputdir/histograms/:
|
|
1217
|
+
hist.num.res.pdf, hist.num.vis.pdf
|
|
1218
|
+
|
|
1219
|
+
Two rows:
|
|
1220
|
+
- first row = 2D JPDF from KDE;
|
|
1221
|
+
- second row = conditional mean and 68% interval per x bin
|
|
1222
|
+
|
|
1223
|
+
Parameters
|
|
1224
|
+
----------
|
|
1225
|
+
nd : NumericalDissipation object
|
|
1226
|
+
xcoverage : fraction of data enclosed by symmetric range for x data (lapl)
|
|
1227
|
+
ycoverage : fraction of data enclosed by symmetric range for y data (diss)
|
|
1228
|
+
resolution: histogram resolution (grid points per dimension for JPDF)
|
|
1229
|
+
"""
|
|
1230
|
+
outputdir = Path(nd.outputdir)
|
|
1231
|
+
outputdir.mkdir(parents=True, exist_ok=True)
|
|
1232
|
+
path = outputdir / "histograms"
|
|
1233
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
1234
|
+
|
|
1235
|
+
ticksize = 14
|
|
1236
|
+
plt.rcParams['xtick.labelsize'] = ticksize
|
|
1237
|
+
plt.rcParams['ytick.labelsize'] = ticksize
|
|
1238
|
+
xlabelpad, ylabelpad = 5, 2
|
|
1239
|
+
xlabelsize = 22
|
|
1240
|
+
ylabelsize = 24
|
|
1241
|
+
cbaroutlinewidth = 1.5
|
|
1242
|
+
|
|
1243
|
+
def xlabel(comp: str, term: str) -> str:
|
|
1244
|
+
if term == 'vis':
|
|
1245
|
+
return r'$u_' + comp + r'(\nabla \cdot \mathrm{\mathbb{T}})_' + comp + r'$'
|
|
1246
|
+
if term == 'res':
|
|
1247
|
+
return r'$B_' + comp + r'\nabla^2 B_' + comp + r'$'
|
|
1248
|
+
raise ValueError(f"Unsupported term {term!r}; expected 'vis' or 'res'.")
|
|
1249
|
+
|
|
1250
|
+
def ylabel(comp: str, term: str, mode: str | None) -> str:
|
|
1251
|
+
sub = r'_{\mathrm{' + term + r'}, ' + comp + r'}'
|
|
1252
|
+
if mode is not None:
|
|
1253
|
+
return r'${\mathscr{D}}^{\mathrm{' + mode + r'}}' + sub + r'$'
|
|
1254
|
+
return r'${\mathscr{D}}' + sub + r'$'
|
|
1255
|
+
|
|
1256
|
+
def flatten(diss: np.ndarray, lapl: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
|
|
1257
|
+
diss = diss.flatten()
|
|
1258
|
+
lapl = lapl.flatten()
|
|
1259
|
+
valid = np.isfinite(diss) & np.isfinite(lapl)
|
|
1260
|
+
return diss[valid], lapl[valid]
|
|
1261
|
+
|
|
1262
|
+
def KDE(lapl: np.ndarray, diss: np.ndarray) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
1263
|
+
"""Estimate JPDF using 2D KDE with KDEpy.FFTKDE
|
|
1264
|
+
|
|
1265
|
+
KDEpy docs: https://kdepy.readthedocs.io/en/latest/index.html
|
|
1266
|
+
|
|
1267
|
+
Returns hist on (resolution, resolution) grid, with xgrid, ygrid from KDEpy.
|
|
1268
|
+
grid: (resolution^2, 2) mesh points; points: density at each grid point.
|
|
1269
|
+
"""
|
|
1270
|
+
if len(lapl) == 0 or len(diss) == 0:
|
|
1271
|
+
raise ValueError("No valid finite data for KDE after flatten.")
|
|
1272
|
+
|
|
1273
|
+
xrange = float(np.percentile(np.abs(lapl), xcoverage * 100))
|
|
1274
|
+
yrange = float(np.percentile(np.abs(diss), ycoverage * 100))
|
|
1275
|
+
mask = (np.abs(lapl) <= xrange) & (np.abs(diss) <= yrange)
|
|
1276
|
+
lapl = lapl[mask]
|
|
1277
|
+
diss = diss[mask]
|
|
1278
|
+
|
|
1279
|
+
if len(lapl) == 0:
|
|
1280
|
+
raise ValueError("No data within coverage range for KDE.")
|
|
1281
|
+
|
|
1282
|
+
# KDEpy 2D: data shape (obs, dims), grid_points tuple = (n_x, n_y) per dimension
|
|
1283
|
+
# Normalize each dimension by its own sigma so a scalar bw applies uniformly.
|
|
1284
|
+
# Reference:
|
|
1285
|
+
# Scott, D.W. (1992) Multivariate Density Estimation. Theory, Practice and Visualization.
|
|
1286
|
+
# Silverman, Bernard W. Density estimation for statistics and data analysis. Routledge, 2018.
|
|
1287
|
+
def Silverman(data: np.ndarray) -> float:
|
|
1288
|
+
sigma = float(np.std(data, ddof=1))
|
|
1289
|
+
IQR = float(np.percentile(data, 75) - np.percentile(data, 25))
|
|
1290
|
+
N = len(data)
|
|
1291
|
+
bw = min(sigma, IQR/1.34) * N ** (-1.0 / 5)
|
|
1292
|
+
return float(bw)
|
|
1293
|
+
|
|
1294
|
+
bwx = Silverman(lapl)
|
|
1295
|
+
bwy = Silverman(diss)
|
|
1296
|
+
data = np.column_stack([lapl / bwx, diss / bwy])
|
|
1297
|
+
kde = FFTKDE(kernel='gaussian', bw=1).fit(data)
|
|
1298
|
+
|
|
1299
|
+
# Use an explicit grid so the plotting domain matches coverage range.
|
|
1300
|
+
# FFTKDE requires all data points to lie strictly inside the grid;
|
|
1301
|
+
# expand endpoints by a tiny epsilon to satisfy this when points land on boundary.
|
|
1302
|
+
epsilon = 1e-6
|
|
1303
|
+
xgrid = np.linspace(-xrange - epsilon, xrange + epsilon, resolution)
|
|
1304
|
+
ygrid = np.linspace(-yrange - epsilon, yrange + epsilon, resolution)
|
|
1305
|
+
# Custom FFTKDE grids must be sorted in cartesian-product order.
|
|
1306
|
+
grid = np.column_stack([
|
|
1307
|
+
np.repeat(xgrid / bwx, resolution),
|
|
1308
|
+
np.tile(ygrid / bwy, resolution),
|
|
1309
|
+
])
|
|
1310
|
+
points = kde.evaluate(grid)
|
|
1311
|
+
|
|
1312
|
+
hist = np.asarray(points).reshape(resolution, resolution).T / (bwx * bwy)
|
|
1313
|
+
|
|
1314
|
+
return hist, xgrid, ygrid
|
|
1315
|
+
|
|
1316
|
+
cmap = plt.get_cmap('inferno')
|
|
1317
|
+
|
|
1318
|
+
if nd.type == 'hydro':
|
|
1319
|
+
configs = [
|
|
1320
|
+
('vis', 'num', nd.V, nd.numVisTerm, nd.divStressT, nd.nu),
|
|
1321
|
+
]
|
|
1322
|
+
else:
|
|
1323
|
+
configs = [
|
|
1324
|
+
('res', 'num', nd.B, nd.numResTerm, nd.LaplacianB, nd.eta),
|
|
1325
|
+
('vis', 'num', nd.V, nd.numVisTerm, nd.divStressT, nd.nu),
|
|
1326
|
+
]
|
|
1327
|
+
|
|
1328
|
+
for term, mode, field, dissterm, laplacian, coeff in configs:
|
|
1329
|
+
|
|
1330
|
+
if dissterm is None or laplacian is None:
|
|
1331
|
+
continue
|
|
1332
|
+
|
|
1333
|
+
components = [
|
|
1334
|
+
('x', field.x * dissterm.x, field.x * laplacian.x),
|
|
1335
|
+
('y', field.y * dissterm.y, field.y * laplacian.y),
|
|
1336
|
+
('z', field.z * dissterm.z, field.z * laplacian.z)
|
|
1337
|
+
]
|
|
1338
|
+
|
|
1339
|
+
fig, axes = plt.subplots(2, 3, figsize=(18, 11.75), constrained_layout=True)
|
|
1340
|
+
layout_engine = fig.get_layout_engine()
|
|
1341
|
+
assert isinstance(layout_engine, ConstrainedLayoutEngine)
|
|
1342
|
+
layout_engine.set(wspace=0.04)
|
|
1343
|
+
|
|
1344
|
+
# First pass: compute KDE and conditional stats for all three components
|
|
1345
|
+
jpdflist: list[tuple[np.ndarray, np.ndarray, np.ndarray]] = []
|
|
1346
|
+
meanlist: list[tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]] = []
|
|
1347
|
+
|
|
1348
|
+
for comp, diss, lapl in components:
|
|
1349
|
+
diss, lapl = flatten(diss, lapl)
|
|
1350
|
+
hist, xgrid, ygrid = KDE(lapl, diss)
|
|
1351
|
+
|
|
1352
|
+
totals = hist.sum(axis=0)
|
|
1353
|
+
totals = np.where(totals > 0, totals, 1.0)
|
|
1354
|
+
conditional = hist / totals
|
|
1355
|
+
|
|
1356
|
+
cdf = np.cumsum(conditional, axis=0)
|
|
1357
|
+
cdf_last = cdf[-1, :]
|
|
1358
|
+
cdf_last = np.where(cdf_last > 0, cdf_last, 1.0)
|
|
1359
|
+
cdf = cdf / cdf_last
|
|
1360
|
+
|
|
1361
|
+
ymean = np.sum(conditional * ygrid[:, None], axis=0)
|
|
1362
|
+
|
|
1363
|
+
lower = np.zeros(cdf.shape[1])
|
|
1364
|
+
upper = np.zeros(cdf.shape[1])
|
|
1365
|
+
for i in range(cdf.shape[1]):
|
|
1366
|
+
lower[i] = np.interp(0.1585, cdf[:, i], ygrid)
|
|
1367
|
+
upper[i] = np.interp(0.8415, cdf[:, i], ygrid)
|
|
1368
|
+
|
|
1369
|
+
valid = (totals > 0) & np.isfinite(ymean) & (upper > lower)
|
|
1370
|
+
x = xgrid[valid]
|
|
1371
|
+
ymean = ymean[valid]
|
|
1372
|
+
lower = lower[valid]
|
|
1373
|
+
upper = upper[valid]
|
|
1374
|
+
|
|
1375
|
+
jpdflist.append((hist, xgrid, ygrid))
|
|
1376
|
+
meanlist.append((x, ymean, lower, upper))
|
|
1377
|
+
|
|
1378
|
+
# Compute unified y-axis range for second row
|
|
1379
|
+
xranges = [float(xgrid[-1]) for (_, xgrid, _) in jpdflist]
|
|
1380
|
+
ymeanranges = [
|
|
1381
|
+
float(np.max(np.abs(ymean))) if len(ymean) > 0 else 0.0
|
|
1382
|
+
for (_, ymean, _, _) in meanlist
|
|
1383
|
+
]
|
|
1384
|
+
kranges = [
|
|
1385
|
+
ymeanranges[i] / xranges[i] if xranges[i] > 0 else 0.0
|
|
1386
|
+
for i in range(3)
|
|
1387
|
+
]
|
|
1388
|
+
kmax = float(np.max(kranges))
|
|
1389
|
+
ymaxs = [xranges[i] * kmax * 1.2 for i in range(3)]
|
|
1390
|
+
|
|
1391
|
+
# Second pass: plot
|
|
1392
|
+
for idx, (comp, _, _) in enumerate(components):
|
|
1393
|
+
ax0 = axes[0, idx]
|
|
1394
|
+
ax1 = axes[1, idx]
|
|
1395
|
+
|
|
1396
|
+
hist, xgrid, ygrid = jpdflist[idx]
|
|
1397
|
+
x, ymean, lower, upper = meanlist[idx]
|
|
1398
|
+
ymax = ymaxs[idx]
|
|
1399
|
+
|
|
1400
|
+
extent = (float(xgrid[0]), float(xgrid[-1]), float(ygrid[0]), float(ygrid[-1]))
|
|
1401
|
+
|
|
1402
|
+
# ===== First row: JPDF =====
|
|
1403
|
+
visible = hist[(ygrid >= -ymax) & (ygrid <= ymax), :]
|
|
1404
|
+
positive = visible[visible > 0]
|
|
1405
|
+
if positive.size == 0:
|
|
1406
|
+
raise ValueError(
|
|
1407
|
+
f'JPDF color scale: no positive values in y-range '
|
|
1408
|
+
f'(term={term!r}, mode={mode!r}, comp={comp!r}, ymax={ymax})'
|
|
1409
|
+
)
|
|
1410
|
+
vmin = float(np.percentile(positive, 5))
|
|
1411
|
+
vmax = float(np.max(positive))
|
|
1412
|
+
if not (
|
|
1413
|
+
np.isfinite(vmin)
|
|
1414
|
+
and np.isfinite(vmax)
|
|
1415
|
+
and vmin > 0
|
|
1416
|
+
and vmax > 0
|
|
1417
|
+
and vmax > vmin
|
|
1418
|
+
):
|
|
1419
|
+
raise ValueError(
|
|
1420
|
+
f'LogNorm needs finite vmin, vmax with 0 < vmin < vmax; '
|
|
1421
|
+
f'got vmin={vmin}, vmax={vmax} (term={term!r}, mode={mode!r}, comp={comp!r})'
|
|
1422
|
+
)
|
|
1423
|
+
norm = LogNorm(vmin=vmin, vmax=vmax)
|
|
1424
|
+
|
|
1425
|
+
im = ax0.imshow(hist, origin='lower', extent=extent, cmap=cmap, aspect='auto', norm=norm)
|
|
1426
|
+
ax0.set_xlim(xgrid[0], xgrid[-1])
|
|
1427
|
+
ax0.set_ylim(-ymax, ymax)
|
|
1428
|
+
|
|
1429
|
+
ax0.set_xlabel(xlabel(comp, term), labelpad=xlabelpad, fontsize=xlabelsize)
|
|
1430
|
+
ax0.set_ylabel(ylabel(comp, term, mode), labelpad=ylabelpad, fontsize=ylabelsize)
|
|
1431
|
+
ax0.tick_params(direction='in', width=1.5, pad=7, labelsize=ticksize)
|
|
1432
|
+
for spine in ax0.spines.values():
|
|
1433
|
+
spine.set_linewidth(1.5)
|
|
1434
|
+
ax0.set_box_aspect(1)
|
|
1435
|
+
|
|
1436
|
+
cax = ax0.inset_axes((0, 1.035, 1, 0.06))
|
|
1437
|
+
cbar = fig.colorbar(im, cax=cax, orientation='horizontal')
|
|
1438
|
+
cbar.ax.xaxis.set_ticks_position('top')
|
|
1439
|
+
cbar.ax.xaxis.set_label_position('top')
|
|
1440
|
+
cbar.ax.tick_params(labelsize=12)
|
|
1441
|
+
cbar.outline.set_linewidth(cbaroutlinewidth) # type: ignore[union-attr]
|
|
1442
|
+
|
|
1443
|
+
# ===== Second row: conditional mean + 68% interval =====
|
|
1444
|
+
if len(x) > 0:
|
|
1445
|
+
ax1.scatter(
|
|
1446
|
+
x, ymean, marker='o', facecolors='k', edgecolors='k', s=24, alpha=1.0, zorder=2,
|
|
1447
|
+
label=r'mean ' + ylabel(comp, term, mode)
|
|
1448
|
+
)
|
|
1449
|
+
ax1.fill_between(
|
|
1450
|
+
x, lower, upper, alpha=0.2, color='gray', zorder=1,
|
|
1451
|
+
label=r'$68.3\%$ interval'
|
|
1452
|
+
)
|
|
1453
|
+
ax1.plot(x, lower, color='gray', linewidth=1, linestyle='--', zorder=1)
|
|
1454
|
+
ax1.plot(x, upper, color='gray', linewidth=1, linestyle='--', zorder=1)
|
|
1455
|
+
|
|
1456
|
+
if coeff != 0.0:
|
|
1457
|
+
|
|
1458
|
+
coefficient = {
|
|
1459
|
+
'vis': r'\nu',
|
|
1460
|
+
'res': r'\eta',
|
|
1461
|
+
}[term]
|
|
1462
|
+
|
|
1463
|
+
ax1.plot(
|
|
1464
|
+
x, coeff * x, color='r', linestyle='--', linewidth=3, zorder=3,
|
|
1465
|
+
label=rf'${coefficient} = {float2LaTeX(coeff)}$',
|
|
1466
|
+
)
|
|
1467
|
+
|
|
1468
|
+
frame = ax1.legend(
|
|
1469
|
+
loc='lower right', fontsize=16, bbox_to_anchor=(0.995, 0.005),
|
|
1470
|
+
framealpha=1.0, fancybox=True, facecolor="white", edgecolor="k",
|
|
1471
|
+
).get_frame()
|
|
1472
|
+
frame.set_edgecolor("k")
|
|
1473
|
+
frame.set_linewidth(0.8)
|
|
1474
|
+
frame.set_boxstyle("round", pad=0.15, rounding_size=0.4)
|
|
1475
|
+
|
|
1476
|
+
ax1.set_xlim(xgrid[0], xgrid[-1])
|
|
1477
|
+
ax1.set_ylim(-ymax, ymax)
|
|
1478
|
+
ax1.set_xlabel(xlabel(comp, term), labelpad=xlabelpad, fontsize=xlabelsize)
|
|
1479
|
+
ax1.set_ylabel(ylabel(comp, term, None), labelpad=ylabelpad, fontsize=ylabelsize)
|
|
1480
|
+
ax1.tick_params(direction='in', width=1.5, pad=7, labelsize=ticksize)
|
|
1481
|
+
for spine in ax1.spines.values():
|
|
1482
|
+
spine.set_linewidth(1.5)
|
|
1483
|
+
ax1.set_box_aspect(1)
|
|
1484
|
+
|
|
1485
|
+
plt.savefig(path / f'hist.{mode}.{term}.pdf', bbox_inches='tight')
|
|
1486
|
+
plt.close()
|
|
1487
|
+
|
|
1488
|
+
|
|
1489
|
+
def plot(
|
|
1490
|
+
nd : NumericalDissipation,
|
|
1491
|
+
fraction : float = 1.0,
|
|
1492
|
+
) -> None:
|
|
1493
|
+
"""Plot numerical dissipation slices and histograms
|
|
1494
|
+
|
|
1495
|
+
Parameters
|
|
1496
|
+
----------
|
|
1497
|
+
nd : NumericalDissipation object
|
|
1498
|
+
xcoverage : fraction of data enclosed by symmetric range for x (lapl)
|
|
1499
|
+
ycoverage : fraction of data enclosed by symmetric range for y (diss)
|
|
1500
|
+
fraction : float in (0, 1], passed to slice colormap scaling (see plotSlices).
|
|
1501
|
+
"""
|
|
1502
|
+
print("═════════ Result Visualization ═════════\n")
|
|
1503
|
+
print(f"Plotting dissipation term slices ...")
|
|
1504
|
+
plotSlices(nd, fraction=fraction)
|
|
1505
|
+
|
|
1506
|
+
print(f"Plotting histograms of numerical dissipation ...")
|
|
1507
|
+
plotHistogram(nd)
|
|
1508
|
+
|
|
1509
|
+
print("Plotting dissipation spectra ...")
|
|
1510
|
+
path = Path("spectra") / "spectra.pkl"
|
|
1511
|
+
if not path.is_file():
|
|
1512
|
+
raise FileNotFoundError(f"Energy spectra cache not found: {path}")
|
|
1513
|
+
|
|
1514
|
+
with path.open("rb") as f:
|
|
1515
|
+
spc = pickle.load(f)
|
|
1516
|
+
|
|
1517
|
+
plotSpectra(nd, spc)
|
|
1518
|
+
|
|
1519
|
+
print(f"All plots completed! Numerical dissipation analysis done.")
|