osiris-utils 1.1.1__tar.gz → 1.1.2__tar.gz

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.
@@ -1,8 +1,8 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: osiris_utils
3
- Version: 1.1.1
3
+ Version: 1.1.2
4
4
  Summary: Utilities to manipulate and visualize OSIRIS framework output data
5
- Author: ['João Pedro Ferreira Biu', 'João Cândido']
5
+ Author: ['João Pedro Ferreira Biu', 'João Cândido', 'Diogo Carvalho']
6
6
  Author-email: ['joaopedrofbiu@tecnico.ulisboa.pt']
7
7
  License: MIT
8
8
  Project-URL: Issues Tracker, https://github.com/joaopedrobiu6/osiris_utils/issues
@@ -37,6 +37,7 @@ Dynamic: description
37
37
  Dynamic: description-content-type
38
38
  Dynamic: keywords
39
39
  Dynamic: license
40
+ Dynamic: license-file
40
41
  Dynamic: project-url
41
42
  Dynamic: requires-dist
42
43
  Dynamic: requires-python
@@ -0,0 +1,15 @@
1
+ from .utils import (time_estimation, filesize_estimation, transverse_average, integrate, animate_2D,
2
+ save_data, read_data, courant2D)
3
+ from .gui.gui import LAVA_Qt, LAVA
4
+ from .data.data import OsirisGridFile, OsirisRawFile, OsirisData, OsirisHIST
5
+ from .data.simulation import Simulation
6
+ from .data.diagnostic import Diagnostic
7
+
8
+ from .postprocessing.postprocess import PostProcess
9
+ from .postprocessing.derivative import Derivative, Derivative_Diagnostic
10
+ from .postprocessing.fft import FFT_Diagnostic, FastFourierTransform
11
+
12
+ from .postprocessing.mean_field_theory_single import MFT_Single
13
+ from .postprocessing.mean_field_theory import MeanFieldTheory_Diagnostic
14
+
15
+ # true div not working because of rtruediv - division is not commutative
@@ -1,8 +1,8 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: osiris_utils
3
- Version: 1.1.1
3
+ Version: 1.1.2
4
4
  Summary: Utilities to manipulate and visualize OSIRIS framework output data
5
- Author: ['João Pedro Ferreira Biu', 'João Cândido']
5
+ Author: ['João Pedro Ferreira Biu', 'João Cândido', 'Diogo Carvalho']
6
6
  Author-email: ['joaopedrofbiu@tecnico.ulisboa.pt']
7
7
  License: MIT
8
8
  Project-URL: Issues Tracker, https://github.com/joaopedrobiu6/osiris_utils/issues
@@ -37,6 +37,7 @@ Dynamic: description
37
37
  Dynamic: description-content-type
38
38
  Dynamic: keywords
39
39
  Dynamic: license
40
+ Dynamic: license-file
40
41
  Dynamic: project-url
41
42
  Dynamic: requires-dist
42
43
  Dynamic: requires-python
@@ -5,10 +5,6 @@ pyproject.toml
5
5
  requirements.txt
6
6
  setup.py
7
7
  osiris_utils/__init__.py
8
- osiris_utils/data.py
9
- osiris_utils/gui.py
10
- osiris_utils/mean_field_theory.py
11
- osiris_utils/simulation_data.py
12
8
  osiris_utils/utils.py
13
9
  osiris_utils.egg-info/PKG-INFO
14
10
  osiris_utils.egg-info/SOURCES.txt
@@ -15,11 +15,11 @@ with open(os.path.join(here, 'requirements.txt'), encoding='utf-8') as f:
15
15
 
