py-pluto 1.1.4__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.
Files changed (73) hide show
  1. pyPLUTO/__init__.py +22 -0
  2. pyPLUTO/amr.py +745 -0
  3. pyPLUTO/baseloadmixin.py +258 -0
  4. pyPLUTO/baseloadstate.py +45 -0
  5. pyPLUTO/codes/echo_load.py +161 -0
  6. pyPLUTO/configure.py +261 -0
  7. pyPLUTO/gui/config.py +174 -0
  8. pyPLUTO/gui/custom_var.py +435 -0
  9. pyPLUTO/gui/globals.py +108 -0
  10. pyPLUTO/gui/main.py +17 -0
  11. pyPLUTO/gui/main_window.py +177 -0
  12. pyPLUTO/gui/panels.py +66 -0
  13. pyPLUTO/gui/utils.py +273 -0
  14. pyPLUTO/h_pypluto.py +84 -0
  15. pyPLUTO/image.py +302 -0
  16. pyPLUTO/imagefuncs/colorbar.py +240 -0
  17. pyPLUTO/imagefuncs/contour.py +254 -0
  18. pyPLUTO/imagefuncs/create_axes.py +464 -0
  19. pyPLUTO/imagefuncs/display.py +306 -0
  20. pyPLUTO/imagefuncs/figure.py +395 -0
  21. pyPLUTO/imagefuncs/imagetools.py +487 -0
  22. pyPLUTO/imagefuncs/interactive.py +403 -0
  23. pyPLUTO/imagefuncs/legend.py +250 -0
  24. pyPLUTO/imagefuncs/plot.py +311 -0
  25. pyPLUTO/imagefuncs/range.py +242 -0
  26. pyPLUTO/imagefuncs/scatter.py +270 -0
  27. pyPLUTO/imagefuncs/set_axis.py +497 -0
  28. pyPLUTO/imagefuncs/streamplot.py +297 -0
  29. pyPLUTO/imagefuncs/zoom.py +428 -0
  30. pyPLUTO/imagemixin.py +259 -0
  31. pyPLUTO/imagestate.py +45 -0
  32. pyPLUTO/load.py +447 -0
  33. pyPLUTO/loadfuncs/baseloadtools.py +71 -0
  34. pyPLUTO/loadfuncs/codeselection.py +48 -0
  35. pyPLUTO/loadfuncs/defpluto.py +123 -0
  36. pyPLUTO/loadfuncs/descriptor.py +102 -0
  37. pyPLUTO/loadfuncs/findfiles.py +182 -0
  38. pyPLUTO/loadfuncs/findformat.py +245 -0
  39. pyPLUTO/loadfuncs/initload.py +203 -0
  40. pyPLUTO/loadfuncs/loadvars.py +227 -0
  41. pyPLUTO/loadfuncs/offsetdata.py +87 -0
  42. pyPLUTO/loadfuncs/offsetfluid.py +408 -0
  43. pyPLUTO/loadfuncs/read_files.py +213 -0
  44. pyPLUTO/loadfuncs/readdata.py +619 -0
  45. pyPLUTO/loadfuncs/readdata_old.py +567 -0
  46. pyPLUTO/loadfuncs/readdefplini.py +101 -0
  47. pyPLUTO/loadfuncs/readfluid.py +479 -0
  48. pyPLUTO/loadfuncs/readformat.py +277 -0
  49. pyPLUTO/loadfuncs/readgridalone.py +224 -0
  50. pyPLUTO/loadfuncs/readgridfile.py +255 -0
  51. pyPLUTO/loadfuncs/readgridout.py +451 -0
  52. pyPLUTO/loadfuncs/readpart.py +419 -0
  53. pyPLUTO/loadfuncs/readtab.py +105 -0
  54. pyPLUTO/loadfuncs/write_files.py +283 -0
  55. pyPLUTO/loadmixin.py +419 -0
  56. pyPLUTO/loadpart.py +233 -0
  57. pyPLUTO/loadstate.py +68 -0
  58. pyPLUTO/newload.py +81 -0
  59. pyPLUTO/pytools.py +145 -0
  60. pyPLUTO/toolfuncs/findlines.py +551 -0
  61. pyPLUTO/toolfuncs/fourier.py +149 -0
  62. pyPLUTO/toolfuncs/nabla.py +676 -0
  63. pyPLUTO/toolfuncs/parttools.py +152 -0
  64. pyPLUTO/toolfuncs/transform.py +638 -0
  65. pyPLUTO/utils/annotator.py +27 -0
  66. pyPLUTO/utils/inspector.py +145 -0
  67. pyPLUTO/utils/make_docstrings.py +3 -0
  68. py_pluto-1.1.4.dist-info/METADATA +218 -0
  69. py_pluto-1.1.4.dist-info/RECORD +73 -0
  70. py_pluto-1.1.4.dist-info/WHEEL +5 -0
  71. py_pluto-1.1.4.dist-info/entry_points.txt +2 -0
  72. py_pluto-1.1.4.dist-info/licenses/LICENSE +27 -0
  73. py_pluto-1.1.4.dist-info/top_level.txt +1 -0
