pyvlasiator 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.
@@ -0,0 +1,1115 @@
1
+ from __future__ import annotations
2
+
3
+ import numpy as np
4
+ import math
5
+ import warnings
6
+ from typing import Callable
7
+ from collections import namedtuple
8
+ from enum import Enum
9
+ from pyvlasiator.vlsv import Vlsv
10
+ from pyvlasiator.vlsv.reader import _getdim2d
11
+ from pyvlasiator.vlsv.variables import RE
12
+
13
+
14
+ class ColorScale(Enum):
15
+ """
16
+ Represents the available color scales for data visualization.
17
+
18
+ Attributes:
19
+ - Linear (1): A linear color scale, where colors are evenly distributed across the data range.
20
+ - Log (2): A logarithmic color scale, suitable for data with a wide range of values, where smaller values are more emphasized.
21
+ - SymLog (3): A symmetric logarithmic color scale, similar to Log but with symmetry around zero, useful for data with both positive and negative values.
22
+ """
23
+
24
+ Linear = 1
25
+ Log = 2
26
+ SymLog = 3
27
+
28
+
29
+ class AxisUnit(Enum):
30
+ """
31
+ Specifies the units for representing values on an axis.
32
+
33
+ Attributes:
34
+ - EARTH (1): Units based on Earth's physical properties, such as Earth's radius for distance and km/s for velocity.
35
+ - SI (2): Units from the International System of Units (SI), such as meters, seconds, kilograms, etc.
36
+ """
37
+
38
+ EARTH = 1
39
+ SI = 2
40
+
41
+
42
+ # Plotting arguments
43
+ PlotArgs = namedtuple(
44
+ "PlotArgs",
45
+ [
46
+ "axisunit",
47
+ "sizes",
48
+ "plotrange",
49
+ "origin",
50
+ "idlist",
51
+ "indexlist",
52
+ "str_title",
53
+ "strx",
54
+ "stry",
55
+ "cb_title",
56
+ ],
57
+ )
58
+
59
+
60
+ def plot(
61
+ self: Vlsv,
62
+ var: str = "",
63
+ ax=None,
64
+ figsize: tuple[float, float] | None = None,
65
+ **kwargs,
66
+ ) -> list[matplotlib.lines.Line2D]:
67
+ """
68
+ Plots 1D data from a VLSV file.
69
+
70
+ Parameters
71
+ ----------
72
+ var : str
73
+ Name of the variable to plot from the VLSV file.
74
+ ax : matplotlib.axes._axes.Axes, optional
75
+ Axes object to plot on. If not provided, a new figure and axes will be created.
76
+ figsize : tuple[float, float], optional
77
+ Size of the figure in inches (width, height). Only used if a new
78
+ figure is created.
79
+ **kwargs
80
+ Additional keyword arguments passed to Matplotlib's `plot` function.
81
+
82
+ Returns
83
+ -------
84
+ list[matplotlib.lines.Line2D]
85
+ A list of created Line2D objects.
86
+
87
+ Raises
88
+ ------
89
+ ValueError
90
+ If the specified variable is not found in the VLSV file.
91
+
92
+ Examples
93
+ --------
94
+ >>> vlsv_file = Vlsv("my_vlsv_file.vlsv")
95
+ >>> axes = vlsv_file.plot("proton/vg_rho") # Plot density on a new figure
96
+ >>> axes = vlsv_file.plot("helium/vg_v", ax=my_axes) # Plot velocity on an existing axes
97
+ """
98
+
99
+ import matplotlib.pyplot as plt
100
+
101
+ fig = kwargs.pop(
102
+ "figure", plt.gcf() if plt.get_fignums() else plt.figure(figsize=figsize)
103
+ )
104
+ if ax is None:
105
+ ax = fig.gca()
106
+
107
+ if not self.has_variable(var):
108
+ raise ValueError(f"Variable {var} not found in the file")
109
+
110
+ x = np.linspace(self.coordmin[0], self.coordmax[0], self.ncells[0])
111
+
112
+ data = self.read_variable(var)
113
+ axes = ax.plot(x, data, **kwargs)
114
+
115
+ return axes
116
+
117
+
118
+ def pcolormesh(
119
+ self: Vlsv,
120
+ var: str = "",
121
+ axisunit: AxisUnit = AxisUnit.EARTH,
122
+ colorscale: ColorScale = ColorScale.Linear,
123
+ addcolorbar: bool = True,
124
+ vmin: float = float("-inf"),
125
+ vmax: float = float("inf"),
126
+ extent: list = [0.0, 0.0, 0.0, 0.0],
127
+ comp: int = -1,
128
+ ax=None,
129
+ figsize: tuple[float, float] | None = None,
130
+ **kwargs,
131
+ ):
132
+ """
133
+ Plots 2D VLSV data using pcolormesh.
134
+
135
+ Parameters
136
+ ----------
137
+ var : str
138
+ Name of the variable to plot from the VLSV file.
139
+ axisunit : AxisUnit
140
+ Unit of the axis, `AxisUnit.EARTH` or `AxisUnit.SI`.
141
+ addcolorbar : bool
142
+ Add colorbar to the right.
143
+ colorscale: ColorScale
144
+ Color scale of the data, `ColorScale.Linear`, `ColorScale.Log`, or `ColorScale.SymLog`.
145
+ extent : list
146
+ Extent of the domain (WIP).
147
+ comp : int
148
+ Vector composition of data, -1 is magnitude, 0 is x, 1 is y, and 2 is z.
149
+ ax : matplotlib.axes._axes.Axes, optional
150
+ Axes object to plot on. If not provided, a new figure and axes
151
+ will be created using `set_figure`.
152
+ figsize : tuple[float, float], optional
153
+ Size of the figure in inches. Only used if a new figure is created.
154
+ **kwargs
155
+ Additional keyword arguments passed to `ax.pcolormesh`.
156
+
157
+ Returns
158
+ -------
159
+ matplotlib.figure.Figure
160
+ The created or existing figure object.
161
+
162
+ Raises
163
+ ------
164
+ ValueError
165
+ If the specified variable is not found in the VLSV file.
166
+
167
+ Examples
168
+ --------
169
+ >>> vlsv_file = Vlsv("my_vlsv_file.vlsv")
170
+ >>> # Plot density on a new figure
171
+ >>> fig = vlsv_file.pcolormesh("proton/vg_rho")
172
+ >>> # Plot velocity on an existing axes
173
+ >>> ax = ... # Existing axes object
174
+ >>> fig = vlsv_file.pcolormesh("proton/vg_v", ax=ax)
175
+ """
176
+
177
+ fig, ax = set_figure(ax, figsize, **kwargs)
178
+
179
+ return _plot2d(
180
+ self,
181
+ ax.pcolormesh,
182
+ var=var,
183
+ ax=ax,
184
+ comp=comp,
185
+ axisunit=axisunit,
186
+ colorscale=colorscale,
187
+ addcolorbar=addcolorbar,
188
+ vmin=vmin,
189
+ vmax=vmax,
190
+ extent=extent,
191
+ **kwargs,
192
+ )
193
+
194
+
195
+ def contourf(
196
+ self: Vlsv,
197
+ var: str = "",
198
+ axisunit: AxisUnit = AxisUnit.EARTH,
199
+ colorscale: ColorScale = ColorScale.Linear,
200
+ addcolorbar: bool = True,
201
+ vmin: float = float("-inf"),
202
+ vmax: float = float("inf"),
203
+ extent: list = [0.0, 0.0, 0.0, 0.0],
204
+ comp: int = -1,
205
+ ax=None,
206
+ figsize: tuple[float, float] | None = None,
207
+ **kwargs,
208
+ ):
209
+ fig, ax = set_figure(ax, figsize, **kwargs)
210
+
211
+ return _plot2d(
212
+ self,
213
+ ax.contourf,
214
+ var=var,
215
+ ax=ax,
216
+ comp=comp,
217
+ axisunit=axisunit,
218
+ colorscale=colorscale,
219
+ addcolorbar=addcolorbar,
220
+ vmin=vmin,
221
+ vmax=vmax,
222
+ extent=extent,
223
+ **kwargs,
224
+ )
225
+
226
+
227
+ def contour(
228
+ self: Vlsv,
229
+ var: str = "",
230
+ axisunit: AxisUnit = AxisUnit.EARTH,
231
+ colorscale: ColorScale = ColorScale.Linear,
232
+ addcolorbar: bool = True,
233
+ vmin: float = float("-inf"),
234
+ vmax: float = float("inf"),
235
+ extent: list = [0.0, 0.0, 0.0, 0.0],
236
+ comp: int = -1,
237
+ ax=None,
238
+ figsize: tuple[float, float] | None = None,
239
+ **kwargs,
240
+ ):
241
+ fig, ax = set_figure(ax, figsize, **kwargs)
242
+
243
+ return _plot2d(
244
+ self,
245
+ ax.contour,
246
+ var=var,
247
+ ax=ax,
248
+ comp=comp,
249
+ axisunit=axisunit,
250
+ colorscale=colorscale,
251
+ addcolorbar=addcolorbar,
252
+ vmin=vmin,
253
+ vmax=vmax,
254
+ extent=extent,
255
+ **kwargs,
256
+ )
257
+
258
+
259
+ def _plot2d(
260
+ meta: Vlsv,
261
+ plot_func: Callable,
262
+ var: str = "",
263
+ axisunit: AxisUnit = AxisUnit.EARTH,
264
+ colorscale: ColorScale = ColorScale.Linear,
265
+ addcolorbar: bool = True,
266
+ vmin: float = float("-inf"),
267
+ vmax: float = float("inf"),
268
+ extent: list = [0.0, 0.0, 0.0, 0.0],
269
+ comp: int = -1,
270
+ ax=None,
271
+ **kwargs,
272
+ ):
273
+ """
274
+ Plot 2d data.
275
+
276
+ Parameters
277
+ ----------
278
+ var : str
279
+ Variable name from the VLSV file.
280
+
281
+ Returns
282
+ -------
283
+
284
+ """
285
+
286
+ if not meta.has_variable(var):
287
+ raise ValueError(f"Variable {var} not found in the file")
288
+
289
+ if meta.ndims() == 3 or meta.maxamr > 0:
290
+ # check if origin and normal exist in kwargs
291
+ normal = kwargs["normal"] if "normal" in kwargs else 1
292
+ origin = kwargs["origin"] if "origin" in kwargs else 0.0
293
+ kwargs.pop("normal", None)
294
+ kwargs.pop("origin", None)
295
+
296
+ pArgs = set_args(meta, var, axisunit, normal, origin)
297
+ data = prep2dslice(meta, var, normal, comp, pArgs)
298
+ else:
299
+ pArgs = set_args(meta, var, axisunit)
300
+ data = prep2d(meta, var, comp)
301
+
302
+ x1, x2 = get_axis(pArgs.axisunit, pArgs.plotrange, pArgs.sizes)
303
+
304
+ if var in ("fg_b", "fg_e", "vg_b_vol", "vg_e_vol") or var.endswith("vg_v"):
305
+ _fillinnerBC(data)
306
+
307
+ norm, ticks = set_colorbar(colorscale, vmin, vmax, data)
308
+
309
+ range1 = range(
310
+ np.searchsorted(x1, extent[0]), np.searchsorted(x1, extent[1], side="right")
311
+ )
312
+ range2 = range(
313
+ np.searchsorted(x2, extent[2]), np.searchsorted(x2, extent[3], side="right")
314
+ )
315
+
316
+ c = plot_func(x1, x2, data, **kwargs)
317
+
318
+ configure_plot(c, ax, pArgs, ticks, addcolorbar)
319
+
320
+ return c
321
+
322
+
323
+ def streamplot(
324
+ meta: Vlsv,
325
+ var: str,
326
+ ax=None,
327
+ comp: str = "xy",
328
+ axisunit: AxisUnit = AxisUnit.EARTH,
329
+ origin: float = 0.0,
330
+ **kwargs,
331
+ ) -> matplotlib.streamplot.StreamplotSet:
332
+ """
333
+ Creates a streamplot visualization of a vector field from a VLSV dataset.
334
+
335
+ Parameters
336
+ ----------
337
+ meta: Vlsv
338
+ A VLSV metadata object containing the dataset information.
339
+ var: str
340
+ The name of the vector variable to visualize.
341
+ ax: matplotlib.axes.Axes, optional
342
+ The axes object to plot on. If not provided, a new figure and axes
343
+ will be created.
344
+ comp: str, optional
345
+ The components of the vector to plot, specified as a string containing
346
+ "x", "y", or "z" (e.g., "xy" for x-y components). Defaults to "xy".
347
+ axisunit: AxisUnit, optional
348
+ The unit system for the plot axes. Defaults to AxisUnit.EARTH.
349
+ origin: float, optional
350
+ The origin point for slice plots. Defaults to 0.0.
351
+ **kwargs
352
+ Additional keyword arguments passed to Matplotlib's streamplot function.
353
+
354
+ Returns
355
+ -------
356
+ matplotlib.streamplot.StreamplotSet
357
+ The streamplot object created by Matplotlib.
358
+ """
359
+
360
+ X, Y, v1, v2 = set_vector(meta, var, comp, axisunit, origin)
361
+ fig, ax = set_figure(ax, **kwargs)
362
+
363
+ s = ax.streamplot(X, Y, v1, v2, **kwargs)
364
+
365
+ return s
366
+
367
+
368
+ def set_vector(
369
+ meta: Vlsv, var: str, comp: str, axisunit: AxisUnit, origin: float = 0.0
370
+ ):
371
+ """
372
+ Extracts and prepares vector data for plotting from a VLSV dataset.
373
+
374
+ Parameters
375
+ ----------
376
+ meta: Vlsv
377
+ A VLSV metadata object containing the dataset information.
378
+ var: str
379
+ The name of the vector variable to extract.
380
+ comp: str
381
+ The components of the vector to extract, specified as a string
382
+ containing "x", "y", or "z" (e.g., "xy" for x-y components).
383
+ axisunit: AxisUnit
384
+ The unit system for the plot axes.
385
+ origin: float, optional
386
+ The origin point for slice plots. Defaults to 0.0.
387
+
388
+ Returns
389
+ -------
390
+ tuple
391
+ A tuple containing:
392
+ - x: The x-axis coordinates for plotting.
393
+ - y: The y-axis coordinates for plotting.
394
+ - v1: The values of the first vector component.
395
+ - v2: The values of the second vector component.
396
+
397
+ Raises
398
+ ------
399
+ ValueError
400
+ If the specified variable is not a vector variable.
401
+ """
402
+
403
+ ncells = meta.ncells
404
+ maxamr = meta.maxamr
405
+ coordmin, coordmax = meta.coordmin, meta.coordmax
406
+
407
+ if "x" in comp:
408
+ v1_ = 0
409
+ if "y" in comp:
410
+ dir = 2
411
+ v2_ = 1
412
+ sizes = _getdim2d(ncells, maxamr, 2)
413
+ plotrange = (coordmin[0], coordmax[0], coordmin[1], coordmax[1])
414
+ else:
415
+ dir = 1
416
+ v2_ = 2
417
+ sizes = _getdim2d(ncells, maxamr, 1)
418
+ plotrange = (coordmin[0], coordmax[0], coordmin[2], coordmax[2])
419
+ else:
420
+ dir = 0
421
+ v1_, v2_ = 1, 2
422
+ sizes = _getdim2d(ncells, maxamr, 0)
423
+ plotrange = (coordmin[1], coordmax[1], coordmin[2], coordmax[2])
424
+
425
+ data = meta.read_variable(var)
426
+
427
+ if not var.startswith("fg_"): # vlasov grid
428
+ if data.ndim != 2 and data.shape[0] == 3:
429
+ raise ValueError("Vector variable required!")
430
+ if meta.maxamr == 0:
431
+ data = data.reshape((sizes[1], sizes[0], 3))
432
+ v1 = data[:, :, v1_]
433
+ v2 = data[:, :, v2_]
434
+ else:
435
+ sliceoffset = origin - coordmin[dir]
436
+ idlist, indexlist = meta.getslicecell(
437
+ sliceoffset, dir, coordmin[dir], coordmax[dir]
438
+ )
439
+ v2D = data[indexlist, :]
440
+ v1 = meta.refineslice(idlist, v2D[:, v1_], dir)
441
+ v2 = meta.refineslice(idlist, v2D[:, v2_], dir)
442
+ else: # FS grid
443
+ data = np.squeeze(data)
444
+ v1 = np.transpose(data[:, :, v1_])
445
+ v2 = np.transpose(data[:, :, v2_])
446
+
447
+ x, y = get_axis(axisunit, plotrange, sizes)
448
+
449
+ return x, y, v1, v2
450
+
451
+
452
+ def set_figure(ax, figsize: tuple = (10, 6), **kwargs) -> tuple:
453
+ """
454
+ Sets up a Matplotlib figure and axes for plotting.
455
+
456
+ Parameters
457
+ ----------
458
+ ax: matplotlib.axes.Axes, optional
459
+ An existing axes object to use for plotting. If not provided, a new
460
+ figure and axes will be created.
461
+ figsize: tuple, optional
462
+ The desired figure size in inches, as a tuple (width, height).
463
+ Defaults to (10, 6).
464
+ **kwargs
465
+ Additional keyword arguments passed to Matplotlib's figure() function
466
+ if a new figure is created.
467
+
468
+ Returns
469
+ -------
470
+ tuple
471
+ A tuple containing:
472
+ - fig: The Matplotlib figure object.
473
+ - ax: The Matplotlib axes object.
474
+ """
475
+
476
+ import matplotlib.pyplot as plt
477
+
478
+ fig = kwargs.pop(
479
+ "figure", plt.gcf() if plt.get_fignums() else plt.figure(figsize=figsize)
480
+ )
481
+ if ax is None:
482
+ ax = fig.gca()
483
+
484
+ return fig, ax
485
+
486
+
487
+ def set_args(
488
+ meta: Vlsv,
489
+ var: str,
490
+ axisunit: AxisUnit = AxisUnit.EARTH,
491
+ dir: int = -1,
492
+ origin: float = 0.0,
493
+ ) -> PlotArgs:
494
+ """
495
+ Set plot-related arguments of `var` in `axisunit`.
496
+
497
+ Parameters
498
+ ----------
499
+ var : str
500
+ Variable name from the VLSV file.
501
+ axisunit : AxisUnit
502
+ Unit of the axis.
503
+ dir : int
504
+ Normal direction of the 2D slice, 0 for x, 1 for y, and 2 for z.
505
+ origin : float
506
+ Origin of the 2D slice.
507
+
508
+ Returns
509
+ -------
510
+ PlotArgs
511
+
512
+ See Also
513
+ --------
514
+ :func:`pcolormesh`
515
+ """
516
+
517
+ ncells, coordmin, coordmax = meta.ncells, meta.coordmin, meta.coordmax
518
+
519
+ if dir == 0:
520
+ seq = (1, 2)
521
+ elif dir == 1 or (ncells[1] == 1 and ncells[2] != 1): # polar
522
+ seq = (0, 2)
523
+ dir = 1
524
+ elif dir == 2 or (ncells[2] == 1 and ncells[1] != 1): # ecliptic
525
+ seq = (0, 1)
526
+ dir = 2
527
+ else:
528
+ raise ValueError("1D data detected. Please use 1D plot functions.")
529
+
530
+ plotrange = (coordmin[seq[0]], coordmax[seq[0]], coordmin[seq[1]], coordmax[seq[1]])
531
+ axislabels = tuple(("X", "Y", "Z")[i] for i in seq)
532
+ # Scale the sizes to the highest refinement level for data to be refined later
533
+ sizes = tuple(ncells[i] << meta.maxamr for i in seq)
534
+
535
+ if dir == -1:
536
+ idlist, indexlist = np.empty(0, dtype=int), np.empty(0, dtype=int)
537
+ else:
538
+ sliceoffset = origin - coordmin[dir]
539
+ idlist, indexlist = meta.getslicecell(
540
+ sliceoffset, dir, coordmin[dir], coordmax[dir]
541
+ )
542
+
543
+ if axisunit == AxisUnit.EARTH:
544
+ unitstr = r"$R_E$"
545
+ else:
546
+ unitstr = r"$m$"
547
+ strx = axislabels[0] + " [" + unitstr + "]"
548
+ stry = axislabels[1] + " [" + unitstr + "]"
549
+
550
+ str_title = f"t={meta.time:4.1f}s"
551
+
552
+ datainfo = meta.read_variable_meta(var)
553
+
554
+ if not datainfo.variableLaTeX:
555
+ cb_title = datainfo.variableLaTeX + " [" + datainfo.unitLaTeX + "]"
556
+ else:
557
+ cb_title = ""
558
+
559
+ return PlotArgs(
560
+ axisunit,
561
+ sizes,
562
+ plotrange,
563
+ origin,
564
+ idlist,
565
+ indexlist,
566
+ str_title,
567
+ strx,
568
+ stry,
569
+ cb_title,
570
+ )
571
+
572
+
573
+ def prep2d(meta: Vlsv, var: str, comp: int = -1):
574
+ """
575
+ Obtain data of `var` for 2D plotting. Use `comp` to select vector components.
576
+
577
+ Parameters
578
+ ----------
579
+ meta : Vlsv
580
+ Metadata corresponding to the file.
581
+ var : str
582
+ Name of the variable.
583
+ comp : int
584
+ Vector component. -1 refers to the magnitude of the vector.
585
+ Returns
586
+ -------
587
+ numpy.ndarray
588
+ """
589
+
590
+ dataRaw = _getdata2d(meta, var)
591
+
592
+ if dataRaw.ndim == 3:
593
+ if comp != -1:
594
+ data = dataRaw[:, :, comp]
595
+ else:
596
+ data = np.linalg.norm(dataRaw, axis=2)
597
+ if var.startswith("fg_"):
598
+ data = np.transpose(data)
599
+ else:
600
+ data = dataRaw
601
+
602
+ return data
603
+
604
+
605
+ def prep2dslice(meta: Vlsv, var: str, dir: int, comp: int, pArgs: PlotArgs):
606
+ origin = pArgs.origin
607
+ idlist = pArgs.idlist
608
+ indexlist = pArgs.indexlist
609
+
610
+ data3D = meta.read_variable(var)
611
+
612
+ if var.startswith("fg_") or data3D.ndim > 2: # field or derived quantities, fsgrid
613
+ ncells = meta.ncells * 2**meta.maxamr
614
+ if not dir in (0, 1, 2):
615
+ raise ValueError(f"Unknown normal direction {dir}")
616
+
617
+ sliceratio = (origin - meta.coordmin[dir]) / (
618
+ meta.coordmax[dir] - meta.coordmin[dir]
619
+ )
620
+ if not (0.0 <= sliceratio <= 1.0):
621
+ raise ValueError("slice plane index out of bound!")
622
+ # Find the cut plane index for each refinement level
623
+ icut = int(np.floor(sliceratio * ncells[dir]))
624
+ if dir == 0:
625
+ if comp != -1:
626
+ data = data3D[icut, :, :, comp]
627
+ else:
628
+ data = np.linalg.norm(data3D[icut, :, :, :], axis=3)
629
+ elif dir == 1:
630
+ if comp != -1:
631
+ data = data3D[:, icut, :, comp]
632
+ else:
633
+ data = np.linalg.norm(data3D[:, icut, :, :], axis=3)
634
+ elif dir == 2:
635
+ if comp != -1:
636
+ data = data3D[:, :, icut, comp]
637
+ else:
638
+ data = np.linalg.norm(data3D[:, :, icut, :], axis=3)
639
+ else: # moments, dccrg grid
640
+ # vlasov grid, AMR
641
+ if data3D.ndim == 1:
642
+ data2D = data3D[indexlist]
643
+
644
+ data = meta.refineslice(idlist, data2D, dir)
645
+ elif data3D.ndim == 2:
646
+ data2D = data3D[indexlist, :]
647
+
648
+ if comp in (0, 1, 2):
649
+ slice = data2D[:, comp]
650
+ data = meta.refineslice(idlist, slice, dir)
651
+ elif comp == -1:
652
+ datax = meta.refineslice(idlist, data2D[:, 0], dir)
653
+ datay = meta.refineslice(idlist, data2D[:, 1], dir)
654
+ dataz = meta.refineslice(idlist, data2D[:, 2], dir)
655
+ data = np.fromiter(
656
+ (np.linalg.norm([x, y, z]) for x, y, z in zip(datax, datay, dataz)),
657
+ dtype=float,
658
+ )
659
+ else:
660
+ slice = data2D[:, comp]
661
+ data = meta.refineslice(idlist, slice, dir)
662
+
663
+ return data
664
+
665
+
666
+ def _getdata2d(meta: Vlsv, var: str) -> np.ndarray:
667
+ """
668
+ Retrieves and reshapes 2D data from a VLSV dataset.
669
+
670
+ Raises:
671
+ ValueError: If the dataset is not 2D.
672
+ """
673
+
674
+ if meta.ndims() != 2:
675
+ raise ValueError("2D outputs required")
676
+ sizes = [i for i in meta.ncells if i != 1]
677
+ data = meta.read_variable(var)
678
+ if data.ndim in (1, 3) or data.shape[-1] == 1:
679
+ data = data.reshape((sizes[1], sizes[0]))
680
+ elif var.startswith("fg_"):
681
+ data = data.reshape((sizes[0], sizes[1], 3))
682
+ else:
683
+ data = data.reshape((sizes[1], sizes[0], 3))
684
+
685
+ return data
686
+
687
+
688
+ def get_axis(axisunit: AxisUnit, plotrange: tuple, sizes: tuple) -> tuple:
689
+ """
690
+ Generates the 2D domain axis coordinates, potentially applying Earth radius scaling.
691
+
692
+ Parameters
693
+ ----------
694
+ axisunit: AxisUnit
695
+ The unit system for the plot axes.
696
+ plotrange: tuple
697
+ A tuple containing the minimum and maximum values for both axes (xmin, xmax, ymin, ymax).
698
+ sizes: tuple
699
+ A tuple containing the number of points for each axis (nx, ny).
700
+
701
+ Returns
702
+ -------
703
+ tuple
704
+ A tuple containing the x and y axis coordinates.
705
+ """
706
+
707
+ scale_factor = 1.0 / RE if axisunit == AxisUnit.EARTH else 1.0
708
+ start = tuple(s * scale_factor for s in plotrange[:2])
709
+ stop = tuple(s * scale_factor for s in plotrange[2:])
710
+
711
+ # Vectorized generation of coordinates for efficiency
712
+ x, y = np.linspace(*start, num=sizes[0]), np.linspace(*stop, num=sizes[1])
713
+
714
+ return x, y
715
+
716
+
717
+ def _fillinnerBC(data: np.ndarray):
718
+ """
719
+ Fill sparsity/inner boundary cells with NaN.
720
+ """
721
+ data[data == 0] = np.nan
722
+
723
+
724
+ def set_colorbar(
725
+ colorscale: ColorScale = ColorScale.Linear,
726
+ v1: float = np.nan,
727
+ v2: float = np.nan,
728
+ data: np.ndarray = np.array([1.0]),
729
+ linthresh: float = 1.0,
730
+ logstep: float = 1.0,
731
+ linscale: float = 0.03,
732
+ ):
733
+ """
734
+ Creates a color normalization object and tick values for a colorbar.
735
+
736
+ Parameters
737
+ ----------
738
+ colorscale: ColorScale, optional
739
+ The type of color scale to use. Can be 'Linear', 'Log', or 'SymLog'.
740
+ Defaults to 'Linear'.
741
+ v1: float, optional
742
+ The minimum value for the colorbar. Defaults to np.nan, which means
743
+ it will be inferred from the data.
744
+ v2: float, optional
745
+ The maximum value for the colorbar. Defaults to np.nan, which means
746
+ it will be inferred from the data.
747
+ data: np.ndarray, optional
748
+ The data to use for inferring the colorbar limits if v1 and v2 are
749
+ not provided. Defaults to np.array([1.0]).
750
+ linthresh: float, optional
751
+ The threshold value for symmetric log color scales. Defaults to 1.0.
752
+ logstep: float, optional
753
+ The step size for tick values in log color scales. Defaults to 1.0.
754
+ linscale: float, optional
755
+ A scaling factor for linear regions in symmetric log color scales.
756
+ Defaults to 0.03.
757
+
758
+ Returns
759
+ -------
760
+ tuple
761
+ A tuple containing:
762
+ - norm: A Matplotlib color normalization object for the colorbar.
763
+ - ticks: A list of tick values for the colorbar.
764
+
765
+ Raises
766
+ ------
767
+ ValueError
768
+ If an invalid colorscale type is provided.
769
+
770
+ Notes
771
+ -----
772
+ - The 'SymLog' colorscale is currently not fully implemented.
773
+ """
774
+ import matplotlib
775
+
776
+ vmin, vmax = set_plot_limits(v1, v2, data, colorscale)
777
+ if colorscale == ColorScale.Linear:
778
+ levels = matplotlib.ticker.MaxNLocator(nbins=255).tick_values(vmin, vmax)
779
+ norm = matplotlib.colors.BoundaryNorm(levels, ncolors=256, clip=True)
780
+ ticks = matplotlib.ticker.LinearLocator(numticks=9)
781
+ elif colorscale == ColorScale.Log: # logarithmic
782
+ norm = matplotlib.colors.LogNorm(vmin, vmax)
783
+ ticks = matplotlib.ticker.LogLocator(base=10, subs=range(0, 9))
784
+ else: # symmetric log
785
+ logthresh = int(math.floor(math.log10(linthresh)))
786
+ minlog = int(math.ceil(math.log10(-vmin)))
787
+ maxlog = int(math.ceil(math.log10(vmax)))
788
+ # TODO: fix this!
789
+ # norm = matplotlib.colors.SymLogNorm(linthresh, linscale, vmin, vmax, base=10)
790
+ # ticks = [ [-(10.0**x) for x in minlog:-logstep:logthresh]..., 0.0,
791
+ # [10.0**x for x in logthresh:logstep:maxlog]..., ]
792
+
793
+ return norm, ticks
794
+
795
+
796
+ def set_plot_limits(
797
+ vmin: float,
798
+ vmax: float,
799
+ data: np.ndarray,
800
+ colorscale: ColorScale = ColorScale.Linear,
801
+ ) -> tuple:
802
+ """
803
+ Calculates appropriate plot limits based on data and colorscale.
804
+ """
805
+
806
+ if colorscale in (ColorScale.Linear, ColorScale.SymLog):
807
+ vmin = vmin if not math.isinf(vmin) else np.nanmin(data)
808
+ vmax = vmax if not math.isinf(vmax) else np.nanmax(data)
809
+ else: # Logarithmic colorscale
810
+ positive_data = data[data > 0.0] # Exclude non-positive values
811
+ vmin = vmin if not math.isinf(vmin) else np.min(positive_data)
812
+ vmax = vmax if not math.isinf(vmax) else np.max(positive_data)
813
+
814
+ return vmin, vmax
815
+
816
+
817
+ def configure_plot(
818
+ c: matplotlib.cm.ScalarMappable, # Assuming c is a colormap or similar
819
+ ax: matplotlib.pyplot.Axes,
820
+ plot_args: PlotArgs,
821
+ ticks: list,
822
+ add_colorbar: bool = True,
823
+ ):
824
+ """
825
+ Configures plot elements based on provided arguments.
826
+ """
827
+
828
+ import matplotlib.pyplot as plt
829
+
830
+ title = plot_args.str_title
831
+ x_label = plot_args.strx
832
+ y_label = plot_args.stry
833
+ colorbar_title = plot_args.cb_title
834
+
835
+ # Add colorbar if requested
836
+ if add_colorbar:
837
+ cb = plt.colorbar(c, ax=ax, ticks=ticks, fraction=0.04, pad=0.02)
838
+ if colorbar_title:
839
+ cb.ax.set_ylabel(colorbar_title)
840
+ cb.ax.tick_params(direction="in")
841
+
842
+ # Set plot title and labels
843
+ ax.set_title(title, fontweight="bold")
844
+ ax.set_xlabel(x_label)
845
+ ax.set_ylabel(y_label)
846
+ ax.set_aspect("equal")
847
+
848
+ # Style plot borders and ticks
849
+ for spine in ax.spines.values():
850
+ spine.set_linewidth(2.0)
851
+ ax.tick_params(axis="both", which="major", width=2.0, length=3)
852
+
853
+
854
+ def vdfslice(
855
+ meta: Vlsv,
856
+ location: tuple | list,
857
+ ax=None,
858
+ limits: tuple = (float("-inf"), float("inf"), float("-inf"), float("inf")),
859
+ verbose: bool = False,
860
+ species: str = "proton",
861
+ unit: AxisUnit = AxisUnit.SI,
862
+ unitv: str = "km/s",
863
+ vmin: float = float("-inf"),
864
+ vmax: float = float("inf"),
865
+ slicetype: str = None,
866
+ vslicethick: float = 0.0,
867
+ center: str = None,
868
+ weight: str = "particle",
869
+ flimit: float = -1.0,
870
+ **kwargs,
871
+ ):
872
+ v1, v2, r1, r2, weights, strx, stry, str_title = prep_vdf(
873
+ meta,
874
+ location,
875
+ species,
876
+ unit,
877
+ unitv,
878
+ slicetype,
879
+ vslicethick,
880
+ center,
881
+ weight,
882
+ flimit,
883
+ verbose,
884
+ )
885
+
886
+ import matplotlib
887
+ import matplotlib.pyplot as plt
888
+
889
+ if math.isinf(vmin):
890
+ vmin = np.min(weights)
891
+ if math.isinf(vmax):
892
+ vmax = np.max(weights)
893
+
894
+ if verbose:
895
+ print(f"Active f range is {vmin}, {vmax}")
896
+
897
+ if not ax:
898
+ ax = plt.gca()
899
+
900
+ norm = matplotlib.colors.LogNorm(vmin, vmax)
901
+
902
+ h = ax.hist2d(v1, v2, bins=(r1, r2), weights=weights, norm=norm, shading="flat")
903
+
904
+ ax.set_title(str_title, fontweight="bold")
905
+ ax.set_xlabel(strx)
906
+ ax.set_ylabel(stry)
907
+ ax.set_aspect("equal")
908
+ ax.grid(color="grey", linestyle="-")
909
+ ax.tick_params(direction="in")
910
+
911
+ cb = plt.colorbar(h[3], ax=ax, fraction=0.04, pad=0.02)
912
+ cb.ax.tick_params(which="both", direction="in")
913
+ cb_title = cb.ax.set_ylabel("f(v)")
914
+
915
+ # TODO: Draw vector of magnetic field direction
916
+ # if slicetype in ("xy", "xz", "yz"):
917
+
918
+ return h[3] # h[0] is 2D data, h[1] is x axis, h[2] is y axis
919
+
920
+
921
+ def prep_vdf(
922
+ meta: Vlsv,
923
+ location: tuple | list,
924
+ species: str = "proton",
925
+ unit: AxisUnit = AxisUnit.SI,
926
+ unitv: str = "km/s",
927
+ slicetype: str = None,
928
+ vslicethick: float = 0.0,
929
+ center: str = None,
930
+ weight: str = "particle",
931
+ flimit: float = -1.0,
932
+ verbose: bool = False,
933
+ ):
934
+ ncells = meta.ncells
935
+
936
+ if species in meta.meshes:
937
+ vmesh = meta.meshes[species]
938
+ else:
939
+ raise ValueError(f"Unable to detect population {species}")
940
+
941
+ if not slicetype in (None, "xy", "xz", "yz", "bperp", "bpar1", "bpar2"):
942
+ raise ValueError(f"Unknown type {slicetype}")
943
+
944
+ if unit == AxisUnit.EARTH:
945
+ location = [loc * RE for loc in location]
946
+
947
+ # Set unit conversion factor
948
+ unitvfactor = 1e3 if unitv == "km/s" else 1.0
949
+
950
+ # Get closest cell ID from input coordinates
951
+ cidReq = meta.getcell(location)
952
+ cidNearest = meta.getnearestcellwithvdf(cidReq)
953
+
954
+ # Set normal direction
955
+ if not slicetype:
956
+ if ncells[1] == 1 and ncells[2] == 1: # 1D, select xz
957
+ slicetype = "xz"
958
+ elif ncells[1] == 1: # polar
959
+ slicetype = "xz"
960
+ elif ncells[2] == 1: # ecliptic
961
+ slicetype = "xy"
962
+ else:
963
+ slicetype = "xy"
964
+
965
+ if slicetype in ("xy", "yz", "xz"):
966
+ if slicetype == "xy":
967
+ dir1, dir2, dir3 = 0, 1, 2
968
+ ŝ = [0.0, 0.0, 1.0]
969
+ elif slicetype == "xz":
970
+ dir1, dir2, dir3 = 0, 2, 1
971
+ ŝ = [0.0, 1.0, 0.0]
972
+ elif slicetype == "yz":
973
+ dir1, dir2, dir3 = 1, 2, 0
974
+ ŝ = [1.0, 0.0, 0.0]
975
+ v1size = vmesh.vblocks[dir1] * vmesh.vblock_size[dir1]
976
+ v2size = vmesh.vblocks[dir2] * vmesh.vblock_size[dir2]
977
+
978
+ v1min, v1max = vmesh.vmin[dir1], vmesh.vmax[dir1]
979
+ v2min, v2max = vmesh.vmin[dir2], vmesh.vmax[dir2]
980
+ elif slicetype in ("bperp", "bpar1", "bpar2"):
981
+ # TODO: WIP
982
+ pass
983
+
984
+ if not math.isclose((v1max - v1min) / v1size, (v2max - v2min) / v2size):
985
+ warnings.warn("Noncubic vgrid applied!")
986
+
987
+ cellsize = (v1max - v1min) / v1size
988
+
989
+ r1 = np.linspace(v1min / unitvfactor, v1max / unitvfactor, v1size + 1)
990
+ r2 = np.linspace(v2min / unitvfactor, v2max / unitvfactor, v2size + 1)
991
+
992
+ vcellids, vcellf = meta.read_vcells(cidNearest, species)
993
+
994
+ V = meta.getvcellcoordinates(vcellids, species)
995
+
996
+ if center:
997
+ if center == "bulk": # centered with bulk velocity
998
+ if meta.has_variable("moments"): # From a restart file
999
+ Vcenter = meta.read_variable("restart_V", cidNearest)
1000
+ elif meta.has_variable(species * "/vg_v"): # Vlasiator 5
1001
+ Vcenter = meta.read_variable(species * "/vg_v", cidNearest)
1002
+ elif meta.has_variable(species * "/V"):
1003
+ Vcenter = meta.read_variable(species * "/V", cidNearest)
1004
+ else:
1005
+ Vcenter = meta.read_variable("V", cidNearest)
1006
+ elif center == "peak": # centered on highest VDF-value
1007
+ Vcenter = np.maximum(vcellf)
1008
+
1009
+ V = np.array(
1010
+ [np.fromiter((v[i] - Vcenter for i in range(3)), dtype=float) for v in V]
1011
+ )
1012
+
1013
+ # Set sparsity threshold
1014
+ if flimit < 0:
1015
+ if meta.has_variable(species + "/vg_effectivesparsitythreshold"):
1016
+ flimit = meta.readvariable(
1017
+ species + "/vg_effectivesparsitythreshold", cidNearest
1018
+ )
1019
+ elif meta.has_variable(species + "/EffectiveSparsityThreshold"):
1020
+ flimit = meta.read_variable(
1021
+ species + "/EffectiveSparsityThreshold", cidNearest
1022
+ )
1023
+ else:
1024
+ flimit = 1e-16
1025
+
1026
+ # Drop velocity cells which are below the sparsity threshold
1027
+ findex_ = vcellf >= flimit
1028
+ fselect = vcellf[findex_]
1029
+ Vselect = V[findex_]
1030
+
1031
+ if slicetype in ("xy", "yz", "xz"):
1032
+ v1select = np.array([v[dir1] for v in Vselect])
1033
+ v2select = np.array([v[dir2] for v in Vselect])
1034
+ vnormal = np.array([v[dir3] for v in Vselect])
1035
+
1036
+ vec = ("vx", "vy", "vz")
1037
+ strx = vec[dir1]
1038
+ stry = vec[dir2]
1039
+ elif slicetype in ("bperp", "bpar1", "bpar2"):
1040
+ v1select = np.empty(len(Vselect), dtype=Vselect.dtype)
1041
+ v2select = np.empty(len(Vselect), dtype=Vselect.dtype)
1042
+ vnormal = np.empty(len(Vselect), dtype=Vselect.dtype)
1043
+ # TODO: WIP
1044
+ # for v1s, v2s, vn, vs in zip(v1select, v2select, vnormal, Vselect):
1045
+ # v1s, v2s, vn = Rinv * Vselect
1046
+
1047
+ # if slicetype == "bperp":
1048
+ # strx = r"$v_{B \times V}$"
1049
+ # stry = r"$v_{B \times (B \times V)}$"
1050
+ # elif slicetype == "bpar2":
1051
+ # strx = r"$v_{B}$"
1052
+ # stry = r"$v_{B \times V}$"
1053
+ # elif slicetype == "bpar1":
1054
+ # strx = r"$v_{B \times (B \times V)}$"
1055
+ # stry = r"$v_{B}$"
1056
+
1057
+ unitstr = f" [{unitv}]"
1058
+ strx += unitstr
1059
+ stry += unitstr
1060
+
1061
+ if vslicethick < 0: # Set a proper thickness
1062
+ if any(
1063
+ i == 1.0 for i in ŝ
1064
+ ): # Assure that the slice cut through at least 1 vcell
1065
+ vslicethick = cellsize
1066
+ else: # Assume cubic vspace grid, add extra space
1067
+ vslicethick = cellsize * (math.sqrt(3) + 0.05)
1068
+
1069
+ # Weights using particle flux or phase-space density
1070
+ fweight = (
1071
+ fselect * np.linalg.norm([v1select, v2select, vnormal])
1072
+ if weight == "flux"
1073
+ else fselect
1074
+ )
1075
+
1076
+ # Select cells within the slice area
1077
+ if vslicethick > 0.0:
1078
+ ind_ = abs(vnormal) <= 0.5 * vslicethick
1079
+ v1, v2, fweight = v1select[ind_], v2select[ind_], fweight[ind_]
1080
+ else:
1081
+ v1, v2 = v1select, v2select
1082
+
1083
+ v1 = [v / unitvfactor for v in v1]
1084
+ v2 = [v / unitvfactor for v in v2]
1085
+
1086
+ str_title = f"t = {meta.time:4.1f}s"
1087
+
1088
+ if verbose:
1089
+ print(f"Original coordinates : {location}")
1090
+ print(f"Original cell : {meta.getcellcoordinates(cidReq)}")
1091
+ print(f"Nearest cell with VDF: {meta.getcellcoordinates(cidNearest)}")
1092
+ print(f"CellID: {cidNearest}")
1093
+
1094
+ if center == "bulk":
1095
+ print(f"Transforming to plasma frame, travelling at speed {Vcenter}")
1096
+ elif center == "peak":
1097
+ print(f"Transforming to peak f-value frame, travelling at speed {Vcenter}")
1098
+
1099
+ print(f"Using VDF threshold value of {flimit}.")
1100
+
1101
+ if vslicethick > 0:
1102
+ print("Performing slice with a counting thickness of $vslicethick")
1103
+ else:
1104
+ print(f"Projecting total VDF to a single plane")
1105
+
1106
+ return v1, v2, r1, r2, fweight, strx, stry, str_title
1107
+
1108
+
1109
+ # Append plotting functions
1110
+ Vlsv.plot = plot
1111
+ Vlsv.pcolormesh = pcolormesh
1112
+ Vlsv.contourf = contourf
1113
+ Vlsv.contour = contour
1114
+ Vlsv.streamplot = streamplot
1115
+ Vlsv.vdfslice = vdfslice