16
16
  setup(
17
17
  name='osiris_utils',
18
- version='v1.1.1',
18
+ version='v1.1.2',
19
19
  description=('Utilities to manipulate and visualize OSIRIS framework output data'),
20
20
  long_description=long_description,
21
21
  long_description_content_type='text/x-rst',
22
- author=['João Pedro Ferreira Biu', 'João Cândido'],
22
+ author=['João Pedro Ferreira Biu', 'João Cândido', 'Diogo Carvalho'],
23
23
  author_email=['joaopedrofbiu@tecnico.ulisboa.pt'],
24
24
  license='MIT',
25
25
  classifiers=[
@@ -1,6 +0,0 @@
1
- from .utils import (time_estimation, filesize_estimation, transverse_average, integrate, animate_2D,
2
- save_data, read_data, courant2D)
3
- from .gui import LAVA_Qt, LAVA
4
- from .data import OsirisGridFile, OsirisRawFile, OsirisData, OsirisHIST
5
- from .mean_field_theory import MeanFieldTheory
6
- from .simulation_data import OsirisSimulation
@@ -1,418 +0,0 @@
1
- import numpy as np
2
- import pandas as pd
3
- import h5py
4
-
5
- class OsirisData():
6
- """
7
- Base class for handling OSIRIS simulation data files (HDF5 and HIST formats).
8
-
9
- This class provides common functionality for reading and managing basic attributes
10
- from OSIRIS output files. It serves as the parent class for specialized data handlers.
11
-
12
- Parameters
13
- ----------
14
- filename : str
15
- Path to the data file. Supported formats:
16
- - HDF5 files (.h5 extension)
17
- - HIST files (ending with _ene)
18
-
19
- Attributes
20
- ----------
21
- dt : float
22
- Time step of the simulation [simulation units]
23
- dim : int
24
- Number of dimensions in the simulation (1, 2, or 3)
25
- time : list[float, str]
26
- Current simulation time and units as [value, unit_string]
27
- iter : int
28
- Current iteration number
29
- name : str
30
- Name identifier of the data field
31
- type : str
32
- Type of data (e.g., 'grid', 'particles')
33
- verbose : bool
34
- Verbosity flag controlling diagnostic messages (default: False)
35
- """
36
-
37
- def __init__(self, filename):
38
- self._filename = filename
39
- # self._file = None
40
-
41
- self._verbose = False
42
-
43
- if self._filename.endswith('.h5'):
44
- self._open_file_hdf5(self._filename)
45
- self._load_basic_attributes(self._file)
46
- elif self._filename.endswith('_ene'):
47
- self._open_hist_file(self._filename)
48
- else:
49
- raise ValueError('The file should be an HDF5 file with the extension .h5, or a HIST file ending with _ene.')
50
-
51
-
52
- def _load_basic_attributes(self, f: h5py.File) -> None:
53
- '''Load common attributes from HDF5 file'''
54
- self._dt = float(f['SIMULATION'].attrs['DT'][0])
55
- self._dim = int(f['SIMULATION'].attrs['NDIMS'][0])
56
- self._time = [float(f.attrs['TIME'][0]), f.attrs['TIME UNITS'][0].decode('utf-8')]
57
- self._iter = int(f.attrs['ITER'][0])
58
- self._name = f.attrs['NAME'][0].decode('utf-8')
59
- self._type = f.attrs['TYPE'][0].decode('utf-8')
60
-
61
- def verbose(self, verbose: bool = True):
62
- '''
63
- Set the verbosity of the class
64
-
65
- Parameters
66
- ----------
67
- verbose : bool, optional
68
- If True, the class will print messages, by default True when calling (False when not calling)
69
- '''
70
- self._verbose = verbose
71
-
72
- def _open_file_hdf5(self, filename):
73
- '''
74
- Open the OSIRIS output file. Usually an HDF5 file or txt.
75
-
76
- Parameters
77
- ----------
78
- filename : str
79
- The path to the HDF5 file.
80
- '''
81
- if self._verbose: print(f'Opening file > {filename}')
82
-
83
- if filename.endswith('.h5'):
84
- self._file = h5py.File(filename, 'r')
85
- else:
86
- raise ValueError('The file should be an HDF5 file with the extension .h5')
87
-
88
- def _open_hist_file(self, filename):
89
- self._df = pd.read_csv(filename, sep=r'\s+', comment='!', header=0, engine='python')
90
-
91
- def _close_file(self):
92
- '''
93
- Close the HDF5 file.
94
- '''
95
- if self._verbose: print('Closing file')
96
- if self._file:
97
- self._file.close()
98
-
99
- @property
100
- def dt(self):
101
- return self._dt
102
- @property
103
- def dim(self):
104
- return self._dim
105
- @property
106
- def time(self):
107
- return self._time
108
- @property
109
- def iter(self):
110
- return self._iter
111
- @property
112
- def name(self):
113
- return self._name
114
- @property
115
- def type(self):
116
- return self._type
117
-
118
- class OsirisGridFile(OsirisData):
119
- """
120
- Handles structured grid data from OSIRIS HDF5 simulations, including electromagnetic fields.
121
-
122
- Parameters
123
- ----------
124
- filename : str
125
- Path to OSIRIS HDF5 grid file (.h5 extension)
126
-
127
- Attributes
128
- ----------
129
- grid : np.ndarray
130
- Grid boundaries as ((x1_min, x1_max), (x2_min, x2_max), ...)
131
- nx : tuple
132
- Number of grid points per dimension (nx1, nx2, nx3)
133
- dx : np.ndarray
134
- Grid spacing per dimension (dx1, dx2, dx3)
135
- x : list[np.ndarray]
136
- Spatial coordinates arrays for each dimension
137
- axis : list[dict]
138
- Axis metadata with keys:
139
- - 'name': Axis identifier (e.g., 'x1')
140
- - 'units': Physical units (LaTeX formatted)
141
- - 'long_name': Descriptive name (LaTeX formatted)
142
- - 'type': Axis type (e.g., 'SPATIAL')
143
- - 'plot_label': Combined label for plotting
144
- data : np.ndarray
145
- Raw field data array (shape depends on simulation dimensions)
146
- units : str
147
- Field units (LaTeX formatted)
148
- label : str
149
- Field label/name (LaTeX formatted, e.g., r'$E_x$')
150
- FFTdata : np.ndarray
151
- Fourier-transformed data (available after calling FFT())
152
- """
153
-
154
- def __init__(self, filename):
155
- super().__init__(filename)
156
-
157
- variable_key = self._get_variable_key(self._file)
158
-
159
- self._units = self._file.attrs['UNITS'][0].decode('utf-8')
160
- self._label = self._file.attrs['LABEL'][0].decode('utf-8')
161
- self._FFTdata = None
162
-
163
- data = np.array(self._file[variable_key][:])
164
-
165
- axis = list(self._file['AXIS'].keys())
166
- if len(axis) == 1:
167
- self._grid = self._file['AXIS/' + axis[0]][()]
168
- self._nx = len(data)
169
- self._dx = (self.grid[1] - self.grid[0] ) / self.nx
170
- self._x = np.arange(self.grid[0], self.grid[1], self.dx)
171
- else:
172
- grid = []
173
- for ax in axis: grid.append(self._file['AXIS/' + ax][()])
174
- self._grid = np.array(grid)
175
- self._nx = self._file[variable_key][()].transpose().shape
176
- self._dx = (self.grid[:, 1] - self.grid[:, 0])/self.nx
177
- self._x = [np.arange(self.grid[i, 0], self.grid[i, 1], self.dx[i]) for i in range(self.dim)]
178
-
179
- self._axis = []
180
- for ax in axis:
181
- axis_data = {
182
- 'name': self._file['AXIS/'+ax].attrs['NAME'][0].decode('utf-8'),
183
- 'units': self._file['AXIS/'+ax].attrs['UNITS'][0].decode('utf-8'),
184
- 'long_name': self._file['AXIS/'+ax].attrs['LONG_NAME'][0].decode('utf-8'),
185
- 'type': self._file['AXIS/'+ax].attrs['TYPE'][0].decode('utf-8'),
186
- 'plot_label': rf'${self._file["AXIS/"+ax].attrs["LONG_NAME"][0].decode("utf-8")}$ $[{self._file["AXIS/"+ax].attrs["UNITS"][0].decode("utf-8")}]$',
187
- }
188
- self._axis.append(axis_data)
189
-
190
- self._data = np.ascontiguousarray(data.T)
191
-
192
- self._close_file()
193
-
194
- def _load_basic_attributes(self, f: h5py.File) -> None:
195
- '''Load common attributes from HDF5 file'''
196
- self._dt = float(f['SIMULATION'].attrs['DT'][0])
197
- self._dim = int(f['SIMULATION'].attrs['NDIMS'][0])
198
- self._time = [float(f.attrs['TIME'][0]), f.attrs['TIME UNITS'][0].decode('utf-8')]
199
- self._iter = int(f.attrs['ITER'][0])
200
- self._name = f.attrs['NAME'][0].decode('utf-8')
201
- self._type = f.attrs['TYPE'][0].decode('utf-8')
202
-
203
- def _get_variable_key(self, f: h5py.File) -> str:
204
- return next(k for k in f.keys() if k not in {'AXIS', 'SIMULATION'})
205
-
206
-
207
-
208
- def _yeeToCellCorner1d(self, boundary):
209
- '''
210
- Converts 1d EM fields from a staggered Yee mesh to a grid with field values centered on the corner of the cell (the corner of the cell [1] has coordinates [1])
211
- '''
212
-
213
- if self.name.lower() in ['b2', 'b3', 'e1']:
214
- if boundary == 'periodic': return 0.5 * (np.roll(self.data, shift=1) + self.data)
215
- else: return 0.5 * (self.data[1:] + self.data[:-1])
216
- elif self.name.lower() in ['b1', 'e2', 'e3']:
217
- if boundary == 'periodic': return self.data
218
- else: return self.data[1:]
219
- else:
220
- raise TypeError(f'This method expects magnetic or electric field grid data but received \'{self.name}\' instead')
221
-
222
-
223
- def _yeeToCellCorner2d(self, boundary):
224
- '''
225
- Converts 2d EM fields from a staggered Yee mesh to a grid with field values centered on the corner of the cell (the corner of the cell [1,1] has coordinates [1,1])
226
- '''
227
-
228
- if self.name.lower() in ['e1', 'b2']:
229
- if boundary == 'periodic': return 0.5 * (np.roll(self.data, shift=1, axis=0) + self.data)
230
- else: return 0.5 * (self.data[1:, 1:] + self.data[:-1, 1:])
231
- elif self.name.lower() in ['e2', 'b1']:
232
- if boundary == 'periodic': return 0.5 * (np.roll(self.data, shift=1, axis=1) + self.data)
233
- else: return 0.5 * (self.data[1:, 1:] + self.data[1:, :-1])
234
- elif self.name.lower() in ['b3']:
235
- if boundary == 'periodic':
236
- return 0.5 * (np.roll((0.5 * (np.roll(self.data, shift=1, axis=0) + self.data)), shift=1, axis=1) + (0.5 * (np.roll(self.data, shift=1, axis=0) + self.data)))
237
- else:
238
- return 0.25 * (self.data[1:, 1:] + self.data[:-1, 1:] + self.data[1:, :-1] + self.data[:-1, :-1])
239
- elif self.name.lower() in ['e3']:
240
- if boundary == 'periodic': return self.data
241
- else: return self.data[1:, 1:]
242
- else:
243
- raise TypeError(f'This method expects magnetic or electric field grid data but received \'{self.name}\' instead')
244
-
245
-
246
- def _yeeToCellCorner3d(self, boundary):
247
- '''
248
- Converts 3d EM fields from a staggered Yee mesh to a grid with field values centered on the corner of the cell (the corner of the cell [1,1,1] has coordinates [1,1,1])
249
- '''
250
- if boundary == 'periodic':
251
- raise ValueError('Centering field from 3D simulations considering periodic boundary conditions is not implemented yet')
252
- if self.name.lower() == 'b1':
253
- return 0.25 * (self.data[1:, 1:, 1:] + self.data[1:, :-1, 1:] + self.data[1:, 1:, :-1] + self.data[1:, :-1, :-1])
254
- elif self.name.lower() == 'b2':
255
- return 0.25 * (self.data[1:, 1:, 1:] + self.data[:-1, 1:, 1:] + self.data[1:, 1:, :-1] + self.data[:-1, 1:, :-1])
256
- elif self.name.lower() == 'b3':
257
- return 0.25 * (self.data[1:, 1:, 1:] + self.data[:-1, 1:, 1:] + self.data[1:, :-1, 1:] + self.data[:-1, :-1, 1:])
258
- elif self.name.lower() == 'e1':
259
- return 0.5 * (self.data[1:, 1:, 1:] + self.data[:-1, 1:, 1:])
260
- elif self.name.lower() == 'e2':
261
- return 0.5 * (self.data[1:, 1:, 1:] + self.data[1:, :-1, 1:])
262
- elif self.name.lower() == 'e3':
263
- return 0.5 * (self.data[1:, 1:, 1:] + self.data[1:, 1:, :-1])
264
- else:
265
- raise TypeError(f'This method expects magnetic or electric field grid data but received \'{self.name}\' instead')
266
-
267
- def yeeToCellCorner(self, boundary=None):
268
- ''''
269
- Converts EM fields from a staggered Yee mesh to a grid with field values centered on the corner of the cell.'
270
- Can be used for 1D, 2D and 3D simulations.'
271
- Creates a new attribute `data_centered` with the centered data.'
272
- '''
273
-
274
- cases = {'b1', 'b2', 'b3', 'e1', 'e2', 'e3'}
275
- if self.name not in cases:
276
- raise TypeError(f'This method expects magnetic or electric field grid data but received \'{self.name}\' instead')
277
-
278
- if self.dim == 1:
279
- self.data_centered = self._yeeToCellCorner1d(boundary)
280
- return self.data_centered
281
- elif self.dim == 2:
282
- self.data_centered = self._yeeToCellCorner2d(boundary)
283
- return self.data_centered
284
- elif self.dim == 3:
285
- self.data_centered = self._yeeToCellCorner3d(boundary)
286
- return self.data_centered
287
- else:
288
- raise ValueError(f'Dimension {self.dim} is not supported')
289
-
290
- def FFT(self, axis=(0, )):
291
- '''
292
- Computes the Fast Fourier Transform of the data along the specified axis and shifts the zero frequency to the center.
293
- Transforms the data to the frequency domain. A(x, y, z) -> A(kx, ky, kz)
294
- '''
295
- datafft = np.fft.fftn(self.data, axes=axis)
296
- self._FFTdata = np.fft.fftshift(datafft, axes=axis)
297
-
298
- # Getters
299
- @property
300
- def grid(self):
301
- return self._grid
302
- @property
303
- def nx(self):
304
- return self._nx
305
- @property
306
- def dx(self):
307
- return self._dx
308
- @property
309
- def x(self):
310
- return self._x
311
- @property
312
- def axis(self):
313
- return self._axis
314
- @property
315
- def data(self):
316
- return self._data
317
- @property
318
- def units(self):
319
- return self._units
320
- @property
321
- def label(self):
322
- return self._label
323
- @property
324
- def FFTdata(self):
325
- if self._FFTdata is None:
326
- raise ValueError('The FFT of the data has not been computed yet. Compute it using the FFT method.')
327
- return self._FFTdata
328
- # Setters
329
- @data.setter
330
- def data(self, data):
331
- self._data = data
332
-
333
- def __str__(self):
334
- # write me a template to print with the name, label, units, time, iter, grid, nx, dx, axis, dt, dim in a logical way
335
- return rf'{self.name}' + f'\n' + rf'Time: [{self.time[0]} {self.time[1]}], dt = {self.dt}' + f'\n' + f'Iteration: {self.iter}' + f'\n' + f'Grid: {self.grid}' + f'\n' + f'dx: {self.dx}' + f'\n' + f'Dimensions: {self.dim}D'
336
-
337
-
338
- def __array__(self):
339
- return np.asarray(self.data)
340
-
341
-
342
- class OsirisRawFile(OsirisData):
343
- '''
344
- Class to read the raw data from an OSIRIS HDF5 file.
345
-
346
- Input:
347
- - filename: the path to the HDF5 file
348
-
349
- Attributes:
350
- - axis - a dictionary where each key is a dataset name, and each value is another dictionary containing
351
- name (str): The name of the quantity (e.g., r'x1', r'ene').
352
- units (str): The units associated with that dataset in LaTeX (e.g., r'c/\\omega_p', r'm_e c^2').
353
- long_name (str): The name of the quantity in LaTeX (e.g., r'x_1', r'En2').
354
- dictionary of dictionaries
355
- - data - a dictionary where each key is a dataset name, and each value is the data
356
- dictionary of np.arrays
357
- - dim - the number of dimensions
358
- int
359
- - dt - the time step
360
- float
361
- - grid - maximum and minimum coordinates of the box, for each axis
362
- numpy.ndarray(dim,2)
363
- - iter - the iteration number
364
- int
365
- - name - the name of the species
366
- str
367
- - time - the time and its units
368
- list [time, units]
369
- list [float, str]
370
- - type - type of data (particles in the case of raw files)
371
- str
372
-
373
- '''
374
-
375
- def __init__(self, filename):
376
- super().__init__(filename)
377
-
378
- self.grid = np.array([self._file['SIMULATION'].attrs['XMIN'], self._file['SIMULATION'].attrs['XMAX']]).T
379
-
380
- self.data = {}
381
- self.axis = {}
382
- for key in self._file.keys():
383
- if key == 'SIMULATION': continue
384
-
385
- self.data[key] = np.array(self._file[key][()])
386
-
387
- idx = np.where(self._file.attrs['QUANTS'] == str(key).encode('utf-8'))
388
- axis_data = {
389
- 'name': self._file.attrs['QUANTS'][idx][0].decode('utf-8'),
390
- 'units': self._file.attrs['UNITS'][idx][0].decode('utf-8'),
391
- 'long_name': self._file.attrs['LABELS'][idx][0].decode('utf-8'),
392
- }
393
- self.axis[key] = axis_data
394
-
395
- class OsirisHIST(OsirisData):
396
- ''''
397
- Class to read the data from an OSIRIS HIST file.'
398
-
399
- Input:
400
- - filename: the path to the HIST file
401
-
402
- Attributes:
403
- - filename - the path to the file
404
- str
405
- - verbose - if True, the class will print messages
406
- bool
407
- - df - the data in a pandas DataFrame
408
- pandas.DataFrame
409
- '''
410
- def __init__(self, filename):
411
- super().__init__(filename)
412
-
413
- @property
414
- def df(self):
415
- """
416
- Returns the data in a pandas DataFrame
417
- """
418
- return self._df
@@ -1,266 +0,0 @@
1
- import sys
2
- import os
3
- from PySide6.QtWidgets import (QApplication, QMainWindow, QWidget, QPushButton,
4
- QFileDialog, QMessageBox, QComboBox, QHBoxLayout,
5
- QVBoxLayout, QLabel, QLineEdit, QFrame, QDoubleSpinBox)
6
- from PySide6.QtCore import Qt
7
- from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
8
- import matplotlib.pyplot as plt
9
- from matplotlib.colors import LogNorm
10
- import numpy as np
11
- from .data import OsirisGridFile # Update import as needed
12
- from .utils import integrate, transverse_average # Update import as needed
13
-
14
- class LAVA_Qt(QMainWindow):
15
- def __init__(self):
16
- super().__init__()
17
- self.setWindowTitle('LAVA (LabAstro Visualization Assistant) - OSIRIS Data Grid Viewer')
18
- self.setGeometry(100, 100, 1000, 600)
19
-
20
- # Initialize data
21
- self.data_info = None
22
- self.dims = 0
23
- self.current_ax = None
24
- self.current_folder = None
25
-
26
- # Main widget and layout
27
- self.main_widget = QWidget()
28
- self.setCentralWidget(self.main_widget)
29
- self.main_layout = QVBoxLayout(self.main_widget)
30
-
31
- # Create UI elements
32
- self.create_controls()
33
- self.create_labels_section()
34
- self.create_plot_area()
35
-
36
- def create_controls(self):
37
- # Control buttons frame
38
- control_frame = QWidget()
39
- control_layout = QHBoxLayout(control_frame)
40
-
41
- # Buttons
42
- self.browse_btn = QPushButton('Browse Folder')
43
- self.browse_btn.clicked.connect(self.load_folder)
44
- self.save_btn = QPushButton('Save Plot')
45
- self.save_btn.clicked.connect(self.save_plot)
46
-
47
- # File selector
48
- self.file_selector = QComboBox()
49
- self.file_selector.setPlaceholderText('Select file...')
50
- self.file_selector.currentIndexChanged.connect(self.file_selection_changed)
51
- self.file_selector.view().setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
52
- self.file_selector.setSizeAdjustPolicy(QComboBox.AdjustToContents)
53
-
54
- # Plot type combo box
55
- self.plot_combo = QComboBox()
56
- self.plot_combo.addItem('Select Plot Type')
57
- self.plot_combo.currentTextChanged.connect(self.plot_data)
58
-
59
- control_layout.addWidget(self.browse_btn)
60
- control_layout.addWidget(self.save_btn)
61
- control_layout.addWidget(QLabel('Files:'))
62
- control_layout.addWidget(self.file_selector)
63
- control_layout.addWidget(QLabel('Plot Type:'))
64
- control_layout.addWidget(self.plot_combo)
65
- self.main_layout.addWidget(control_frame)
66
-
67
- def create_labels_section(self):
68
- # Labels frame
69
- labels_frame = QWidget()
70
- labels_layout = QHBoxLayout(labels_frame)
71
-
72
- # Title and labels
73
- self.title_edit = QLineEdit()
74
- self.xlabel_edit = QLineEdit()
75
- self.ylabel_edit = QLineEdit()
76
-
77
- # Connect text changes
78
- self.title_edit.textChanged.connect(self.update_plot_labels)
79
- self.xlabel_edit.textChanged.connect(self.update_plot_labels)
80
- self.ylabel_edit.textChanged.connect(self.update_plot_labels)
81
-
82
-
83
- labels_layout.addWidget(QLabel('Title:'))
84
- labels_layout.addWidget(self.title_edit)
85
- labels_layout.addWidget(QLabel('X Label:'))
86
- labels_layout.addWidget(self.xlabel_edit)
87
- labels_layout.addWidget(QLabel('Y Label:'))
88
- labels_layout.addWidget(self.ylabel_edit)
89
-
90
- # define the size of the labels frame
91
- self.main_layout.addWidget(labels_frame)
92
-
93
-
94
- def load_folder(self):
95
- folder_dialog = QFileDialog()
96
- folderpath = folder_dialog.getExistingDirectory(
97
- self, 'Select Folder with HDF5 Files'
98
- )
99
-
100
- if not folderpath:
101
- return
102
-
103
- try:
104
- self.current_folder = folderpath
105
- self.file_selector.clear()
106
-
107
- # Find all .h5 files
108
- h5_files = [f for f in os.listdir(folderpath) if f.endswith('.h5')]
109
- # all the files end with xxxxxx.h5 so we can use this to order them by the number
110
- def sort_key(filename):
111
- try:
112
- # Split filename into parts and get the numeric portion
113
- base = os.path.splitext(filename)[0] # Remove .h5
114
- numeric_part = base.split('-')[-1] # Get last part after -
115
- return int(numeric_part)
116
- except (IndexError, ValueError):
117
- return 0 # Fallback for malformed filenames
118
-
119
- h5_files.sort(key=sort_key)
120
-
121
- if not h5_files:
122
- raise ValueError('No HDF5 files found in selected folder')
123
-
124
- self.file_selector.addItems(h5_files)
125
- self.file_selector.setCurrentIndex(0)
126
-
127
- except Exception as e:
128
- QMessageBox.critical(self, 'Error', str(e))
129
-
130
- def file_selection_changed(self, index):
131
- '''Handle file selection change in the combo box'''
132
- if index >= 0 and self.current_folder:
133
- filename = self.file_selector.itemText(index)
134
- self.process_file(filename)
135
-
136
- def process_file(self, filename):
137
- try:
138
- filepath = os.path.join(self.current_folder, filename)
139
- gridfile = OsirisGridFile(filepath)
140
- self.dims = len(gridfile.axis)
141
- self.type = gridfile.type
142
-
143
- if self.type == 'grid':
144
- if self.dims == 1:
145
- x = np.arange(gridfile.grid[0], gridfile.grid[1], gridfile.dx)
146
- self.xlabel_edit.setText(r'$%s$ [$%s$]' % (gridfile.axis[0]['long_name'], gridfile.axis[0]['units']))
147
- self.ylabel_edit.setText(r'$%s$ [$%s$]' % (gridfile.label, gridfile.units))
148
- self.data_info = (x, gridfile.data)
149
- elif self.dims == 2:
150
- x = np.arange(gridfile.grid[0][0], gridfile.grid[0][1], gridfile.dx[0])
151
- y = np.arange(gridfile.grid[1][0], gridfile.grid[1][1], gridfile.dx[1])
152
- self.xlabel_edit.setText(r'$%s$ [$%s$]' % (gridfile.axis[0]['long_name'], gridfile.axis[0]['units']))
153
- self.ylabel_edit.setText(r'$%s$ [$%s$]' % (gridfile.axis[1]['long_name'], gridfile.axis[1]['units']))
154
- self.data_info = (x, y, gridfile.data)
155
- elif self.dims == 3:
156
- raise ValueError('3D not supported yet')
157
- else:
158
- raise ValueError('Unsupported dimensionality')
159
-
160
- self.title_edit.setText(r'$%s$ [$%s$]' %( gridfile.label, gridfile.units))
161
- self.update_plot_menu()
162
- self.plot_data()
163
-
164
- else:
165
- QMessageBox.information(self, 'Info', f'{self.type} data not supported yet')
166
-
167
- except Exception as e:
168
- QMessageBox.critical(self, 'Error', str(e))
169
-
170
- def create_plot_area(self):
171
- # Matplotlib figure and canvas
172
- self.figure = plt.figure(figsize=(8, 6))
173
- self.canvas = FigureCanvas(self.figure)
174
- self.main_layout.addWidget(self.canvas)
175
-
176
- def update_plot_labels(self):
177
- if self.current_ax:
178
- self.current_ax.set_xlabel(self.xlabel_edit.text())
179
- self.current_ax.set_ylabel(self.ylabel_edit.text())
180
- self.figure.suptitle(self.title_edit.text())
181
- self.canvas.draw()
182
-
183
- def plot_data(self):
184
- self.figure.clear()
185
- if self.dims == 1:
186
- self.plot_1d()
187
- elif self.dims == 2:
188
- self.plot_2d()
189
- self.update_plot_labels()
190
- self.canvas.draw()
191
-
192
- def plot_1d(self):
193
- x, data = self.data_info
194
- self.current_ax = self.figure.add_subplot(111)
195
- plot_type = self.plot_combo.currentText()
196
-
197
- if 'Line' in plot_type:
198
- self.current_ax.plot(x, data)
199
- elif 'Scatter' in plot_type:
200
- self.current_ax.scatter(x, data)
201
-
202
- self.current_ax.set_xlabel(self.xlabel_edit.text())
203
- self.current_ax.set_ylabel(self.ylabel_edit.text())
204
- self.figure.suptitle(self.title_edit.text())
205
-
206
- def plot_2d(self):
207
- x, y, data = self.data_info
208
- self.current_ax = self.figure.add_subplot(111)
209
- plot_type = self.plot_combo.currentText()
210
-
211
- if 'Quantity' in plot_type:
212
- img = self.current_ax.imshow(data.T, extent=(x[0], x[-1], y[0], y[-1]), origin='lower', aspect='auto')
213
- self.figure.colorbar(img)
214
- elif 'Integral' in plot_type:
215
- avg = integrate(transverse_average(data), x[-1]/len(x))
216
- self.current_ax.plot(x, avg)
217
- elif 'Transverse' in plot_type:
218
- avg = transverse_average(data)
219
- self.current_ax.plot(x, avg)
220
- elif 'Phase' in plot_type:
221
- img = self.current_ax.imshow(np.abs(-data.T), extent=(x[0], x[-1], y[0], y[-1]), origin='lower', aspect='auto', norm=LogNorm())
222
- self.figure.colorbar(img)
223
-
224
- self.current_ax.set_xlabel(self.xlabel_edit.text())
225
- self.current_ax.set_ylabel(self.ylabel_edit.text())
226
- self.figure.suptitle(self.title_edit.text())
227
-
228
- def update_plot_menu(self):
229
-
230
- # Save current plot type before clearing
231
- current_plot_type = self.plot_combo.currentText()
232
- self.plot_combo.clear()
233
-
234
- # Determine items based on dimensions
235
- if self.dims == 1:
236
- items = ['Line Plot', 'Scatter Plot']
237
- elif self.dims == 2:
238
- items = ['Quantity Plot', 'T. Average Integral', 'Transverse Average', 'Phase Space']
239
- else:
240
- items = []
241
-
242
- self.plot_combo.addItems(items)
243
-
244
- # Restore previous selection if possible
245
- if current_plot_type in items:
246
- self.plot_combo.setCurrentText(current_plot_type)
247
- else:
248
- self.plot_combo.setCurrentIndex(0 if items else -1)
249
-
250
- def save_plot(self):
251
- file_dialog = QFileDialog()
252
- filepath, _ = file_dialog.getSaveFileName(
253
- self, 'Save Plot', '', 'PNG Files (*.png);;PDF Files (*.pdf)'
254
- )
255
-
256
- if filepath:
257
- self.figure.savefig(filepath, dpi=800, bbox_inches='tight')
258
-
259
- def LAVA():
260
- app = QApplication(sys.argv)
261
- window = LAVA_Qt()
262
- window.show()
263
- sys.exit(app.exec())
264
-
265
- if __name__ == '__main__':
266
- LAVA()
@@ -1,52 +0,0 @@
1
- import numpy as np
2
- from .data import OsirisGridFile
3
-
4
-
5
- class MeanFieldTheory(OsirisGridFile):
6
- '''
7
- Class to handle the mean field theory on data. Inherits from OsirisGridFile.
8
-
9
- Parameters
10
- ----------
11
- source : str or OsirisGridFile
12
- The filename or an OsirisGridFile object.
13
- axis : int
14
- The axis to average over.
15
- '''
16
- def __init__(self, source, axis=1):
17
- if isinstance(source, OsirisGridFile):
18
- self.__dict__.update(source.__dict__)
19
- else:
20
- super().__init__(source)
21
- self._compute_mean_field(axis=axis)
22
-
23
- def _compute_mean_field(self, axis=1):
24
- self._average = np.expand_dims(np.mean(self.data, axis=axis), axis=axis)
25
- self._fluctuations = self.data - self._average
26
-
27
- def __array__(self):
28
- return self.data
29
-
30
- @property
31
- def average(self):
32
- return self._average
33
-
34
- @property
35
- def delta(self):
36
- return self._fluctuations
37
-
38
- def __str__(self):
39
- return super().__str__() + f'\nAverage: {self.average.shape}\nDelta: {self.delta.shape}'
40
-
41
- def derivative(self, field, axis=0):
42
- '''
43
- Compute the derivative of the average or the fluctuations.
44
-
45
- Parameters
46
- ----------
47
- field : MeanFieldTheory.average or MeanFieldTheory.delta
48
- The field to compute the derivative.
49
- axis : int
50
- The axis to compute the derivative.
51
- '''
52
- return np.gradient(field, self.dx[axis], axis=0)
@@ -1,229 +0,0 @@
1
- """
2
- The utilities on data.py are cool but not useful when you want to work with whole data of a simulation instead
3
- of just a single file. This is what this file is for - deal with ''folders'' of data.
4
-
5
- Took some inspiration from Diogo and Madox's work.
6
-
7
- This would be awsome to compute time derivatives.
8
- """
9
-
10
- import numpy as np
11
- import os
12
- from .data import OsirisGridFile, OsirisRawFile, OsirisHIST
13
- import tqdm
14
- import itertools
15
- import multiprocessing as mp
16
-
17
- class OsirisSimulation:
18
- def __init__(self, simulation_folder):
19
- self._simulation_folder = simulation_folder
20
- if not os.path.isdir(simulation_folder):
21
- raise FileNotFoundError(f"Simulation folder {simulation_folder} not found.")
22
-
23
- def get_moment(self, species, moment):
24
- self._path = f"{self._simulation_folder}/MS/UDIST/{species}/{moment}/"
25
- self._file_template = os.listdir(self._path)[0][:-9]
26
- self._load_attributes(self._file_template)
27
-
28
- def get_field(self, field, centered=False):
29
- if centered:
30
- self._path = f"{self._simulation_folder}/MS/FLD/{field}/"
31
- self._path = f"{self._simulation_folder}/MS/FLD/{field}/"
32
- self._file_template = os.listdir(self._path)[0][:-9]
33
- self._load_attributes(self._file_template)
34
-
35
- def get_density(self, species, quantity):
36
- self._path = f"{self._simulation_folder}/MS/DENSITY/{species}/{quantity}/"
37
- self._file_template = os.listdir(self._path)[0][:-9]
38
- self._load_attributes(self._file_template)
39
-
40
- def _load_attributes(self, file_template):
41
- path_file1 = os.path.join(self._path, file_template + "000001.h5")
42
- dump1 = OsirisGridFile(path_file1)
43
- self._dx = dump1.dx
44
- self._nx = dump1.nx
45
- self._x = dump1.x
46
- self._dt = dump1.dt
47
- self._grid = dump1.grid
48
- self._axis = dump1.axis
49
- self._units = dump1.units
50
- self._name = dump1.name
51
- self._dim = dump1.dim
52
- self._ndump = dump1.iter
53
-
54
- def _data_generator(self, index):
55
- file = os.path.join(self._path, self._file_template + f"{index:06d}.h5")
56
- data_object = OsirisGridFile(file)
57
- if self._current_centered:
58
- data_object.yeeToCellCorner(boundary="periodic")
59
- yield data_object.data_centered if self._current_centered else data_object.data
60
-
61
- def load_all(self, centered=False):
62
- self._current_centered = centered
63
- size = len(sorted(os.listdir(self._path)))
64
- self._data = np.stack([self[i] for i in tqdm.tqdm(range(size), desc="Loading data")])
65
-
66
- def load_all_parallel(self, centered=False, processes=None):
67
- self._current_centered = centered
68
- files = sorted(os.listdir(self._path))
69
- size = len(files)
70
-
71
- if processes is None:
72
- processes = mp.cpu_count()
73
- print(f"Using {processes} CPUs for parallel loading")
74
-
75
- with mp.Pool(processes=processes) as pool:
76
- data = list(tqdm.tqdm(pool.imap(self.__getitem__, range(size)), total=size, desc="Loading data"))
77
-
78
- self._data = np.stack(data)
79
-
80
- def load(self, index, centered=False):
81
- self._current_centered = centered
82
- self._data = next(self._data_generator(index))
83
-
84
- def __getitem__(self, index):
85
- return next(self._data_generator(index))
86
-
87
- def __iter__(self):
88
- for i in itertools.count():
89
- yield next(self._data_generator(i))
90
-
91
- def derivative(self, point, type, axis=None):
92
- if point == "all":
93
- if type == "t":
94
- self._deriv_t = np.gradient(self.data, self.dt, axis=0, edge_order=2)
95
- elif type == "x1":
96
- if self._dim == 1:
97
- self._deriv_x1 = np.gradient(self.data, self.dx, axis=1, edge_order=2)
98
- else:
99
- self._deriv_x1 = np.gradient(self.data, self.dx[0], axis=1, edge_order=2)
100
- elif type == "x2":
101
- self._deriv_x1 = np.gradient(self.data, self.dx[0], axis=2, edge_order=2)
102
- elif type == "x3":
103
- self._deriv_x2 = np.gradient(self.data, self.dx[0], axis=3, edge_order=2)
104
- elif type == "xx":
105
- if len(axis) != 2:
106
- raise ValueError("Axis must be a tuple with two elements.")
107
- self._deriv_xx = np.gradient(np.gradient(self.data, self.dx[axis[0]], axis=axis[0], edge_order=2), self.dx[axis[1]], axis=axis[1], edge_order=2)
108
- elif type == "xt":
109
- if not isinstance(axis, int):
110
- raise ValueError("Axis must be an integer.")
111
- self._deriv_xt = np.gradient(np.gradient(self.data, self.dt, axis=0, edge_order=2), self.dx[axis], axis=axis, edge_order=2)
112
- elif type == "tx":
113
- if not isinstance(axis, int):
114
- raise ValueError("Axis must be an integer.")
115
- self._deriv_tx = np.gradient(np.gradient(self.data, self.dx[axis], axis=axis, edge_order=2), self.dt, axis=axis, edge_order=2)
116
- else:
117
- raise ValueError("Invalid type.")
118
- else:
119
- try:
120
- if type == "x1":
121
- if self._dim == 1:
122
- return np.gradient(self[point], self._dx, axis=0)
123
- else:
124
- return np.gradient(self[point], self._dx[0], axis=0)
125
-
126
- elif type == "x2":
127
- return np.gradient(self[point], self._dx[1], axis=1)
128
-
129
- elif type == "x3":
130
- return np.gradient(self[point], self._dx[2], axis=2)
131
-
132
- elif type == "t":
133
- if point == 0:
134
- return (-3 * self[point] + 4 * self[point + 1] - self[point + 2]) / (2 * self._dt)
135
- # derivate at last point not implemented yet
136
- # elif self[point + 1] is None:
137
- # return (3 * self[point] - 4 * self[point - 1] + self[point - 2]) / (2 * self._dt)
138
- else:
139
- return (self[point + 1] - self[point - 1]) / (2 * self._dt)
140
- else:
141
- raise ValueError("Invalid derivative type. Use 'x1', 'x2' or 't'.")
142
-
143
- except Exception as e:
144
- raise ValueError(f"Error computing derivative at point {point}: {str(e)}")
145
-
146
- # Getters
147
- @property
148
- def data(self):
149
- if self._data is None:
150
- raise ValueError("Data not loaded into memory. Use get_* method with load_all=True or access via generator/index.")
151
- return self._data
152
-
153
- @property
154
- def time(self):
155
- return self._time
156
-
157
- @property
158
- def dx(self):
159
- return self._dx
160
-
161
- @property
162
- def nx(self):
163
- return self._nx
164
-
165
- @property
166
- def x(self):
167
- return self._x
168
-
169
- @property
170
- def dt(self):
171
- return self._dt
172
-
173
- @property
174
- def grid(self):
175
- return self._grid
176
-
177
- @property
178
- def axis(self):
179
- return self._axis
180
-
181
- @property
182
- def units(self):
183
- return self._units
184
-
185
- @property
186
- def name(self):
187
- return self._name
188
-
189
- @property
190
- def dim(self):
191
- return self._dim
192
-
193
- @property
194
- def path(self):
195
- return self
196
-
197
- @property
198
- def simulation_folder(self):
199
- return self._simulation_folder
200
-
201
- @property
202
- def ndump(self):
203
- return self._ndump
204
-
205
- @property
206
- def deriv_t(self):
207
- return self._deriv_t
208
-
209
- @property
210
- def deriv_x1(self):
211
- return self._deriv_x1
212
-
213
- @property
214
- def deriv_x2(self):
215
- return self._deriv_x2
216
-
217
- @property
218
- def deriv_xx(self):
219
- return self._deriv_xx
220
-
221
- @property
222
- def deriv_xt(self):
223
- return self._deriv_xt
224
-
225
- @property
226
- def deriv_tx(self):
227
- return self._deriv_tx
228
-
229
-
File without changes
File without changes
File without changes
File without changes