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
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Adapted from AthenaK (https://github.com/IAS-Astrophysics/athenak)
|
|
2
|
+
# Copyright (c) 2020, Institute for Advanced Study / High-Performance Computing / jmstone / Athena-Parthenon
|
|
3
|
+
# Licensed under BSD-3-Clause License
|
|
4
|
+
|
|
5
|
+
# A simple script for converting a collection of .bin files to .athdf/.xdmf files using
|
|
6
|
+
# bin_convert
|
|
7
|
+
|
|
8
|
+
# Python modules
|
|
9
|
+
import os
|
|
10
|
+
import argparse
|
|
11
|
+
import glob
|
|
12
|
+
|
|
13
|
+
# AthenaK modules
|
|
14
|
+
import bin_convert
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# Main function
|
|
18
|
+
def main(**kwargs):
|
|
19
|
+
# Get the root name for the file.
|
|
20
|
+
files = glob.glob(kwargs['file_stem'] + '*.bin')
|
|
21
|
+
if len(files) < 1:
|
|
22
|
+
print(f"No files found with stem {kwargs['file_stem']}")
|
|
23
|
+
quit()
|
|
24
|
+
|
|
25
|
+
total = len(files)
|
|
26
|
+
count = 1
|
|
27
|
+
|
|
28
|
+
for fname in files:
|
|
29
|
+
athdf_name = fname.replace(".bin", ".athdf")
|
|
30
|
+
xdmf_name = athdf_name + ".xdmf"
|
|
31
|
+
filedata = bin_convert.read_binary(fname)
|
|
32
|
+
bin_convert.write_athdf(athdf_name, filedata)
|
|
33
|
+
bin_convert.write_xdmf_for(xdmf_name, os.path.basename(athdf_name), filedata)
|
|
34
|
+
if kwargs['verbose']:
|
|
35
|
+
print(f'Converting {count}/{total}: {fname}')
|
|
36
|
+
count = count+1
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
if __name__ == '__main__':
|
|
40
|
+
parser = argparse.ArgumentParser()
|
|
41
|
+
parser.add_argument('file_stem', help='path to files, excluding .#.bin')
|
|
42
|
+
parser.add_argument('-v', '--verbose', action='store_true',
|
|
43
|
+
help='print file conversion progress')
|
|
44
|
+
args = parser.parse_args()
|
|
45
|
+
main(**vars(args))
|
pymhd/spectra.py
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
# PyMHD: Python for Magnetohydrodynamic Turbulence.
|
|
2
|
+
# Copyright (c) 2026 Yuyang Hua (华宇阳)
|
|
3
|
+
# License: MIT
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
pymhd/spectra.py
|
|
7
|
+
----------------
|
|
8
|
+
|
|
9
|
+
Implements the tools for turbulent energy spectra calculations:
|
|
10
|
+
- Spectrum class: data container for a single 3D spectrum.
|
|
11
|
+
- Spectrum1D class: data container for a 1D spectrum.
|
|
12
|
+
- EnergySpectra class: data container for magnetic and kinetic energy spectra.
|
|
13
|
+
- get1D() function: project a 3D spectrum to a 1D spectrum.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
import numpy as np
|
|
20
|
+
|
|
21
|
+
import pickle
|
|
22
|
+
|
|
23
|
+
from collections import UserList
|
|
24
|
+
from typing import Literal
|
|
25
|
+
|
|
26
|
+
from .turbulence import Turbulence, VectorField
|
|
27
|
+
|
|
28
|
+
class Spectrum:
|
|
29
|
+
"""Container for a single 3D spectrum.
|
|
30
|
+
|
|
31
|
+
Parameters
|
|
32
|
+
----------
|
|
33
|
+
EK : np.ndarray with shape (Nx, Ny, Nz), the 3D spectrum data.
|
|
34
|
+
box : tuple[float, float, float], the box size in x, y, z directions (Lx, Ly, Lz).
|
|
35
|
+
|
|
36
|
+
Attributes
|
|
37
|
+
----------
|
|
38
|
+
EK : np.ndarray
|
|
39
|
+
box : tuple[float, float, float]
|
|
40
|
+
Nx, Ny, Nz : int, the number of grid points in x, y, z directions.
|
|
41
|
+
dx, dy, dz : float, the grid spacings in x, y, z directions.
|
|
42
|
+
dkx, dky, dkz: float, the grid spacings in Fourier space.
|
|
43
|
+
dVk : float, the volume of a grid cell in Fourier space.
|
|
44
|
+
kx, ky, kz : np.ndarray, the wavenumbers.
|
|
45
|
+
"""
|
|
46
|
+
def __init__(self, EK: np.ndarray, box: tuple[float, float, float]):
|
|
47
|
+
if EK.ndim != 3:
|
|
48
|
+
raise ValueError("Spectrum.EK must be a 3D array")
|
|
49
|
+
|
|
50
|
+
self.EK = EK
|
|
51
|
+
self.box = box
|
|
52
|
+
self.Lx, self.Ly, self.Lz = box
|
|
53
|
+
|
|
54
|
+
self.Nx, self.Ny, self.Nz = EK.shape
|
|
55
|
+
self.dx = self.Lx / self.Nx
|
|
56
|
+
self.dy = self.Ly / self.Ny
|
|
57
|
+
self.dz = self.Lz / self.Nz
|
|
58
|
+
|
|
59
|
+
self.dkx = 1.0 / self.Lx
|
|
60
|
+
self.dky = 1.0 / self.Ly
|
|
61
|
+
self.dkz = 1.0 / self.Lz
|
|
62
|
+
self.dVk = self.dkx * self.dky * self.dkz
|
|
63
|
+
|
|
64
|
+
self.kx = np.fft.fftfreq(self.Nx, d=self.dx)
|
|
65
|
+
self.ky = np.fft.fftfreq(self.Ny, d=self.dy)
|
|
66
|
+
self.kz = np.fft.fftfreq(self.Nz, d=self.dz)
|
|
67
|
+
|
|
68
|
+
def __add__(self, other) -> Spectrum:
|
|
69
|
+
"""Addition
|
|
70
|
+
|
|
71
|
+
Supported operations
|
|
72
|
+
--------------------
|
|
73
|
+
Spectrum + Spectrum -> Spectrum
|
|
74
|
+
Spectrum + None -> Spectrum
|
|
75
|
+
"""
|
|
76
|
+
if other is None:
|
|
77
|
+
return self
|
|
78
|
+
if isinstance(other, Spectrum) and assertMatchSpectra(self, other):
|
|
79
|
+
return Spectrum(self.EK + other.EK, self.box)
|
|
80
|
+
|
|
81
|
+
return NotImplemented
|
|
82
|
+
|
|
83
|
+
def __radd__(self, other) -> Spectrum:
|
|
84
|
+
"""Right addition
|
|
85
|
+
|
|
86
|
+
Supported operations
|
|
87
|
+
--------------------
|
|
88
|
+
None + Spectrum -> Spectrum
|
|
89
|
+
"""
|
|
90
|
+
if other is None:
|
|
91
|
+
return self
|
|
92
|
+
|
|
93
|
+
return NotImplemented
|
|
94
|
+
|
|
95
|
+
def __sub__(self, other) -> Spectrum:
|
|
96
|
+
"""Subtraction
|
|
97
|
+
|
|
98
|
+
Supported operations
|
|
99
|
+
--------------------
|
|
100
|
+
Spectrum - Spectrum -> Spectrum
|
|
101
|
+
"""
|
|
102
|
+
if isinstance(other, Spectrum) and assertMatchSpectra(self, other):
|
|
103
|
+
return Spectrum(self.EK - other.EK, self.box)
|
|
104
|
+
|
|
105
|
+
return NotImplemented
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def assertMatchSpectra(spectrum1: Spectrum, spectrum2: Spectrum) -> bool:
|
|
109
|
+
"""Assert that two spectra have matching box and data shape.
|
|
110
|
+
|
|
111
|
+
Returns True by default; raises ValueError on mismatch.
|
|
112
|
+
|
|
113
|
+
Raises
|
|
114
|
+
------
|
|
115
|
+
ValueError : if box or array shapes do not match.
|
|
116
|
+
"""
|
|
117
|
+
if spectrum1.box != spectrum2.box:
|
|
118
|
+
raise ValueError("Box must match")
|
|
119
|
+
if spectrum1.EK.shape != spectrum2.EK.shape:
|
|
120
|
+
raise ValueError("Spectrum data shapes must match")
|
|
121
|
+
|
|
122
|
+
return True
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class Spectrum1D:
|
|
126
|
+
"""Container for a 1D spectrum.
|
|
127
|
+
|
|
128
|
+
Parameters
|
|
129
|
+
----------
|
|
130
|
+
k : np.ndarray, shape (N,), the 1D wavenumbers.
|
|
131
|
+
Ek : np.ndarray, shape (N,), the 1D spectrum data.
|
|
132
|
+
|
|
133
|
+
Attributes
|
|
134
|
+
----------
|
|
135
|
+
k : np.ndarray
|
|
136
|
+
Ek : np.ndarray
|
|
137
|
+
dk : float, the grid spacing in wavenumber space.
|
|
138
|
+
kmax : float, the maximum wavenumber.
|
|
139
|
+
"""
|
|
140
|
+
def __init__(self, k: np.ndarray, Ek: np.ndarray):
|
|
141
|
+
|
|
142
|
+
if k.ndim != 1 or Ek.ndim != 1:
|
|
143
|
+
raise ValueError("Spectrum1D must contain 1D arrays")
|
|
144
|
+
|
|
145
|
+
self.k = k
|
|
146
|
+
self.Ek = Ek
|
|
147
|
+
self.dk = k[1] - k[0]
|
|
148
|
+
self.kmax = np.max(k)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class SpectraList(UserList):
|
|
152
|
+
"""List of Spectrum objects.
|
|
153
|
+
|
|
154
|
+
Attributes
|
|
155
|
+
----------
|
|
156
|
+
data : list[Spectrum]
|
|
157
|
+
avg : Spectrum, the arithmetic mean of the spectra.
|
|
158
|
+
"""
|
|
159
|
+
@property
|
|
160
|
+
def avg(self) -> Spectrum:
|
|
161
|
+
if len(self.data) == 0:
|
|
162
|
+
raise ValueError("Cannot compute avg on an empty SpectraList")
|
|
163
|
+
|
|
164
|
+
box = self.data[0].box
|
|
165
|
+
stack = np.stack([spc.EK for spc in self.data], axis=0)
|
|
166
|
+
|
|
167
|
+
return Spectrum(np.mean(stack, axis=0), box)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class EnergySpectra:
|
|
171
|
+
"""Magnetic and kinetic energy spectra.
|
|
172
|
+
|
|
173
|
+
Parameters
|
|
174
|
+
----------
|
|
175
|
+
turbulence: Turbulence, the Turbulence object.
|
|
176
|
+
|
|
177
|
+
Attributes
|
|
178
|
+
----------
|
|
179
|
+
type : Literal['SSD', 'Bx', 'Bz', 'MRI', 'hydro'], inherited from Turbulence.
|
|
180
|
+
nu : float, kinematic viscosity, inherited from Turbulence.
|
|
181
|
+
eta : float | None, magnetic diffusivity, inherited from Turbulence.
|
|
182
|
+
|
|
183
|
+
totEkin : Spectrum, total kinetic energy spectrum.
|
|
184
|
+
totEmag : Spectrum | None, total magnetic energy spectrum.
|
|
185
|
+
|
|
186
|
+
xEkin : Spectrum, x-component kinetic energy spectrum.
|
|
187
|
+
yEkin : Spectrum, y-component kinetic energy spectrum.
|
|
188
|
+
zEkin : Spectrum, z-component kinetic energy spectrum.
|
|
189
|
+
|
|
190
|
+
xEmag : Spectrum | None, x-component magnetic energy spectrum.
|
|
191
|
+
yEmag : Spectrum | None, y-component magnetic energy spectrum.
|
|
192
|
+
zEmag : Spectrum | None, z-component magnetic energy spectrum.
|
|
193
|
+
"""
|
|
194
|
+
def cache(self, path: Path) -> None:
|
|
195
|
+
"""Cache computed result with pickle serialization."""
|
|
196
|
+
try:
|
|
197
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
198
|
+
with path.open("wb") as f:
|
|
199
|
+
pickle.dump(self, f, protocol=pickle.HIGHEST_PROTOCOL)
|
|
200
|
+
except Exception as exc:
|
|
201
|
+
print(f"Failed to save cache: {exc}\n")
|
|
202
|
+
return
|
|
203
|
+
|
|
204
|
+
print(f"EnergySpectra cache saved to ./{path}\n")
|
|
205
|
+
|
|
206
|
+
def __init__(self, turbulence: Turbulence) -> None:
|
|
207
|
+
|
|
208
|
+
self.type = turbulence.type
|
|
209
|
+
self.nu = turbulence.nu
|
|
210
|
+
self.eta = turbulence.eta
|
|
211
|
+
|
|
212
|
+
def computeEnergySpectra(
|
|
213
|
+
fields: list[VectorField],
|
|
214
|
+
) -> tuple[Spectrum, Spectrum, Spectrum, Spectrum]:
|
|
215
|
+
|
|
216
|
+
xlist, ylist, zlist, totlist = [], [], [], []
|
|
217
|
+
|
|
218
|
+
for field in fields:
|
|
219
|
+
dV = field.dxdydz
|
|
220
|
+
fx = np.fft.fftn(field.x) * dV
|
|
221
|
+
fy = np.fft.fftn(field.y) * dV
|
|
222
|
+
fz = np.fft.fftn(field.z) * dV
|
|
223
|
+
Ex = Spectrum(0.5 * np.abs(fx) ** 2, field.box)
|
|
224
|
+
Ey = Spectrum(0.5 * np.abs(fy) ** 2, field.box)
|
|
225
|
+
Ez = Spectrum(0.5 * np.abs(fz) ** 2, field.box)
|
|
226
|
+
xlist.append(Ex)
|
|
227
|
+
ylist.append(Ey)
|
|
228
|
+
zlist.append(Ez)
|
|
229
|
+
totlist.append(Ex + Ey + Ez)
|
|
230
|
+
|
|
231
|
+
return (
|
|
232
|
+
SpectraList(xlist).avg,
|
|
233
|
+
SpectraList(ylist).avg,
|
|
234
|
+
SpectraList(zlist).avg,
|
|
235
|
+
SpectraList(totlist).avg,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
self.xEkin, self.yEkin, self.zEkin, self.totEkin = computeEnergySpectra(turbulence.wVs)
|
|
239
|
+
|
|
240
|
+
if turbulence.type != "hydro":
|
|
241
|
+
self.xEmag, self.yEmag, self.zEmag, self.totEmag = computeEnergySpectra(turbulence.Bs)
|
|
242
|
+
else:
|
|
243
|
+
self.xEmag, self.yEmag, self.zEmag, self.totEmag = None, None, None, None
|
|
244
|
+
|
|
245
|
+
self.cache(Path("spectra") / "spectra.pkl")
|
|
246
|
+
|
|
247
|
+
def get1D(
|
|
248
|
+
spc : Spectrum,
|
|
249
|
+
mode : Literal["shell", "perp", "para", "avg"],
|
|
250
|
+
axis : Literal["x", "y", "z"] | None = None,
|
|
251
|
+
negative: bool = False,
|
|
252
|
+
) -> Spectrum1D:
|
|
253
|
+
r"""Project a 3D Spectrum to a Spectrum1D.
|
|
254
|
+
|
|
255
|
+
Parameters
|
|
256
|
+
----------
|
|
257
|
+
spc : Spectrum, the 3D spectrum object.
|
|
258
|
+
mode: the mode of the 3D to 1D projection.
|
|
259
|
+
- "shell": shell-integrated spectrum E(k), where k = |\bm{k}|;
|
|
260
|
+
- "perp" : E(k_\perp), axisymmetric around the background field;
|
|
261
|
+
- "para" : E(k_\parallel), where k_\parallel is parallel to the background field;
|
|
262
|
+
- "avg" : integrate out the two axes perpendicular to `axis`, giving E(k_{axis}).
|
|
263
|
+
axis: the axis of the 3D to 1D projection for certain modes:
|
|
264
|
+
- "shell": should be None;
|
|
265
|
+
- "perp" : specifies the background field direction;
|
|
266
|
+
- "para" : specifies the background field direction;
|
|
267
|
+
- "avg" : specifies the retained axis.
|
|
268
|
+
negative: bool
|
|
269
|
+
- False: return the absolute value of the spectrum (default);
|
|
270
|
+
- True : return the negative of the spectrum (mainly for dissipation spectra).
|
|
271
|
+
|
|
272
|
+
Returns
|
|
273
|
+
-------
|
|
274
|
+
Spectrum1D, the 1D spectrum object.
|
|
275
|
+
"""
|
|
276
|
+
if mode == "shell":
|
|
277
|
+
|
|
278
|
+
kx3, ky3, kz3 = np.meshgrid(spc.kx, spc.ky, spc.kz, indexing="ij")
|
|
279
|
+
K = np.sqrt(kx3**2 + ky3**2 + kz3**2)
|
|
280
|
+
|
|
281
|
+
# Integrate up to the maximum complete spherical shell fully in the Fourier space.
|
|
282
|
+
kxmax, kymax, kzmax = 1.0 / (2.0 * spc.dx), 1.0 / (2.0 * spc.dy), 1.0 / (2.0 * spc.dz)
|
|
283
|
+
Kmax = min(kxmax, kymax, kzmax)
|
|
284
|
+
dK = max(spc.dkx, spc.dky, spc.dkz)
|
|
285
|
+
|
|
286
|
+
bins = np.arange(0.0, Kmax + dK, dK)
|
|
287
|
+
ctrs = bins[:-1] + 0.5 * dK
|
|
288
|
+
Ek = np.zeros_like(ctrs)
|
|
289
|
+
|
|
290
|
+
for i, (kl, kr) in enumerate(zip(bins[:-1], bins[1:])):
|
|
291
|
+
mask = (K >= kl) & (K < kr)
|
|
292
|
+
Ek[i] = np.sum(spc.EK[mask]) * spc.dVk / dK
|
|
293
|
+
|
|
294
|
+
valid = np.isfinite(Ek) & (ctrs >= dK)
|
|
295
|
+
k = ctrs[valid]
|
|
296
|
+
Ek = Ek[valid]
|
|
297
|
+
|
|
298
|
+
if Ek.size == 0:
|
|
299
|
+
raise ValueError("No valid shell-integrated spectrum values found.")
|
|
300
|
+
|
|
301
|
+
Ek = -Ek if negative else np.abs(Ek)
|
|
302
|
+
return Spectrum1D(k, Ek)
|
|
303
|
+
|
|
304
|
+
# Map the parallel axis to (axis index, dk, k array),
|
|
305
|
+
# and the two perpendicular axes to their indices and dk values.
|
|
306
|
+
if axis == "x":
|
|
307
|
+
para , Kpara , dKpara = 0, spc.kx, spc.dkx
|
|
308
|
+
perp1, Kperp1, dKperp1 = 1, spc.ky, spc.dky
|
|
309
|
+
perp2, Kperp2, dKperp2 = 2, spc.kz, spc.dkz
|
|
310
|
+
elif axis == "y":
|
|
311
|
+
para , Kpara , dKpara = 1, spc.ky, spc.dky
|
|
312
|
+
perp1, Kperp1, dKperp1 = 0, spc.kx, spc.dkx
|
|
313
|
+
perp2, Kperp2, dKperp2 = 2, spc.kz, spc.dkz
|
|
314
|
+
elif axis == "z":
|
|
315
|
+
para , Kpara , dKpara = 2, spc.kz, spc.dkz
|
|
316
|
+
perp1, Kperp1, dKperp1 = 0, spc.kx, spc.dkx
|
|
317
|
+
perp2, Kperp2, dKperp2 = 1, spc.ky, spc.dky
|
|
318
|
+
else:
|
|
319
|
+
raise ValueError(f"axis must be 'x', 'y', or 'z' for mode='{mode}'")
|
|
320
|
+
|
|
321
|
+
if mode == "avg":
|
|
322
|
+
|
|
323
|
+
Ek = np.sum(spc.EK, axis=(perp1, perp2)) * dKperp1 * dKperp2
|
|
324
|
+
k = np.fft.fftshift(Kpara)
|
|
325
|
+
Ek = np.fft.fftshift(Ek)
|
|
326
|
+
|
|
327
|
+
mask = k > 0.0
|
|
328
|
+
k, Ek = k[mask], 2.0 * Ek[mask]
|
|
329
|
+
Ek = -Ek if negative else np.abs(Ek)
|
|
330
|
+
return Spectrum1D(k, Ek)
|
|
331
|
+
|
|
332
|
+
if mode in ("perp", "para"):
|
|
333
|
+
|
|
334
|
+
mesh = np.meshgrid(Kperp1, Kperp2, indexing="ij")
|
|
335
|
+
Kperp2D = np.sqrt(mesh[0]**2 + mesh[1]**2)
|
|
336
|
+
|
|
337
|
+
# Shell binning for the perpendicular direction
|
|
338
|
+
Kperpmax = min(max(Kperp1), max(Kperp2))
|
|
339
|
+
dKperp = max(dKperp1, dKperp2)
|
|
340
|
+
|
|
341
|
+
bins = np.arange(0.0, Kperpmax + dKperp, dKperp)
|
|
342
|
+
ctrs = bins[:-1] + 0.5 * dKperp
|
|
343
|
+
|
|
344
|
+
# E2D[i_perp, i_para]: shell-integrated 2D spectrum E(k_\perp, k_\parallel).
|
|
345
|
+
# For each perpendicular ring and each parallel index, sum E * dKperp1 * dKperp2,
|
|
346
|
+
# then divide by dKperp to get a density in k_perp.
|
|
347
|
+
Npara = spc.EK.shape[para]
|
|
348
|
+
Ek2D = np.zeros((ctrs.size, Npara))
|
|
349
|
+
data = np.moveaxis(spc.EK, para, -1) # shape: (perp1, perp2, para)
|
|
350
|
+
for i, (kl, kr) in enumerate(zip(bins[:-1], bins[1:])):
|
|
351
|
+
ring = (Kperp2D >= kl) & (Kperp2D < kr)
|
|
352
|
+
if not np.any(ring):
|
|
353
|
+
continue
|
|
354
|
+
Ek2D[i, :] = data[ring, :].sum(axis=0) * dKperp1 * dKperp2 / dKperp
|
|
355
|
+
|
|
356
|
+
if mode == "perp":
|
|
357
|
+
# E(k_\perp) = int E(k_\perp, k_\parallel) dKparallel
|
|
358
|
+
k = ctrs
|
|
359
|
+
Ek = np.sum(Ek2D, axis=1) * dKpara
|
|
360
|
+
valid = np.isfinite(Ek) & (k >= dKperp)
|
|
361
|
+
k, Ek = k[valid], Ek[valid]
|
|
362
|
+
else:
|
|
363
|
+
# E(k_\parallel) = int E(k_\perp, k_\parallel) dKperp
|
|
364
|
+
Ek = np.sum(Ek2D, axis=0) * dKperp
|
|
365
|
+
k = np.fft.fftshift(Kpara)
|
|
366
|
+
Ek = np.fft.fftshift(Ek)
|
|
367
|
+
mask = k > 0.0
|
|
368
|
+
k, Ek = k[mask], 2.0 * Ek[mask]
|
|
369
|
+
|
|
370
|
+
if Ek.size == 0:
|
|
371
|
+
raise ValueError(f"No valid {mode}-projected spectrum values found.")
|
|
372
|
+
|
|
373
|
+
Ek = -Ek if negative else np.abs(Ek)
|
|
374
|
+
return Spectrum1D(k, Ek)
|
|
375
|
+
|
|
376
|
+
raise ValueError("mode must be 'shell', 'perp', 'para', or 'avg'")
|