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/plot/spc.py ADDED
@@ -0,0 +1,249 @@
1
+ # PyMHD: Python for Magnetohydrodynamic Turbulence.
2
+ # Copyright (c) 2026 Yuyang Hua (华宇阳)
3
+ # License: MIT
4
+
5
+ """
6
+ pymhd/plot/spc.py
7
+ -----------------
8
+
9
+ Plotting tools for turbulent energy spectra:
10
+ - plotShell(): plot shell-integrated spectra.
11
+ - plotAnisotropic(): plot anisotropic spectra along x, y, z.
12
+ - plotAxisymmetric(): plot axisymmetric spectra along Bx/Bz.
13
+ - plot(): plot spectra based on the type of the EnergySpectra object.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from pathlib import Path
19
+ from typing import Literal
20
+
21
+ import matplotlib.pyplot as plt
22
+
23
+ from matplotlib.axes import Axes
24
+
25
+ from ..spectra import EnergySpectra, Spectrum1D, get1D
26
+
27
+ # Font: Computer Modern
28
+ plt.rcParams['font.family'] = 'serif'
29
+ plt.rcParams['font.serif'] = ['cmr10']
30
+ plt.rcParams['mathtext.fontset'] = 'cm' # Computer Modern
31
+ plt.rcParams['axes.unicode_minus'] = False
32
+ plt.rcParams['axes.formatter.use_mathtext'] = True
33
+
34
+
35
+ def plotShell(spc: EnergySpectra, outdir: Path) -> None:
36
+ """Plot a single shell-integrated spectra.
37
+
38
+ Currently hard-coded to plot a k^{5/3}-compensated spectrum.
39
+
40
+ Parameters
41
+ ----------
42
+ spc : EnergySpectra, the energy spectra object.
43
+ outdir: Path, the output directory.
44
+ """
45
+ fig, ax = plt.subplots(figsize=(8.0, 6.0))
46
+ slope = 5.0 / 3.0
47
+
48
+ kin1D = get1D(spc.totEkin, mode="shell")
49
+ k = kin1D.k
50
+ ax.loglog(k, kin1D.Ek * k**slope, label="Kinetic", lw=2.0, color="k")
51
+
52
+ if spc.totEmag is not None:
53
+ mag1D = get1D(spc.totEmag, mode="shell")
54
+ ax.loglog(k, mag1D.Ek * k**slope, label="Magnetic", lw=2.0, color="r")
55
+
56
+ ax.set_xlabel(r"$k$", fontsize=14)
57
+ ax.set_ylabel(r"$k^{5/3}E(k)$", fontsize=14)
58
+ ax.tick_params(axis="both", which="major", labelsize=12)
59
+ ax.tick_params(axis="both", which="minor", labelsize=10)
60
+ ax.grid(True, which="both", ls="--", alpha=0.4)
61
+ ax.legend(loc="lower left", fontsize=12)
62
+ fig.tight_layout()
63
+ fig.savefig(outdir / "shell.pdf", bbox_inches="tight")
64
+ plt.close(fig)
65
+
66
+
67
+ def plotAnisotropic(spc: EnergySpectra, outdir: Path) -> None:
68
+ """Plot anisotropic spectra along x, y, z.
69
+
70
+ Currently hard-coded to plot a k^{3/2}-compensated spectrum for MRI-driven turbulence.
71
+
72
+ Two rows:
73
+ - first row : averaged spectra E(k_i) for i = x, y, z;
74
+ - second row: shell-integrated spectra E_i(|k|) for i = x, y, z.
75
+ """
76
+ assert spc.totEmag is not None
77
+ assert spc.xEmag is not None
78
+ assert spc.yEmag is not None
79
+ assert spc.zEmag is not None
80
+
81
+ axes: tuple[Literal["x"], Literal["y"], Literal["z"]] = ("x", "y", "z")
82
+ Ekins = {"x": spc.xEkin, "y": spc.yEkin, "z": spc.zEkin}
83
+ Emags = {"x": spc.xEmag, "y": spc.yEmag, "z": spc.zEmag}
84
+
85
+ fig, axs = plt.subplots(2, 3, figsize=(18, 12), sharey="row")
86
+ slope = 3 / 2
87
+
88
+ # First row: E(k_i) for i = x, y, z.
89
+ for i, axis in enumerate(axes):
90
+ kin1D = get1D(spc.totEkin, mode="avg", axis=axis)
91
+ k = kin1D.k
92
+ axs[0, i].loglog(k, kin1D.Ek * k**slope, label=r"$E_{\mathrm{kin}}$", lw=2.5, color="k")
93
+
94
+ mag1D = get1D(spc.totEmag, mode="avg", axis=axis)
95
+ axs[0, i].loglog(k, mag1D.Ek * k**slope, label=r"$E_{\mathrm{mag}}$", lw=2.5, color="r")
96
+
97
+ axs[0, i].set_xlabel(r"$k_{%s}$" % axis)
98
+ axs[0, i].grid(True, which="both", ls="--", alpha=0.4)
99
+ axs[0, i].tick_params(axis="both", direction="in", which="both", pad=6.5)
100
+
101
+ axs[0, 0].set_ylabel(r"$k_i^{3/2} E(k_i)$")
102
+ for i in range(3):
103
+ axs[0, i].legend(loc="lower left", fontsize=14)
104
+
105
+ # Second row: shell-integrated spectra E_i(|k|) for i = x, y, z.
106
+ for i, axis in enumerate(axes):
107
+
108
+ shellEkin = get1D(Ekins[axis], mode="shell")
109
+ k, Ek = shellEkin.k, shellEkin.Ek
110
+ axs[1, i].loglog(k, Ek * k**slope, label=r"$E_{\mathrm{kin}}$", lw=2.5, color="k")
111
+
112
+ shellEmag = get1D(Emags[axis], mode="shell")
113
+ k, Ek = shellEmag.k, shellEmag.Ek
114
+ axs[1, i].loglog(k, Ek * k**slope, label=r"$E_{\mathrm{mag}}$", lw=2.5, color="r")
115
+
116
+ axs[1, i].set_xlabel(r"$k$")
117
+ axs[1, i].grid(True, which="both", ls="--", alpha=0.4)
118
+ axs[1, i].tick_params(axis="both", direction="in", which="both", pad=6.5)
119
+ axs[1, i].legend(loc="lower left", fontsize=14)
120
+
121
+ axs[1, 0].set_ylabel(r"$k^{3/2}E_i(k)$")
122
+ fig.tight_layout()
123
+ fig.savefig(outdir / "anisotropic.pdf", bbox_inches="tight")
124
+ plt.close(fig)
125
+
126
+
127
+ def plotAxisymmetric(spc: EnergySpectra, outdir: Path) -> None:
128
+ r"""Plot axisymmetric spectra for Afvénic turbulence.
129
+
130
+ The background field direction is derived from spc.type:
131
+ - spc.type == 'Bx' -> axis='x';
132
+ - spc.type == 'Bz' -> axis='z'.
133
+
134
+ Two rows:
135
+ - first row : k_\perp (left) and k_\parallel (right) spectra E(k_\perp)/E(k_\parallel);
136
+ - second row: shell-integrated spectra E_perp(|k|) and E_para(|k|).
137
+ """
138
+ assert spc.totEmag is not None
139
+ assert spc.xEmag is not None
140
+ assert spc.yEmag is not None
141
+ assert spc.zEmag is not None
142
+
143
+ axismap: dict[Literal["Bx", "Bz"], Literal["x", "z"]] = {
144
+ "Bx": "x",
145
+ "Bz": "z",
146
+ }
147
+ if spc.type not in axismap:
148
+ raise ValueError(f"plotAxisymmetric requires Bx or Bz, got {spc.type!r}")
149
+ axis = axismap[spc.type]
150
+
151
+ # Component energy sums for the second row.
152
+ # perp: sum of the two directions perpendicular to the background field.
153
+ # para: the background-field direction.
154
+ if axis == "z":
155
+ perpEkin = spc.xEkin + spc.yEkin
156
+ paraEkin = spc.zEkin
157
+ perpEmag = spc.xEmag + spc.yEmag
158
+ paraEmag = spc.zEmag
159
+ else: # axis == "x"
160
+ perpEkin = spc.yEkin + spc.zEkin
161
+ paraEkin = spc.xEkin
162
+ perpEmag = spc.yEmag + spc.zEmag
163
+ paraEmag = spc.xEmag
164
+
165
+ shellPerpEkin = get1D(perpEkin, mode="shell")
166
+ shellParaEkin = get1D(paraEkin, mode="shell")
167
+ shellPerpEmag = get1D(perpEmag, mode="shell")
168
+ shellParaEmag = get1D(paraEmag, mode="shell")
169
+
170
+ fig, axs = plt.subplots(2, 2, figsize=(12.0, 10.0))
171
+
172
+ # ===== First row =====
173
+ # anisotropic spectra of total kinetic and magnetic energy
174
+
175
+ first: list[tuple[Literal["perp", "para"], float]] = [
176
+ ("perp", 5.0 / 3.0),
177
+ ("para", 2.0 )
178
+ ]
179
+
180
+ for ax, (mode, slope) in zip(axs[0], first):
181
+
182
+ kin1D = get1D(spc.totEkin, mode=mode, axis=axis)
183
+ mag1D = get1D(spc.totEmag, mode=mode, axis=axis)
184
+
185
+ ax.loglog(kin1D.k, kin1D.Ek * kin1D.k**slope, label=r"$E_{\mathrm{kin}}$", lw=2.0, color="k")
186
+ ax.loglog(mag1D.k, mag1D.Ek * mag1D.k**slope, label=r"$E_{\mathrm{mag}}$", lw=2.0, color="r")
187
+
188
+ ax.grid(True, which="both", ls="--", alpha=0.4)
189
+
190
+ ticksize = 12
191
+ ax.tick_params(axis="both", which="major", direction="in", labelsize=ticksize)
192
+ ax.tick_params(axis="both", which="minor", direction="in", labelsize=ticksize - 1)
193
+
194
+ fontsize = 14
195
+ axs[0, 0].set_xlabel(r"$k_{\perp}$" , fontsize=fontsize)
196
+ axs[0, 0].set_ylabel(r"$k_{\perp}^{5/3} E(k_{\perp})$" , fontsize=fontsize)
197
+ axs[0, 1].set_xlabel(r"$k_{\parallel}$" , fontsize=fontsize)
198
+ axs[0, 1].set_ylabel(r"$k_{\parallel}^{2} E(k_{\parallel})$", fontsize=fontsize)
199
+
200
+ axs[0, 0].legend(loc="lower left", fontsize=12)
201
+ axs[0, 1].legend(loc="lower left", fontsize=12)
202
+
203
+ # ===== Second row =====
204
+ # shell-integrated spectra of perp/para components
205
+
206
+ ticksize = 12
207
+ slope = 5.0 / 3.0
208
+ second: list[tuple[Axes, Spectrum1D, Spectrum1D, str, str]] = [
209
+ (axs[1, 0], shellPerpEkin, shellPerpEmag, r"$k$", r"$k^{5/3} E_{\perp}(k)$"),
210
+ (axs[1, 1], shellParaEkin, shellParaEmag, r"$k$", r"$k^{5/3} E_{\parallel}(k)$"),
211
+ ]
212
+ for ax, kin, mag, xlabel, ylabel in second:
213
+
214
+ ax.loglog(kin.k, kin.Ek * kin.k**slope, label=r"$E_{\mathrm{kin}}$", lw=2.0, color="k")
215
+ ax.loglog(mag.k, mag.Ek * mag.k**slope, label=r"$E_{\mathrm{mag}}$", lw=2.0, color="r")
216
+
217
+ ax.set_xlabel(xlabel, fontsize=fontsize)
218
+ ax.set_ylabel(ylabel, fontsize=fontsize)
219
+ ax.grid(True, which="both", ls="--", alpha=0.4)
220
+ ax.tick_params(axis="both", which="major", direction="in", labelsize=ticksize)
221
+ ax.tick_params(axis="both", which="minor", direction="in", labelsize=ticksize - 1)
222
+ ax.legend(loc="lower left", fontsize=12)
223
+
224
+ fig.tight_layout()
225
+ fig.subplots_adjust(hspace=0.15)
226
+ fig.savefig(outdir / "axisymmetric.pdf", bbox_inches="tight")
227
+ plt.close(fig)
228
+
229
+
230
+ def plot(spc: EnergySpectra) -> None:
231
+ """Plot 1D spectra based on the type of the EnergySpectra object.
232
+
233
+ Parameters
234
+ ----------
235
+ spc : EnergySpectra, the energy spectra object.
236
+ """
237
+ outdir = Path("spectra")
238
+ outdir.mkdir(parents=True, exist_ok=True)
239
+
240
+ # plot shell-integrated spectra for all types
241
+ plotShell(spc, outdir=outdir)
242
+
243
+ # plot axisymmetric spectra for Afvénic turbulence
244
+ if spc.type in ("Bx", "Bz"):
245
+ plotAxisymmetric(spc, outdir=outdir)
246
+
247
+ # plot anisotropic spectra for MRI-driven turbulence
248
+ if spc.type == "MRI":
249
+ plotAnisotropic(spc, outdir=outdir)