@@ -0,0 +1,551 @@
1
+ from typing import Any
2
+
3
+ import contourpy as cp
4
+ import matplotlib.colors as mcol
5
+ import matplotlib.pyplot as plt
6
+ import numpy as np
7
+ from numpy.typing import NDArray
8
+ from scipy.integrate import solve_ivp
9
+
10
+ from ..h_pypluto import check_par, makelist
11
+
12
+
13
+ def _check_var(self, var: str | NDArray, transpose: bool = False) -> np.ndarray:
14
+ """Function that checks returns a variable. If the variable is a
15
+ numpy array, it is simply returned (and the transpose is taken into
16
+ account). If the variable is a string, the variable is retrieved
17
+ from the dataset. If the variable is not found, an error is raised.
18
+
19
+ Returns
20
+ -------
21
+ - var: np.ndarray
22
+ The variable.
23
+
24
+ Parameters
25
+ ----------
26
+ - var (not optional): str | np.ndarray
27
+ The variable to be checked.
28
+ - transpose: bool, default False
29
+ If True, the variable is transposed.
30
+
31
+ ----
32
+
33
+ Examples
34
+ --------
35
+ - Example #1: var is a numpy array
36
+
37
+ >>> var = np.array([1, 2, 3])
38
+ >>> D._check_var(var, False)
39
+ array([1, 2, 3])
40
+
41
+ - Example #2: var is a string
42
+
43
+ >>> D = pp.Load()
44
+ >>> D._check_var("Bx1", False)
45
+ D.Bx1
46
+
47
+ """
48
+ # If var is a string the code tries to recover it from the class attributes,
49
+ # if failed it raises an error
50
+ if isinstance(var, str):
51
+ try:
52
+ var = getattr(self, var)
53
+ except ValueError:
54
+ raise ValueError(f"Variable {var} not found in the dataset.")
55
+
56
+ # If the transpose keyword is True, the variable is transposed.
57
+ if transpose is True:
58
+ var = var.T
59
+
60
+ # Return the variable
61
+ return var
62
+
63
+
64
+ def _vector_field(
65
+ t: float,
66
+ y: np.ndarray,
67
+ var1: np.ndarray,
68
+ var2: np.ndarray,
69
+ xc: np.ndarray,
70
+ yc: np.ndarray,
71
+ ) -> list[np.ndarray]:
72
+ """Compute the vector field at the given time and coordinates by
73
+ interpolating the variables var1 and var2 at the given coordinates.
74
+ The interpolation is made through the routine np.interpolate.
75
+
76
+ Returns
77
+ -------
78
+ - [qx, qy]: list[np.ndarray]
79
+ The x1 and x2 vector field components, within a list
80
+
81
+ Parameters
82
+ ----------
83
+ - t (not optional): float
84
+ The time variable (not used here).
85
+ - var1 (not optional): np.ndarray
86
+ The first variable to be interpolated.
87
+ - var2 (not optional): np.ndarray
88
+ The second variable to be interpolated.
89
+ - xc (not optional): np.ndarray
90
+ The x coordinates of the grid.
91
+ - y (not optional): np.ndarray
92
+ The coordinates. The first and second dimension are x and y.
93
+ - yc (not optional): np.ndarray
94
+ The y coordinates of the grid.
95
+
96
+ ----
97
+
98
+ Examples
99
+ --------
100
+ - Example #1: Compute the vector field at the given time and coordinates
101
+
102
+ >>> vector_field(t, y, var1, var2, xc, yc)
103
+
104
+ """
105
+ # Get the coordinates
106
+ x, y = y
107
+
108
+ # Compute indices for closest grid points in xc and yc
109
+ i0 = np.abs(x - xc).argmin()
110
+ j0 = np.abs(y - yc).argmin()
111
+
112
+ # Interpolate U and V at the given coordinates
113
+ scrhUx = np.interp(x, xc, var1[:, j0])
114
+ scrhUy = np.interp(y, yc, var1[i0])
115
+ scrhVx = np.interp(x, xc, var2[:, j0])
116
+ scrhVy = np.interp(y, yc, var2[i0])
117
+
118
+ # Compute the resulting vector field components
119
+ qx = scrhUx + scrhUy - var1[i0, j0]
120
+ qy = scrhVx + scrhVy - var2[i0, j0]
121
+
122
+ # Return the vector field
123
+ return [qx, qy]
124
+
125
+
126
+ def find_fieldlines(
127
+ self,
128
+ var1: str | np.ndarray,
129
+ var2: str | np.ndarray,
130
+ x0: list | float | None = None,
131
+ y0: list | float | None = None,
132
+ text: bool = False,
133
+ check: bool = True,
134
+ **kwargs: Any,
135
+ ) -> list:
136
+ """Find field lines using the vector field. The field lines are
137
+ computed by interpolating the variables var1 and var2 at the
138
+ footpoints x0 and y0. Different integration algorithms are
139
+ available, based on the method solve_ivp of the scipy package.
140
+
141
+ Returns
142
+ -------
143
+ - linelist: list
144
+ A list of lists containing the coordinates of the field lines.
145
+ The strcuture of the list is [[x1, y1], [x2, y2], ...] where
146
+ x1, y1, x2, y2 are numpy arrays representing the coordinates of
147
+ the field lines.
148
+
149
+ Parameters
150
+ ----------
151
+ - atol: float, default 1e-6
152
+ The absolute tolerance for the integration.
153
+ - close: bool, default True
154
+ If True, it checks if the line is closed on itself.
155
+ - ctol: float, default 1e-6
156
+ The absolute tolerance for line closing on itself.
157
+ - dense: bool, default False
158
+ If True, the grid is dense (dense=True) or sparse (dense=False).
159
+ - maxstep: float, default 100*step
160
+ The maximum step size for the integration.
161
+ - minstep: float, default 0.05*step
162
+ The minimum step size for the integration (only used if order is LSODA).
163
+ - numsteps: int, default 16384
164
+ The maximum number of steps for the integration.
165
+ - order: str, default 'RK45'
166
+ The integration method. Available options are: 'RK45', 'RK23', 'DOP853',
167
+ 'Radau', 'BDF', 'LSODA'.
168
+ - rtol: float, default 1e-6
169
+ The relative tolerance for the integration.
170
+ - step: float, default abs(min((xend-xbeg)/self.nx1, (yend-ybeg)/self.nx2))
171
+ The initial step size for the integration.
172
+ - text: bool, default False
173
+ If True some additional information is printed.
174
+ - transpose: bool, default False
175
+ If True, the variables are transposed.
176
+ - var1 (not optional): str | NDArray
177
+ The first variable to be interpolated.
178
+ - var2 (not optional): str | NDArray
179
+ The second variable to be interpolated.
180
+ - x0: list
181
+ The x coordinates of the footpoints.
182
+ - x1: NDArray | list | None, default self.x1
183
+ The x coordinates of the grid.
184
+ - x2: NDArray | list | None, default self.x2
185
+ The y coordinates of the grid.
186
+ - y0: list
187
+ The y coordinates of the footpoints.
188
+
189
+ ----
190
+
191
+ Examples
192
+ --------
193
+ - Example #1: Find field lines using the vector field
194
+
195
+ >>> find_fieldlines(var1, var2, x0, y0)
196
+
197
+ - Example #2: Find field lines using two strings 'Bx1' and 'Bx2'
198
+
199
+ >>> find_fieldlines("Bx1", "Bx2", x0, y0)
200
+
201
+ - Example #3: Find field lines using two variables and two footpoints
202
+
203
+ >>> find_fieldlines(var1, var2, [x1, x2], [y1, y2])
204
+
205
+ """
206
+ # Check parameters
207
+ param = {
208
+ "atol",
209
+ "close",
210
+ "ctol",
211
+ "dense",
212
+ "maxstep",
213
+ "minstep",
214
+ "numsteps",
215
+ "order",
216
+ "rtol",
217
+ "step",
218
+ "transpose",
219
+ "x1",
220
+ "x2",
221
+ }
222
+ if check is True:
223
+ check_par(param, "find_fieldlines", **kwargs)
224
+
225
+ # Get the variable, if it is a string, get the variable from the dataset.
226
+ # The .T is used to transpose the variable to the correct shape.
227
+ varx = self._check_var(var1, kwargs.get("transpose", False))
228
+ vary = self._check_var(var2, kwargs.get("transpose", False))
229
+
230
+ # Get the grid information
231
+ xc = kwargs.get("x1", self.x1)
232
+ yc = kwargs.get("x2", self.x2)
233
+
234
+ # Check if the grid is uniform
235
+
236
+ # if not np.all(np.diff(xc) == np.diff(xc)[0]):
237
+ # err = "The grid is not uniform. Only uniform grids are supported."
238
+ # raise ValueError(err)
239
+
240
+ # Get the footpoints
241
+ if x0 is None or y0 is None:
242
+ raise ValueError("Footpoints not provided. Please provide footpoints!")
243
+
244
+ # Make sure x0 and y0 are lists
245
+ x0 = makelist(x0)
246
+ y0 = makelist(y0)
247
+
248
+ # Get the domain size (Take the initial and final coordinates
249
+ # slightly larger to allow a seed to be specified on the boundary).
250
+ xbeg = xc[0] - 0.51 * (xc[1] - xc[0])
251
+ xend = xc[-1] + 0.51 * (xc[-1] - xc[-2])
252
+
253
+ ybeg = yc[0] - 0.51 * (yc[1] - yc[0])
254
+ yend = yc[-1] + 0.51 * (yc[-1] - yc[-2])
255
+
256
+ # Set the keywords
257
+ rtol = kwargs.get("rtol", 1.0e-3)
258
+ atol = kwargs.get("atol", 1.0e-6)
259
+ ctol = kwargs.get("ctol", 1.0e-6)
260
+ order = kwargs.get("order", "RK45")
261
+ dense = kwargs.get("dense", False)
262
+
263
+ # Set the initial step size and maximum number of steps
264
+ step = np.abs(
265
+ kwargs.get(
266
+ "step", min((xend - xbeg) / self.nx1, (yend - ybeg) / self.nx2)
267
+ )
268
+ )
269
+
270
+ maxstep = kwargs.get("maxstep", 100 * step)
271
+ numstep = int(kwargs.get("numsteps", 16384))
272
+ tfin = maxstep * numstep
273
+
274
+ # Define the system of differential equations
275
+ def system(t, y):
276
+ """System of differential equations for the field lines."""
277
+ return _vector_field(t, y, varx, vary, xc, yc)
278
+
279
+ # Event to detect if the field line exits the domain
280
+ def outside_domain(t, y):
281
+ """Event to detect if the field line exits the domain."""
282
+ if y[0] < xbeg or y[0] > xend or y[1] < ybeg or y[1] > yend:
283
+ return 0 # Trigger event (exiting the domain)
284
+ return 1 # Do not trigger event (still within the domain)
285
+
286
+ # Event to detect if the field line closes on itself
287
+ def close_to_start(t, y):
288
+ """Event to detect if the field line closes on itself."""
289
+ dist_0 = np.linalg.norm(y - np.asarray(self.init_pos))
290
+ if dist_0 < ctol and t > maxstep:
291
+ self.loop_dom = True
292
+ return 0 # Trigger event (closing on itself)
293
+ self.oldpos = y
294
+ return 1 # Do not trigger event (open line)
295
+
296
+ # Event to detect if the maximum number of steps is reached
297
+ def max_num_steps(t, y):
298
+ """Event to detect if the maximum number of steps is reached."""
299
+ self.stepnum += 1
300
+ if self.stepnum > numstep:
301
+ return 0 # Trigger event (maximum number of steps reached)
302
+ return 1 # Do not trigger event (still below maximum step number)
303
+
304
+ # Set the events to be triggered
305
+ close_to_start.terminal = (
306
+ True if kwargs.get("close", True) is True else False
307
+ )
308
+ close_to_start.direction = 0
309
+
310
+ outside_domain.terminal = True
311
+ outside_domain.direction = 0
312
+
313
+ max_num_steps.terminal = True
314
+ max_num_steps.direction = 0
315
+
316
+ # Initilaize the list of lines and set the keywords
317
+ lines_list = []
318
+ linekwargs = {}
319
+
320
+ if order == "LSODA":
321
+ linekwargs["minstep"] = kwargs.get("minstep", 0.05 * step)
322
+
323
+ # Iterate on the footpoints
324
+ for ind, xp in enumerate(x0):
325
+ # Set the initial conditions
326
+ self.loop_dom = False
327
+ yp = y0[ind]
328
+ self.init_pos = [xp, yp]
329
+ self.oldpos = [xp, yp]
330
+ self.stepnum = 0
331
+ t_span = (0, tfin)
332
+
333
+ # Integrate forward
334
+ sol_forward = solve_ivp(
335
+ system,
336
+ t_span,
337
+ [xp, yp],
338
+ method=order,
339
+ events=[outside_domain, max_num_steps, close_to_start],
340
+ rtol=rtol,
341
+ atol=atol,
342
+ max_step=maxstep,
343
+ first_step=step,
344
+ dense_output=dense,
345
+ **linekwargs,
346
+ )
347
+
348
+ # If the line did not close on itself, integrate backward
349
+ numstep = 0 if self.loop_dom is True else numstep
350
+
351
+ # Set the new conditions
352
+ forw_steps = self.stepnum
353
+ self.init_pos = [sol_forward.y.T[:, 0][-1], sol_forward.y.T[:, 1][-1]]
354
+ self.stepnum = 0
355
+ t_span = (0, -tfin)
356
+
357
+ # Integrate backward
358
+ sol_backward = solve_ivp(
359
+ system,
360
+ t_span,
361
+ [xp, yp],
362
+ method=order,
363
+ events=[outside_domain, max_num_steps, close_to_start],
364
+ rtol=rtol,
365
+ atol=atol,
366
+ max_step=maxstep,
367
+ first_step=step,
368
+ dense_output=dense,
369
+ **linekwargs,
370
+ )
371
+
372
+ # Concatenate the solutions (backward and forward)
373
+ x_line = np.vstack((sol_backward.y.T[::-1], sol_forward.y.T))[:, 0]
374
+ y_line = np.vstack((sol_backward.y.T[::-1], sol_forward.y.T))[:, 1]
375
+
376
+ # If the line is closed on itself, close the line
377
+ if self.loop_dom is True:
378
+ x_line = np.append(x_line, x_line[0])
379
+ y_line = np.append(y_line, y_line[0])
380
+
381
+ # Print the time of the integration
382
+ if text is True:
383
+ print("Line with footpoint at x = ", xp, " and y = ", yp)
384
+ print("Final integration time forward: ", sol_forward.t[-1])
385
+ print("Final integration time backward: ", sol_backward.t[-1])
386
+ print("Final step number forward: ", forw_steps)
387
+ print("Final step number backward: ", self.stepnum)
388
+
389
+ # Add the line to the list (if it has more than one point)
390
+ lines_list.append([x_line, y_line]) if len(x_line) > 1 else None
391
+
392
+ # Delete the methods that are not needed
393
+ for method_name in ["init_pos", "stepnum", "out_dom", "oldpos"]:
394
+ if method_name in self.__class__.__dict__:
395
+ delattr(self.__class__, method_name)
396
+
397
+ # Return the list of lines
398
+ return lines_list
399
+
400
+
401
+ def find_contour(
402
+ self, var: str | np.ndarray, check: bool = True, **kwargs: Any
403
+ ) -> list:
404
+ """Generate contour lines for a given variable.
405
+
406
+ Returns
407
+ -------
408
+ - lines_list: list
409
+ List of contour lines. The strcuture of the list is
410
+ [[x1, y1], [x2, y2], ...] where x1, y1, x2, y2 are numpy arrays
411
+ representing the coordinates of the field lines.
412
+
413
+ Parameters
414
+ ----------
415
+ - cmap: str, default 'k'
416
+ The colormap to use to associate each level with a color.
417
+ The colormap can also be a color, which is used for all the levels.
418
+ If not provided, all the lines are associated with the color black.
419
+ - levels: int | np.ndarray, default 10
420
+ The levels of number of levels or the list of levels for the contours.
421
+ If an integer is provided, the levels are generated using a linear or
422
+ logarithmic scale. If an array is provided, the levels are taken from
423
+ the array.
424
+ - levelscale: str, default 'linear'
425
+ The scale of the levels. Available options are 'linear' and
426
+ 'logarithmic'.
427
+ - transpose: bool, default False
428
+ If True, the variable is transposed.
429
+ - var (not optional): str | np.ndarray
430
+ The variable to plot. If a string is provided, the variable is taken
431
+ from the dataset.
432
+ - vmax: float, default np.max(var)
433
+ The maximum value of the variable.
434
+ - vmin: float, default np.min(var)
435
+ The minimum value of the variable.
436
+ - x1: np.ndarray, default self.x1
437
+ The x1 coordinates. If the geometry is non-Cartesian, the x1 cartesian
438
+ coordinates are taken from the dataset.
439
+ - x2: np.ndarray, default self.x2
440
+ The x2 coordinates. If the geometry is non-Cartesian, the x2 cartesian
441
+ coordinates are taken from the dataset.
442
+
443
+ ----
444
+
445
+ Examples
446
+ --------
447
+ - Example #1: Generate contour lines for a given variable.
448
+
449
+ >>> lines_list = find_contour(var)
450
+
451
+ - Example #2: Generate contour lines for a given variable and coordinates.
452
+
453
+ >>> lines_list = find_contour(var, x1=x1, x2=x2)
454
+
455
+ - Example #3: Generate contour lines for a given variable and coordinates
456
+ with a logarithmic scale.
457
+
458
+ >>> lines_list = find_contour(var, x1=x1, x2=x2,
459
+ >>> ... levelscale='logarithmic')
460
+
461
+ - Example #4: Generate contour lines for a given variable and coordinates
462
+ with a logarithmic scale and a colormap.
463
+
464
+ >>> lines_list = find_contour(var, x1=x1, x2=x2,
465
+ >>> ... levelscale='logarithmic', cmap='jet')
466
+
467
+ """
468
+ # Check parameters
469
+ param = {
470
+ "cmap",
471
+ "levels",
472
+ "levelscale",
473
+ "transpose",
474
+ "vmax",
475
+ "vmin",
476
+ "x1",
477
+ "x2",
478
+ }
479
+ if check is True:
480
+ check_par(param, "find_contour", **kwargs)
481
+
482
+ # Get the variable, if it is a string, get the variable from the dataset.
483
+ # The .T is used to transpose the variable to the correct shape.
484
+ var = self._check_var(var, kwargs.get("transpose", False)).T
485
+
486
+ # Get the grid information and provide a default value for the coordinates
487
+ # if they are not provided depending on the geometry.
488
+ if self.geom == "SPHERICAL":
489
+ x1 = self.x1p
490
+ x2 = self.x2p
491
+ elif self.geom == "POLAR" and self.nx2 == 1:
492
+ x1 = self.x1
493
+ x2 = self.x3
494
+ elif self.geom == "POLAR":
495
+ x1 = self.x1c
496
+ x2 = self.x2c
497
+ else:
498
+ x1 = self.x1
499
+ x2 = self.x2
500
+
501
+ # Get the coordinates from the keyword arguments (if provided)
502
+ x1 = kwargs.get("x1", x1)
503
+ x2 = kwargs.get("x2", x2)
504
+
505
+ # Get the variable information (minimum and maximum values)
506
+ vmin = kwargs.get("vmin", np.nanmin(var))
507
+ vmax = kwargs.get("vmax", np.nanmax(var))
508
+
509
+ # Compute the levels of the contours, in linear or logarithmic scale
510
+ levels = kwargs.get("levels", 10)
511
+ levelscale = kwargs.get("levelscale", "linear")
512
+
513
+ # If levels is an integer, the levels are computed in lin or log scale
514
+ if isinstance(levels, int):
515
+ levels = (
516
+ np.linspace(vmin, vmax, levels)
517
+ if levelscale == "linear"
518
+ else np.logspace(np.log10(vmin), np.log10(vmax), levels)
519
+ )
520
+
521
+ # If levels is a float, convert it to a list
522
+ if isinstance(levels, float):
523
+ levels = [levels]
524
+
525
+ # Set colormap (try to get it from the colormap list, if not then use
526
+ # the color provided), if not provided use black.
527
+ if "cmap" in kwargs:
528
+ cmap_val = kwargs.get("cmap")
529
+ try:
530
+ cmap = plt.get_cmap(cmap_val)
531
+ except (ValueError, TypeError):
532
+ cmap = mcol.ListedColormap(cmap_val)
533
+ else:
534
+ cmap = mcol.ListedColormap(["k"])
535
+
536
+ # Initialize the list of lines
537
+ lines_list = []
538
+
539
+ # Get the contour generator and the lines for every level
540
+ cont_gen = cp.contour_generator(x1, x2, var, name="serial")
541
+ for indx, level in enumerate(levels):
542
+ contour = cont_gen.lines(level)
543
+ for line in contour:
544
+ x_c = line[:, 0]
545
+ y_c = line[:, 1]
546
+ col = cmap(indx / (len(levels) - 1)) if "cmap" in kwargs else "k"
547
+
548
+ lines_list.append([x_c, y_c, col]) if len(line) > 1 else None
549
+
550
+ # Return the list of lines
551
+ return lines_list
@@ -0,0 +1,149 @@
1
+ from typing import Any
2
+
3
+ import numpy as np
4
+
5
+ from ..h_pypluto import check_par
6
+
7
+
8
+ def fourier(
9
+ self, f: np.ndarray, check: bool = True, **kwargs: Any
10
+ ) -> tuple[list[np.ndarray], np.ndarray]:
11
+ """Compute the Fourier transform of a given array. The function uses
12
+ the numpy.fft.fftn function. The function returns a tuple containing
13
+ the transformed array and the frequency array (which is a list of
14
+ arrays if the input is in 2D or 3D).
15
+
16
+ Returns
17
+ -------
18
+ - f: np.ndarray
19
+ The transformed array.
20
+ - freqs: np.ndarray | list[np.ndarray]
21
+ The frequency array. It is a list of arrays if the input is in 2D or 3D.
22
+
23
+ Parameters
24
+ ----------
25
+ - dx: float | int | list | np.ndarray | None, default None
26
+ The grid spacing. If None, the grid spacing is set to 1.
27
+ - dy: float | int | list | np.ndarray | None, default None
28
+ The grid spacing. If None, the grid spacing is set to 1.
29
+ - dz: float | int | list | np.ndarray | None, default None
30
+ The grid spacing. If None, the grid spacing is set to 1.
31
+ - f (not optional): np.ndarray
32
+ The array to be transformed.
33
+ - xdir: bool, default True
34
+ If True, the x-direction is transformed.
35
+ - ydir: bool, default True
36
+ If True, the y-direction is transformed.
37
+ - zdir: bool, default True
38
+ If True, the z-direction is transformed.
39
+
40
+ ----
41
+
42
+ Examples
43
+ --------
44
+ - Example #1: Compute the Fourier transform of a given array
45
+
46
+ >>> freqs, f = fourier(func)
47
+
48
+ - Example #2: Compute the Fourier transform of a given array in 2D with
49
+ custom grid spacing
50
+
51
+ >>> freqs, f = fourier(func, dx=1, dy=1)
52
+
53
+ - Example #3: Compute the Fourier transform of a 3D without considering
54
+ the x-direction
55
+
56
+ >>> freqs, f = fourier(func, xdir=False)
57
+
58
+ """
59
+ # Check parameters
60
+ param = {"dx", "dy", "dz", "xdir", "ydir", "zdir"}
61
+ if check is True:
62
+ check_par(param, "fourier", **kwargs)
63
+
64
+ # Convert the input array to a numpy array
65
+ f = np.asarray(f)
66
+
67
+ # Check the dimensions of the input array
68
+ dim = f.ndim
69
+ shp = f.shape
70
+
71
+ # Define the axes to include in the Fourier transform
72
+ axes = []
73
+ freqs = []
74
+
75
+ # Check if dx/dy/dz are provided.
76
+ dir_par = [
77
+ ("dx", "dx1", "xdir", 0),
78
+ ("dy", "dx2", "ydir", 1),
79
+ ("dz", "dx3", "zdir", 2),
80
+ ]
81
+
82
+ # Define the grid spacing
83
+ spacing = {}
84
+
85
+ # Loop over directions
86
+ for pars, def_attr, dir, numdir in dir_par:
87
+ # If the number of dimensions is less than the number of directions
88
+ # break
89
+ if dim <= numdir:
90
+ break
91
+
92
+ # Check if the grid spacing is provided
93
+ try:
94
+ spacing[pars] = _fourier_spacing(kwargs[pars])
95
+ # If the grid spacing is not provided or not valid, use the default
96
+ # grid spacing (and set it to 1 if it still not valid)
97
+ except ValueError:
98
+ spacing[pars] = _fourier_spacing(getattr(self, def_attr))
99
+ spacing[pars] = 1.0 if spacing[pars] is None else spacing[pars]
100
+ # Check if the Fourier transform should be computed in this direction
101
+ if kwargs.get(dir, True) is True and dim > numdir:
102
+ axes.append(numdir)
103
+ # Compute the frequencies
104
+ freqs.append(
105
+ 2.0 * np.pi * np.fft.rfftfreq(shp[numdir], spacing[pars])
106
+ )
107
+
108
+ # Compute the Fourier transform
109
+ fk = np.fft.fftn(f, axes=axes)
110
+
111
+ # Return the Fourier transform and the corresponding frequencies
112
+ slices = tuple(slice(0, dim // 2 + 1) for dim in shp)
113
+ freqs = freqs[0] if len(freqs) == 1 else freqs
114
+ return freqs, np.abs(fk[slices])
115
+
116
+
117
+ def _fourier_spacing(dx: float | int | list | np.ndarray) -> float:
118
+ """Check the grid spacing and return the correct value. If the grid
119
+ spacing is not valid (negative), raise an error.
120
+
121
+ Returns
122
+ -------
123
+ - scrh: float
124
+ The grid spacing.
125
+
126
+ Parameters
127
+ ----------
128
+ - dx (not optional): float | int | list | np.ndarray
129
+ The grid spacing.
130
+
131
+ ----
132
+
133
+ Examples
134
+ --------
135
+ - Example #1: Check the grid spacing and return the correct value
136
+
137
+ >>> scrh = fourier_spacing(dx)
138
+
139
+ """
140
+ # Check if the grid spacing is a list or numpy array, then take the first
141
+ # element
142
+ scrh = dx[0] if not isinstance(dx, (float, int)) else dx
143
+
144
+ # Check if the grid spacing is positive, if not raise an error
145
+ if scrh <= 0:
146
+ raise ValueError("the grid spacing must be positive!")
147
+
148
+ # Return the grid spacing
149
+ return scrh