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/numdiss.py
ADDED
|
@@ -0,0 +1,598 @@
|
|
|
1
|
+
# PyMHD: Python for Magnetohydrodynamic Turbulence.
|
|
2
|
+
# Copyright (c) 2026 Yuyang Hua (华宇阳)
|
|
3
|
+
# License: MIT
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
pymhd/numdiss.py
|
|
7
|
+
----------------
|
|
8
|
+
|
|
9
|
+
Implements the framework for the analysis of numerical dissipation in (M)HD turbulence.
|
|
10
|
+
- NumericalDissipation class: computes and stores numerical dissipation terms
|
|
11
|
+
- DissipationSpectra class: computes and stores physical and numerical dissipation spectra
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
import pickle
|
|
18
|
+
|
|
19
|
+
from typing import Any, Sequence
|
|
20
|
+
|
|
21
|
+
from functools import partial
|
|
22
|
+
|
|
23
|
+
import time
|
|
24
|
+
|
|
25
|
+
from .turbulence import ScalarField, VectorField, Vector, Turbulence
|
|
26
|
+
from .derivatives import derivative, Algorithm
|
|
27
|
+
from .spectra import EnergySpectra, Spectrum
|
|
28
|
+
|
|
29
|
+
# Set flush=True to avoid buffer output
|
|
30
|
+
print = partial(print, flush=True)
|
|
31
|
+
|
|
32
|
+
def calculateFornbergWeights(
|
|
33
|
+
times: Sequence[float], t0: float, M: int
|
|
34
|
+
) -> np.ndarray:
|
|
35
|
+
"""Calculate Fornberg finite difference weights
|
|
36
|
+
|
|
37
|
+
Calculate finite difference weights on non-uniform grid with Fornberg's algorithm
|
|
38
|
+
|
|
39
|
+
References
|
|
40
|
+
----------
|
|
41
|
+
[1] Fornberg, Bengt. Mathematics of computation 51.184 (1988): 699-706.
|
|
42
|
+
|
|
43
|
+
Parameters
|
|
44
|
+
----------
|
|
45
|
+
times : time sequence
|
|
46
|
+
t0 : target time
|
|
47
|
+
M : maximum derivative order
|
|
48
|
+
|
|
49
|
+
Returns
|
|
50
|
+
-------
|
|
51
|
+
array of weights c[m, n, i], where:
|
|
52
|
+
- m: derivative order (0 to M)
|
|
53
|
+
- n: number of data points - 1 (0 to N)
|
|
54
|
+
- i: sum index (0 to n)
|
|
55
|
+
|
|
56
|
+
Usage
|
|
57
|
+
-----
|
|
58
|
+
f^(m)(t0) ≈ sum_i=0^n c[m, n, i] * f(times[i])
|
|
59
|
+
"""
|
|
60
|
+
N = len(times) - 1
|
|
61
|
+
c = np.zeros((M + 1, N + 1, N + 1))
|
|
62
|
+
c[0, 0, 0] = 1.0
|
|
63
|
+
c1 = 1.0
|
|
64
|
+
|
|
65
|
+
for n in range(1, N + 1):
|
|
66
|
+
c2 = 1.0
|
|
67
|
+
for i in range(n):
|
|
68
|
+
c3 = times[n] - times[i]
|
|
69
|
+
c2 *= c3
|
|
70
|
+
|
|
71
|
+
if n <= M:
|
|
72
|
+
c[n, n - 1, i] = 0
|
|
73
|
+
|
|
74
|
+
for m in range(0, min(n, M) + 1):
|
|
75
|
+
c[m, n, i] = ((times[n] - t0) * c[m, n - 1, i] - m * c[m - 1, n - 1, i]) / c3
|
|
76
|
+
|
|
77
|
+
for m in range(0, min(n, M) + 1):
|
|
78
|
+
c[m, n, n] = c1 / c2 * (m * c[m - 1, n - 1, n - 1] - (times[n - 1] - t0) * c[m, n - 1, n - 1])
|
|
79
|
+
|
|
80
|
+
c1 = c2
|
|
81
|
+
|
|
82
|
+
return c
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def computeTimeDerivative(
|
|
86
|
+
turbulence: Turbulence, t0: float
|
|
87
|
+
) -> tuple[VectorField, VectorField | None]:
|
|
88
|
+
"""Calculate time derivative of fields at t0
|
|
89
|
+
|
|
90
|
+
Parameters
|
|
91
|
+
----------
|
|
92
|
+
turbulence : Turbulence object containing consecutive time steps
|
|
93
|
+
t0 : target time for calculating derivative
|
|
94
|
+
|
|
95
|
+
Returns
|
|
96
|
+
-------
|
|
97
|
+
Vdot : time derivative of velocity field, dV/dt
|
|
98
|
+
Bdot : time derivative of magnetic field, dB/dt; None for hydro
|
|
99
|
+
"""
|
|
100
|
+
times = turbulence.times
|
|
101
|
+
N = len(times) - 1 # Order of accuracy
|
|
102
|
+
|
|
103
|
+
coeffs = calculateFornbergWeights(times, t0, 1)[1, N, :]
|
|
104
|
+
|
|
105
|
+
Vdot = sum((c * V for c, V in zip(coeffs, turbulence.Vs)), turbulence.Vs[0] * 0)
|
|
106
|
+
Bdot = None
|
|
107
|
+
if turbulence.type != 'hydro':
|
|
108
|
+
Bdot = sum((c * B for c, B in zip(coeffs, turbulence.Bs)), turbulence.Bs[0] * 0)
|
|
109
|
+
|
|
110
|
+
return Vdot, Bdot
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class NumericalDissipation:
|
|
114
|
+
"""Data container for numerical dissipation analysis
|
|
115
|
+
|
|
116
|
+
Computes and stores numerical dissipation terms in MHD turbulence.
|
|
117
|
+
|
|
118
|
+
Attributes
|
|
119
|
+
----------
|
|
120
|
+
type : turbulence type ('SSD', 'Bx', 'Bz', 'MRI', or 'hydro')
|
|
121
|
+
nu : kinematic viscosity
|
|
122
|
+
eta : resistivity (magnetic diffusivity)
|
|
123
|
+
outputdir : output directory name
|
|
124
|
+
|
|
125
|
+
phyVisTerm : VectorField, ν∇·T, where T = ρ[∇u+(∇u)ᵀ-(2/3)(∇·u)I] is the stress tensor
|
|
126
|
+
divStressT : VectorField, ∇·T (divergence of stress tensor)
|
|
127
|
+
phyResTerm : VectorField, η∇²B
|
|
128
|
+
LaplacianB : VectorField, ∇²B
|
|
129
|
+
numVisTerm : VectorField, D^num_vis
|
|
130
|
+
numResTerm : VectorField, D^num_res
|
|
131
|
+
|
|
132
|
+
rho : ScalarField, density field
|
|
133
|
+
V : VectorField, velocity field
|
|
134
|
+
B : VectorField, magnetic field
|
|
135
|
+
|
|
136
|
+
numVisRate : ScalarField, numerical viscous dissipation rate, V @ numVisTerm
|
|
137
|
+
numResRate : ScalarField, numerical resistive dissipation rate, B @ numResTerm
|
|
138
|
+
VdotStress : ScalarField, V·(∇·T), V @ divStressT
|
|
139
|
+
BdotLaplaB : ScalarField, B·(∇²B), B @ LaplacianB
|
|
140
|
+
phyVisRate : ScalarField, physical viscous dissipation rate, V @ phyVisTerm
|
|
141
|
+
phyResRate : ScalarField, physical resistive dissipation rate, B @ phyResTerm
|
|
142
|
+
"""
|
|
143
|
+
@staticmethod
|
|
144
|
+
def alg2dir(algorithm: Algorithm) -> str:
|
|
145
|
+
|
|
146
|
+
method = algorithm.method.upper()
|
|
147
|
+
stencil = algorithm.stencil
|
|
148
|
+
CT = algorithm.CT
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
'WENO' : f'numdiss(WENO{stencil})',
|
|
152
|
+
'TENO' : f'numdiss(TENO{stencil}-M,CT={CT})',
|
|
153
|
+
'TCS' : f'numdiss(TCS7-M,CT={CT})',
|
|
154
|
+
'CENTRAL' : 'numdiss(CENTRAL)',
|
|
155
|
+
'SPECTRAL': 'numdiss(SPECTRAL)'
|
|
156
|
+
}[method]
|
|
157
|
+
|
|
158
|
+
def load(self, path: Path) -> bool:
|
|
159
|
+
"""Load cached result if metadata matches current request."""
|
|
160
|
+
if not path.is_file():
|
|
161
|
+
return False
|
|
162
|
+
|
|
163
|
+
print(f"Loading NumericalDissipation cache from {path} ...")
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
with path.open('rb') as f:
|
|
167
|
+
obj = pickle.load(f)
|
|
168
|
+
except Exception as exc:
|
|
169
|
+
print(f"Failed to load cache: {exc}. Recomputing...\n")
|
|
170
|
+
return False
|
|
171
|
+
|
|
172
|
+
self.__dict__.update(obj.__dict__)
|
|
173
|
+
print("NumericalDissipation cache loaded.\n")
|
|
174
|
+
return True
|
|
175
|
+
|
|
176
|
+
def cache(self, path: Path) -> None:
|
|
177
|
+
"""Save computed result with low-overhead pickle serialization."""
|
|
178
|
+
try:
|
|
179
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
180
|
+
with path.open('wb') as f:
|
|
181
|
+
pickle.dump(self, f, protocol=pickle.HIGHEST_PROTOCOL)
|
|
182
|
+
except Exception as exc:
|
|
183
|
+
print(f"Failed to save cache: {exc}\n")
|
|
184
|
+
return
|
|
185
|
+
|
|
186
|
+
print(f"NumericalDissipation cache saved to ./{path}\n")
|
|
187
|
+
|
|
188
|
+
def __init__(
|
|
189
|
+
self,
|
|
190
|
+
turbulence: Turbulence | None = None,
|
|
191
|
+
algorithm : Algorithm = Algorithm(method = 'TENO', stencil = 7, CT = 0.01)
|
|
192
|
+
) -> None:
|
|
193
|
+
"""Compute numerical dissipation
|
|
194
|
+
|
|
195
|
+
Parameters
|
|
196
|
+
----------
|
|
197
|
+
turbulence: Turbulence or None, input Turbulence object
|
|
198
|
+
algorithm : high-order scheme in L^{OH}, defaults to TENO7-M
|
|
199
|
+
"""
|
|
200
|
+
self.outputdir = self.alg2dir(algorithm)
|
|
201
|
+
|
|
202
|
+
# if cache exists, load it
|
|
203
|
+
path = Path(self.outputdir) / "cache.pkl"
|
|
204
|
+
if self.load(path):
|
|
205
|
+
return
|
|
206
|
+
|
|
207
|
+
if turbulence is None:
|
|
208
|
+
raise ValueError("NumericalDissipation requires turbulence when cache is not valid.")
|
|
209
|
+
|
|
210
|
+
self.type = turbulence.type
|
|
211
|
+
self.Nx, self.Ny, self.Nz = turbulence.Nx, turbulence.Ny, turbulence.Nz
|
|
212
|
+
self.Lx, self.Ly, self.Lz = turbulence.Lx, turbulence.Ly, turbulence.Lz
|
|
213
|
+
|
|
214
|
+
self.nu = turbulence.nu
|
|
215
|
+
self.eta = turbulence.eta
|
|
216
|
+
|
|
217
|
+
print("┌──────────────────────────────────────┐")
|
|
218
|
+
print("│ │")
|
|
219
|
+
print("│ Numerical Dissipation Analysis │")
|
|
220
|
+
print("│ │")
|
|
221
|
+
print("└──────────────────────────────────────┘")
|
|
222
|
+
|
|
223
|
+
print("")
|
|
224
|
+
print("═════════════ Computation ══════════════")
|
|
225
|
+
print("")
|
|
226
|
+
|
|
227
|
+
start_time = time.time()
|
|
228
|
+
results = self.compute(turbulence, algorithm)
|
|
229
|
+
self.phyResTerm = results["phyResTerm"]
|
|
230
|
+
self.LaplacianB = results["LaplacianB"]
|
|
231
|
+
self.phyVisTerm = results["phyVisTerm"]
|
|
232
|
+
self.divStressT = results["divStressT"]
|
|
233
|
+
self.numVisTerm = results["numVisTerm"]
|
|
234
|
+
self.numResTerm = results["numResTerm"]
|
|
235
|
+
self.rho = results["rho"]
|
|
236
|
+
self.V = results["V"]
|
|
237
|
+
self.B = results["B"]
|
|
238
|
+
self.numVisRate = results["numVisRate"]
|
|
239
|
+
self.numResRate = results["numResRate"]
|
|
240
|
+
self.VdotStress = results["VdotStress"]
|
|
241
|
+
self.BdotLaplaB = results["BdotLaplaB"]
|
|
242
|
+
self.phyVisRate = results["phyVisRate"]
|
|
243
|
+
self.phyResRate = results["phyResRate"]
|
|
244
|
+
self.cache(path)
|
|
245
|
+
end_time = time.time()
|
|
246
|
+
|
|
247
|
+
@staticmethod
|
|
248
|
+
def compute(
|
|
249
|
+
turbulence: Turbulence,
|
|
250
|
+
algorithm : Algorithm,
|
|
251
|
+
) -> dict[str, Any]:
|
|
252
|
+
|
|
253
|
+
center = len(turbulence.times) // 2
|
|
254
|
+
t0 = turbulence.times[center]
|
|
255
|
+
|
|
256
|
+
rho = turbulence.rhos[center]
|
|
257
|
+
p = turbulence.ps[center]
|
|
258
|
+
V = turbulence.Vs[center]
|
|
259
|
+
Vdot, Bdot = computeTimeDerivative(turbulence, t0)
|
|
260
|
+
|
|
261
|
+
acc = None
|
|
262
|
+
if turbulence.type != 'MRI':
|
|
263
|
+
accs = getattr(turbulence, 'accs', None)
|
|
264
|
+
if accs is None or len(accs) == 0:
|
|
265
|
+
raise ValueError("NumericalDissipation for forced turbulence requires acceleration field.")
|
|
266
|
+
acc = accs[center]
|
|
267
|
+
|
|
268
|
+
nu = turbulence.nu
|
|
269
|
+
eta = turbulence.eta
|
|
270
|
+
|
|
271
|
+
if turbulence.solver == 'FVM':
|
|
272
|
+
print(f"Converting cell averages to cell centers...")
|
|
273
|
+
average2center = partial(derivative.average2center, algorithm=algorithm)
|
|
274
|
+
start_time = time.time()
|
|
275
|
+
|
|
276
|
+
rho = average2center(rho)
|
|
277
|
+
p = average2center(p)
|
|
278
|
+
V = average2center(V)
|
|
279
|
+
Vdot = average2center(Vdot)
|
|
280
|
+
|
|
281
|
+
end_time = time.time()
|
|
282
|
+
print(f"rho, p, V, Vdot converted! Time: {end_time - start_time:.2f} s\n")
|
|
283
|
+
|
|
284
|
+
print(f"Computing numerical dissipation terms...")
|
|
285
|
+
start_time = time.time()
|
|
286
|
+
|
|
287
|
+
Dx = partial(derivative.Dx, algorithm=algorithm)
|
|
288
|
+
Dy = partial(derivative.Dy, algorithm=algorithm)
|
|
289
|
+
Dz = partial(derivative.Dz, algorithm=algorithm)
|
|
290
|
+
grad = partial(derivative.grad, algorithm=algorithm)
|
|
291
|
+
div = partial(derivative.div, algorithm=algorithm)
|
|
292
|
+
curl = partial(derivative.curl, algorithm=algorithm)
|
|
293
|
+
laplacian = partial(derivative.laplacian, algorithm=algorithm)
|
|
294
|
+
|
|
295
|
+
Nx, Ny, Nz = rho.data.shape
|
|
296
|
+
box = rho.box
|
|
297
|
+
Lx, Ly, Lz = box
|
|
298
|
+
dx, dy, dz = rho.dx, rho.dy, rho.dz
|
|
299
|
+
|
|
300
|
+
xs = np.linspace(-Lx / 2, Lx / 2, Nx, endpoint=False) + dx / 2
|
|
301
|
+
ys = np.linspace(-Ly / 2, Ly / 2, Ny, endpoint=False) + dy / 2
|
|
302
|
+
zs = np.linspace(-Lz / 2, Lz / 2, Nz, endpoint=False) + dz / 2
|
|
303
|
+
X, Y, Z = np.meshgrid(xs, ys, zs, indexing='ij')
|
|
304
|
+
|
|
305
|
+
x = ScalarField(X, box)
|
|
306
|
+
|
|
307
|
+
# Unit vectors
|
|
308
|
+
Ex = Vector(1, 0, 0)
|
|
309
|
+
Ey = Vector(0, 1, 0)
|
|
310
|
+
Ez = Vector(0, 0, 1)
|
|
311
|
+
|
|
312
|
+
Vx = ScalarField(V.x, V.box)
|
|
313
|
+
Vy = ScalarField(V.y, V.box)
|
|
314
|
+
Vz = ScalarField(V.z, V.box)
|
|
315
|
+
|
|
316
|
+
B: VectorField | None = None
|
|
317
|
+
if turbulence.type != 'hydro':
|
|
318
|
+
B = turbulence.Bs[center]
|
|
319
|
+
|
|
320
|
+
if turbulence.type in ('SSD', 'Bx', 'Bz'):
|
|
321
|
+
|
|
322
|
+
assert Bdot is not None
|
|
323
|
+
assert B is not None
|
|
324
|
+
J = curl(B)
|
|
325
|
+
|
|
326
|
+
assert acc is not None # guaranteed by __init__
|
|
327
|
+
|
|
328
|
+
Faraday = curl(V ** B)
|
|
329
|
+
LaplacianB = laplacian(B)
|
|
330
|
+
|
|
331
|
+
convective = rho * (Vx * Dx(V) + Vy * Dy(V) + Vz * Dz(V))
|
|
332
|
+
pressure = -grad(p)
|
|
333
|
+
Lorentz = J ** B
|
|
334
|
+
|
|
335
|
+
Tx = rho * (grad(Vx) + Dx(V) - (2 / 3) * div(V) * Ex)
|
|
336
|
+
Ty = rho * (grad(Vy) + Dy(V) - (2 / 3) * div(V) * Ey)
|
|
337
|
+
Tz = rho * (grad(Vz) + Dz(V) - (2 / 3) * div(V) * Ez)
|
|
338
|
+
|
|
339
|
+
divStressT = div(Tx) * Ex + div(Ty) * Ey + div(Tz) * Ez
|
|
340
|
+
phyResTerm = eta * LaplacianB
|
|
341
|
+
phyVisTerm = nu * divStressT
|
|
342
|
+
|
|
343
|
+
# ===== Numerical dissipation =====
|
|
344
|
+
NavierStokesLHS = rho * Vdot + convective
|
|
345
|
+
NavierStokesRHS = pressure + Lorentz + phyVisTerm + rho * acc
|
|
346
|
+
|
|
347
|
+
numVisTerm = NavierStokesLHS - NavierStokesRHS
|
|
348
|
+
numResTerm = Bdot - Faraday - phyResTerm
|
|
349
|
+
|
|
350
|
+
elif turbulence.type == 'MRI':
|
|
351
|
+
|
|
352
|
+
assert Bdot is not None
|
|
353
|
+
assert B is not None
|
|
354
|
+
Bx = ScalarField(B.x, B.box)
|
|
355
|
+
J = curl(B)
|
|
356
|
+
|
|
357
|
+
Omega = turbulence.Omega
|
|
358
|
+
q = turbulence.q
|
|
359
|
+
|
|
360
|
+
# ===== Magnetic induction equation =====
|
|
361
|
+
Faraday1 = -q * Omega * Bx * Ey
|
|
362
|
+
Faraday2 = q * Omega * x * Dy(B)
|
|
363
|
+
Faraday3 = curl(V ** B)
|
|
364
|
+
|
|
365
|
+
Faraday = Faraday1 + Faraday2 + Faraday3
|
|
366
|
+
phyResTerm = eta * laplacian(B)
|
|
367
|
+
LaplacianB = laplacian(B)
|
|
368
|
+
|
|
369
|
+
# ===== Navier-Stokes equation =====
|
|
370
|
+
convective = rho * (Vx * Dx(V) + Vy * Dy(V) + Vz * Dz(V) - q * Omega * x * Dy(V) - q * Omega * Vx * Ey)
|
|
371
|
+
pressure = -grad(p)
|
|
372
|
+
Coriolis = -2 * rho * Omega * (Ez ** V)
|
|
373
|
+
Lorentz = J ** B
|
|
374
|
+
|
|
375
|
+
Tx = rho * (grad(Vx) + Dx(V) - (2 / 3) * div(V) * Ex)
|
|
376
|
+
Ty = rho * (grad(Vy) + Dy(V) - (2 / 3) * div(V) * Ey)
|
|
377
|
+
Tz = rho * (grad(Vz) + Dz(V) - (2 / 3) * div(V) * Ez)
|
|
378
|
+
divTx, divTy, divTz = div(Tx), div(Ty), div(Tz)
|
|
379
|
+
divT = divTx * Ex + divTy * Ey + divTz * Ez
|
|
380
|
+
divStressT = divT
|
|
381
|
+
phyVisTerm = nu * divT
|
|
382
|
+
|
|
383
|
+
# ===== Numerical dissipation =====
|
|
384
|
+
NavierStokesLHS = rho * Vdot + convective
|
|
385
|
+
NavierStokesRHS = pressure + Coriolis + Lorentz + phyVisTerm
|
|
386
|
+
|
|
387
|
+
numVisTerm = NavierStokesLHS - NavierStokesRHS
|
|
388
|
+
numResTerm = Bdot - Faraday - phyResTerm
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
elif turbulence.type == 'hydro':
|
|
392
|
+
|
|
393
|
+
convective = rho * (Vx * Dx(V) + Vy * Dy(V) + Vz * Dz(V))
|
|
394
|
+
pressure = -grad(p)
|
|
395
|
+
|
|
396
|
+
Tx = rho * (grad(Vx) + Dx(V) - (2 / 3) * div(V) * Ex)
|
|
397
|
+
Ty = rho * (grad(Vy) + Dy(V) - (2 / 3) * div(V) * Ey)
|
|
398
|
+
Tz = rho * (grad(Vz) + Dz(V) - (2 / 3) * div(V) * Ez)
|
|
399
|
+
|
|
400
|
+
divStressT = div(Tx) * Ex + div(Ty) * Ey + div(Tz) * Ez
|
|
401
|
+
phyVisTerm = nu * divStressT
|
|
402
|
+
|
|
403
|
+
assert acc is not None
|
|
404
|
+
NavierStokesLHS = rho * Vdot + convective
|
|
405
|
+
NavierStokesRHS = pressure + phyVisTerm + rho * acc
|
|
406
|
+
|
|
407
|
+
numVisTerm = NavierStokesLHS - NavierStokesRHS
|
|
408
|
+
|
|
409
|
+
phyResTerm = None
|
|
410
|
+
LaplacianB = None
|
|
411
|
+
numResTerm = None
|
|
412
|
+
|
|
413
|
+
else:
|
|
414
|
+
raise ValueError(
|
|
415
|
+
f"Unsupported type: {turbulence.type!r}; expected 'SSD', 'Bx', 'Bz', 'MRI', or 'hydro'."
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
end_time = time.time()
|
|
419
|
+
print(f"Numerical dissipation computation completed! Time: {end_time - start_time:.2f} s")
|
|
420
|
+
|
|
421
|
+
numVisRate = V @ numVisTerm
|
|
422
|
+
VdotStress = V @ divStressT
|
|
423
|
+
phyVisRate = V @ phyVisTerm
|
|
424
|
+
|
|
425
|
+
if B is not None:
|
|
426
|
+
assert numResTerm is not None
|
|
427
|
+
assert phyResTerm is not None
|
|
428
|
+
assert LaplacianB is not None
|
|
429
|
+
numResRate = B @ numResTerm
|
|
430
|
+
BdotLaplaB = B @ LaplacianB
|
|
431
|
+
phyResRate = B @ phyResTerm
|
|
432
|
+
else:
|
|
433
|
+
numResRate = None
|
|
434
|
+
BdotLaplaB = None
|
|
435
|
+
phyResRate = None
|
|
436
|
+
|
|
437
|
+
results: dict[str, Any] = {
|
|
438
|
+
"phyResTerm": phyResTerm,
|
|
439
|
+
"LaplacianB": LaplacianB,
|
|
440
|
+
"phyVisTerm": phyVisTerm,
|
|
441
|
+
"divStressT": divStressT,
|
|
442
|
+
"numVisTerm": numVisTerm,
|
|
443
|
+
"numResTerm": numResTerm,
|
|
444
|
+
"rho" : rho,
|
|
445
|
+
"V" : V,
|
|
446
|
+
"B" : B,
|
|
447
|
+
"numVisRate": numVisRate,
|
|
448
|
+
"numResRate": numResRate,
|
|
449
|
+
"VdotStress": VdotStress,
|
|
450
|
+
"BdotLaplaB": BdotLaplaB,
|
|
451
|
+
"phyVisRate": phyVisRate,
|
|
452
|
+
"phyResRate": phyResRate,
|
|
453
|
+
}
|
|
454
|
+
return results
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def computeSpectra(
|
|
458
|
+
field1: VectorField | None, field2: VectorField | None
|
|
459
|
+
) -> tuple[Spectrum, Spectrum, Spectrum, Spectrum]:
|
|
460
|
+
r"""Compute component-wise and total spectra
|
|
461
|
+
|
|
462
|
+
Parameters
|
|
463
|
+
----------
|
|
464
|
+
field1: VectorField, the first field
|
|
465
|
+
field2: VectorField, the second field
|
|
466
|
+
|
|
467
|
+
Returns
|
|
468
|
+
-------
|
|
469
|
+
tuple[Spectrum, Spectrum, Spectrum, Spectrum]: the component-wise and total spectra
|
|
470
|
+
|
|
471
|
+
Raises
|
|
472
|
+
------
|
|
473
|
+
ValueError: if field1 or field2 is None or if field1 and field2 do not match
|
|
474
|
+
"""
|
|
475
|
+
if field1 is None or field2 is None:
|
|
476
|
+
raise ValueError("computeSpectra(): field1 and field2 must not be None.")
|
|
477
|
+
|
|
478
|
+
Nx1, Ny1, Nz1 = field1.x.shape
|
|
479
|
+
Nx2, Ny2, Nz2 = field2.x.shape
|
|
480
|
+
|
|
481
|
+
if (Nx1, Ny1, Nz1) != (Nx2, Ny2, Nz2):
|
|
482
|
+
raise ValueError(
|
|
483
|
+
"computeSpectra(): field1 and field2 must have the same resolution, "
|
|
484
|
+
f"got {(Nx1, Ny1, Nz1)} and {(Nx2, Ny2, Nz2)}."
|
|
485
|
+
)
|
|
486
|
+
if field1.box != field2.box:
|
|
487
|
+
raise ValueError(
|
|
488
|
+
"computeSpectra(): field1 and field2 must share the same box, "
|
|
489
|
+
f"got {field1.box} and {field2.box}."
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
dV = field1.dxdydz
|
|
493
|
+
box = field1.box
|
|
494
|
+
|
|
495
|
+
f1x = np.fft.fftn(field1.x) * dV
|
|
496
|
+
f1y = np.fft.fftn(field1.y) * dV
|
|
497
|
+
f1z = np.fft.fftn(field1.z) * dV
|
|
498
|
+
|
|
499
|
+
f2x = np.fft.fftn(field2.x) * dV
|
|
500
|
+
f2y = np.fft.fftn(field2.y) * dV
|
|
501
|
+
f2z = np.fft.fftn(field2.z) * dV
|
|
502
|
+
|
|
503
|
+
Sx = Spectrum(np.real(np.conjugate(f1x) * f2x), box)
|
|
504
|
+
Sy = Spectrum(np.real(np.conjugate(f1y) * f2y), box)
|
|
505
|
+
Sz = Spectrum(np.real(np.conjugate(f1z) * f2z), box)
|
|
506
|
+
Stot = Sx + Sy + Sz
|
|
507
|
+
|
|
508
|
+
return (Sx, Sy, Sz, Stot)
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
class DissipationSpectra:
|
|
512
|
+
"""Container for dissipation spectra used by numerical dissipation plots.
|
|
513
|
+
|
|
514
|
+
Attributes
|
|
515
|
+
----------
|
|
516
|
+
Lx, Ly, Lz: float, size of the box in x, y, z directions
|
|
517
|
+
nu, eta : float, kinematic viscosity and resistivity (magnetic diffusivity)
|
|
518
|
+
|
|
519
|
+
Ek: EnergySpectra, cached energy spectra (component and total E_{kin} / E_{mag}).
|
|
520
|
+
|
|
521
|
+
For nd.type == 'SSD', only the summed spectra are specified:
|
|
522
|
+
- eNumVis, eNumRes, ePhyVis, ePhyRes, eTotVis, eTotRes.
|
|
523
|
+
|
|
524
|
+
Otherwise (e.g. 'Bx', 'Bz', 'MRI'):
|
|
525
|
+
- xNumVis, yNumVis, zNumVis: component-wise numerical viscous dissipation spectra
|
|
526
|
+
- xNumRes, yNumRes, zNumRes: component-wise numerical resistive dissipation spectra
|
|
527
|
+
- xPhyVis, yPhyVis, zPhyVis: component-wise physical viscous dissipation spectra
|
|
528
|
+
- xPhyRes, yPhyRes, zPhyRes: component-wise physical resistive dissipation spectra
|
|
529
|
+
|
|
530
|
+
- eNumVis, eNumRes: sums of the three num components
|
|
531
|
+
- ePhyVis, ePhyRes: sums of the three phy components
|
|
532
|
+
|
|
533
|
+
- xTotVis, yTotVis, zTotVis: per-component viscous total (phy + num)
|
|
534
|
+
- xTotRes, yTotRes, zTotRes: per-component resistive total (phy + num)
|
|
535
|
+
- eTotVis, eTotRes: summed total dissipation
|
|
536
|
+
|
|
537
|
+
- xUdivT, yUdivT, zUdivT, eUdivT: physical viscous dissipation spectra without nu
|
|
538
|
+
- xBLapB, yBLapB, zBLapB, eBLapB: physical resistive dissipation spectra without eta
|
|
539
|
+
"""
|
|
540
|
+
def cache(self, path: Path) -> None:
|
|
541
|
+
|
|
542
|
+
try:
|
|
543
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
544
|
+
with path.open("wb") as f:
|
|
545
|
+
pickle.dump(self, f, protocol=pickle.HIGHEST_PROTOCOL)
|
|
546
|
+
except Exception as exc:
|
|
547
|
+
print(f"Failed to save dissipation spectra cache: {exc}\n")
|
|
548
|
+
return
|
|
549
|
+
|
|
550
|
+
print(f"DissipationSpectra cache saved to ./{path}\n")
|
|
551
|
+
|
|
552
|
+
def __init__(
|
|
553
|
+
self,
|
|
554
|
+
nd: NumericalDissipation,
|
|
555
|
+
Ek: EnergySpectra,
|
|
556
|
+
) -> None:
|
|
557
|
+
|
|
558
|
+
self.box = nd.V.box
|
|
559
|
+
self.Lx, self.Ly, self.Lz = self.box
|
|
560
|
+
|
|
561
|
+
self.nu = nd.nu
|
|
562
|
+
self.eta = nd.eta
|
|
563
|
+
self.Ek = Ek
|
|
564
|
+
|
|
565
|
+
if nd.type == "SSD":
|
|
566
|
+
*_, self.eNumVis = computeSpectra(nd.V, nd.numVisTerm)
|
|
567
|
+
*_, self.eNumRes = computeSpectra(nd.B, nd.numResTerm)
|
|
568
|
+
*_, self.ePhyVis = computeSpectra(nd.V, nd.phyVisTerm)
|
|
569
|
+
*_, self.ePhyRes = computeSpectra(nd.B, nd.phyResTerm)
|
|
570
|
+
|
|
571
|
+
self.eTotVis, self.eTotRes = self.ePhyVis + self.eNumVis, self.ePhyRes + self.eNumRes
|
|
572
|
+
|
|
573
|
+
else:
|
|
574
|
+
xNumVis, yNumVis, zNumVis, eNumVis = computeSpectra(nd.V, nd.numVisTerm)
|
|
575
|
+
xNumRes, yNumRes, zNumRes, eNumRes = computeSpectra(nd.B, nd.numResTerm)
|
|
576
|
+
xPhyVis, yPhyVis, zPhyVis, ePhyVis = computeSpectra(nd.V, nd.phyVisTerm)
|
|
577
|
+
xPhyRes, yPhyRes, zPhyRes, ePhyRes = computeSpectra(nd.B, nd.phyResTerm)
|
|
578
|
+
|
|
579
|
+
self.xNumVis, self.yNumVis, self.zNumVis = xNumVis, yNumVis, zNumVis
|
|
580
|
+
self.xNumRes, self.yNumRes, self.zNumRes = xNumRes, yNumRes, zNumRes
|
|
581
|
+
self.xPhyVis, self.yPhyVis, self.zPhyVis = xPhyVis, yPhyVis, zPhyVis
|
|
582
|
+
self.xPhyRes, self.yPhyRes, self.zPhyRes = xPhyRes, yPhyRes, zPhyRes
|
|
583
|
+
|
|
584
|
+
self.eNumVis, self.eNumRes = eNumVis, eNumRes
|
|
585
|
+
self.ePhyVis, self.ePhyRes = ePhyVis, ePhyRes
|
|
586
|
+
|
|
587
|
+
self.xTotVis, self.yTotVis, self.zTotVis = xPhyVis + xNumVis, yPhyVis + yNumVis, zPhyVis + zNumVis
|
|
588
|
+
self.xTotRes, self.yTotRes, self.zTotRes = xPhyRes + xNumRes, yPhyRes + yNumRes, zPhyRes + zNumRes
|
|
589
|
+
self.eTotVis, self.eTotRes = ePhyVis + eNumVis, ePhyRes + eNumRes
|
|
590
|
+
|
|
591
|
+
xUdivT, yUdivT, zUdivT, eUdivT = computeSpectra(nd.V, nd.divStressT)
|
|
592
|
+
xBLapB, yBLapB, zBLapB, eBLapB = computeSpectra(nd.B, nd.LaplacianB)
|
|
593
|
+
|
|
594
|
+
self.xUdivT, self.yUdivT, self.zUdivT = xUdivT, yUdivT, zUdivT
|
|
595
|
+
self.xBLapB, self.yBLapB, self.zBLapB = xBLapB, yBLapB, zBLapB
|
|
596
|
+
self.eUdivT, self.eBLapB = eUdivT, eBLapB
|
|
597
|
+
|
|
598
|
+
self.cache(Path(nd.outputdir) / "nd.spectra.pkl")
|
pymhd/plot/__init__.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# PyMHD: Python for Magnetohydrodynamic Turbulence.
|
|
2
|
+
# Copyright (c) 2026 Yuyang Hua (华宇阳)
|
|
3
|
+
# License: MIT
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
pymhd/plot/__init__.py
|
|
7
|
+
----------------------
|
|
8
|
+
|
|
9
|
+
Plotting tools for PyMHD.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from ..numdiss import NumericalDissipation
|
|
15
|
+
from ..spectra import EnergySpectra
|
|
16
|
+
|
|
17
|
+
from .nd import plot as plotNumericalDissipation
|
|
18
|
+
from .spc import plot as plotSpectra
|
|
19
|
+
|
|
20
|
+
def plot(obj: NumericalDissipation | EnergySpectra, **kwargs) -> None:
|
|
21
|
+
"""Unified plot function for PyMHD
|
|
22
|
+
|
|
23
|
+
Route to the appropriate plot function for each object type.
|
|
24
|
+
|
|
25
|
+
Parameters
|
|
26
|
+
----------
|
|
27
|
+
obj : NumericalDissipation | EnergySpectra, a plottable PyMHD object
|
|
28
|
+
**kwargs: passed to the plot function of the object (e.g. fraction for colormap scaling).
|
|
29
|
+
|
|
30
|
+
Examples
|
|
31
|
+
--------
|
|
32
|
+
>>> from pymhd import plot, NumericalDissipation, EnergySpectra
|
|
33
|
+
>>> spectra = EnergySpectra(...)
|
|
34
|
+
>>> plot(spectra)
|
|
35
|
+
>>> nd = NumericalDissipation(...)
|
|
36
|
+
>>> plot(nd)
|
|
37
|
+
"""
|
|
38
|
+
if isinstance(obj, NumericalDissipation):
|
|
39
|
+
plotNumericalDissipation(obj, **kwargs)
|
|
40
|
+
return
|
|
41
|
+
if isinstance(obj, EnergySpectra):
|
|
42
|
+
plotSpectra(obj)
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
raise TypeError(
|
|
46
|
+
f"plot() does not support type '{type(obj).__name__}'. "
|
|
47
|
+
f"Supported types: NumericalDissipation, EnergySpectra"
|
|
48
|
+
)
|