osiris-utils 1.1.10a0__py3-none-any.whl → 1.2.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.
- benchmarks/benchmark_hdf5_io.py +46 -0
- benchmarks/benchmark_load_all.py +54 -0
- docs/source/api/decks.rst +48 -0
- docs/source/api/postprocess.rst +66 -2
- docs/source/api/sim_diag.rst +1 -1
- docs/source/api/utilities.rst +1 -1
- docs/source/conf.py +2 -1
- docs/source/examples/example_Derivatives.md +78 -0
- docs/source/examples/example_FFT.md +152 -0
- docs/source/examples/example_InputDeck.md +148 -0
- docs/source/examples/example_Simulation_Diagnostic.md +213 -0
- docs/source/examples/quick_start.md +51 -0
- docs/source/examples.rst +14 -0
- docs/source/index.rst +8 -0
- examples/edited-deck.1d +1 -1
- examples/example_Derivatives.ipynb +24 -36
- examples/example_FFT.ipynb +44 -23
- examples/example_InputDeck.ipynb +24 -277
- examples/example_Simulation_Diagnostic.ipynb +27 -17
- examples/quick_start.ipynb +17 -1
- osiris_utils/__init__.py +10 -6
- osiris_utils/cli/__init__.py +6 -0
- osiris_utils/cli/__main__.py +85 -0
- osiris_utils/cli/export.py +199 -0
- osiris_utils/cli/info.py +156 -0
- osiris_utils/cli/plot.py +189 -0
- osiris_utils/cli/validate.py +247 -0
- osiris_utils/data/__init__.py +15 -0
- osiris_utils/data/data.py +41 -171
- osiris_utils/data/diagnostic.py +285 -274
- osiris_utils/data/simulation.py +20 -13
- osiris_utils/decks/__init__.py +4 -0
- osiris_utils/decks/decks.py +83 -8
- osiris_utils/decks/species.py +12 -9
- osiris_utils/postprocessing/__init__.py +28 -0
- osiris_utils/postprocessing/derivative.py +317 -106
- osiris_utils/postprocessing/fft.py +135 -24
- osiris_utils/postprocessing/field_centering.py +28 -14
- osiris_utils/postprocessing/heatflux_correction.py +39 -18
- osiris_utils/postprocessing/mft.py +10 -2
- osiris_utils/postprocessing/postprocess.py +8 -5
- osiris_utils/postprocessing/pressure_correction.py +29 -17
- osiris_utils/utils.py +26 -17
- osiris_utils/vis/__init__.py +3 -0
- osiris_utils/vis/plot3d.py +148 -0
- {osiris_utils-1.1.10a0.dist-info → osiris_utils-1.2.0.dist-info}/METADATA +55 -7
- {osiris_utils-1.1.10a0.dist-info → osiris_utils-1.2.0.dist-info}/RECORD +51 -34
- {osiris_utils-1.1.10a0.dist-info → osiris_utils-1.2.0.dist-info}/WHEEL +1 -1
- osiris_utils-1.2.0.dist-info/entry_points.txt +2 -0
- {osiris_utils-1.1.10a0.dist-info → osiris_utils-1.2.0.dist-info}/top_level.txt +1 -0
- osiris_utils/postprocessing/mft_for_gridfile.py +0 -55
- {osiris_utils-1.1.10a0.dist-info → osiris_utils-1.2.0.dist-info}/licenses/LICENSE.txt +0 -0
osiris_utils/data/diagnostic.py
CHANGED
|
@@ -1,33 +1,35 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
"""
|
|
4
|
-
The utilities on data.py are cool but not useful when you want to work with whole data of a simulation instead
|
|
5
|
-
of just a single file. This is what this file is for - deal with ''folders'' of data.
|
|
6
|
-
|
|
7
|
-
Took some inspiration from Diogo and Madox's work.
|
|
8
|
-
|
|
9
|
-
This would be awsome to compute time derivatives.
|
|
10
|
-
"""
|
|
11
|
-
|
|
12
3
|
import glob
|
|
13
4
|
import logging
|
|
14
5
|
import operator
|
|
15
6
|
import os
|
|
16
7
|
import warnings
|
|
17
|
-
from
|
|
8
|
+
from collections.abc import Callable, Iterator
|
|
9
|
+
from typing import Any, Literal
|
|
18
10
|
|
|
19
11
|
import h5py
|
|
20
|
-
import matplotlib.pyplot as plt
|
|
21
12
|
import numpy as np
|
|
22
13
|
import tqdm
|
|
23
14
|
|
|
15
|
+
from ..decks.decks import InputDeckIO
|
|
16
|
+
from ..decks.species import Species
|
|
24
17
|
from .data import OsirisGridFile
|
|
25
18
|
|
|
26
19
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(name)s %(levelname)8s │ %(message)s")
|
|
27
20
|
logger = logging.getLogger(__name__)
|
|
28
21
|
|
|
22
|
+
"""
|
|
23
|
+
The utilities on data.py are cool but not useful when you want to work with whole data of a simulation instead
|
|
24
|
+
of just a single file. This is what this file is for - deal with ''folders'' of data.
|
|
25
|
+
|
|
26
|
+
Took some inspiration from Diogo and Madox's work.
|
|
27
|
+
|
|
28
|
+
This would be awsome to compute time derivatives.
|
|
29
|
+
"""
|
|
30
|
+
|
|
29
31
|
OSIRIS_DENSITY = ["n"]
|
|
30
|
-
OSIRIS_SPECIE_REPORTS = ["charge", "q1", "q2", "q3", "j1", "j2", "j3"]
|
|
32
|
+
OSIRIS_SPECIE_REPORTS = ["ene", "charge", "q1", "q2", "q3", "j1", "j2", "j3"]
|
|
31
33
|
OSIRIS_SPECIE_REP_UDIST = [
|
|
32
34
|
"vfl1",
|
|
33
35
|
"vfl2",
|
|
@@ -35,6 +37,9 @@ OSIRIS_SPECIE_REP_UDIST = [
|
|
|
35
37
|
"ufl1",
|
|
36
38
|
"ufl2",
|
|
37
39
|
"ufl3",
|
|
40
|
+
"P00",
|
|
41
|
+
"P01",
|
|
42
|
+
"P02",
|
|
38
43
|
"P11",
|
|
39
44
|
"P12",
|
|
40
45
|
"P13",
|
|
@@ -103,6 +108,8 @@ _ATTRS_TO_CLONE = [
|
|
|
103
108
|
"_quantity",
|
|
104
109
|
]
|
|
105
110
|
|
|
111
|
+
__all__ = ["Diagnostic", "which_quantities"]
|
|
112
|
+
|
|
106
113
|
|
|
107
114
|
def which_quantities():
|
|
108
115
|
print("Available quantities:")
|
|
@@ -110,8 +117,8 @@ def which_quantities():
|
|
|
110
117
|
|
|
111
118
|
|
|
112
119
|
class Diagnostic:
|
|
113
|
-
"""
|
|
114
|
-
|
|
120
|
+
"""Class to handle diagnostics. This is the "base" class of the code. Diagnostics can be loaded from OSIRIS output files,
|
|
121
|
+
but are also created when performing operations with other diagnostics.
|
|
115
122
|
Post-processed quantities are also considered diagnostics. This way, we can perform operations with them as well.
|
|
116
123
|
|
|
117
124
|
Parameters
|
|
@@ -120,6 +127,10 @@ class Diagnostic:
|
|
|
120
127
|
The species to handle the diagnostics.
|
|
121
128
|
simulation_folder : str
|
|
122
129
|
The path to the simulation folder. This is the path to the folder where the input deck is located.
|
|
130
|
+
input_deck : str or dict, optional
|
|
131
|
+
The input deck to load the diagnostic attributes. If None, the attributes are loaded from the files.
|
|
132
|
+
If a string is provided, it is assumed to be the path to the input deck file.
|
|
133
|
+
If a dict is provided, it is assumed to be the parsed input deck.
|
|
123
134
|
|
|
124
135
|
Attributes
|
|
125
136
|
----------
|
|
@@ -136,13 +147,17 @@ class Diagnostic:
|
|
|
136
147
|
grid : np.ndarray
|
|
137
148
|
The grid boundaries.
|
|
138
149
|
axis : dict
|
|
139
|
-
The axis information. Each key is a direction and the value is a dictionary with the keys "name",
|
|
150
|
+
The axis information. Each key is a direction and the value is a dictionary with the keys "name",
|
|
151
|
+
"long_name", "units" and "plot_label".
|
|
140
152
|
units : str
|
|
141
|
-
The units of the diagnostic. This info may not be available for all diagnostics, ie,
|
|
153
|
+
The units of the diagnostic. This info may not be available for all diagnostics, ie,
|
|
154
|
+
diagnostics resulting from operations and postprocessing.
|
|
142
155
|
name : str
|
|
143
|
-
The name of the diagnostic. This info may not be available for all diagnostics, ie,
|
|
156
|
+
The name of the diagnostic. This info may not be available for all diagnostics, ie,
|
|
157
|
+
diagnostics resulting from operations and postprocessing.
|
|
144
158
|
label : str
|
|
145
|
-
The label of the diagnostic. This info may not be available for all diagnostics, ie,
|
|
159
|
+
The label of the diagnostic. This info may not be available for all diagnostics, ie,
|
|
160
|
+
diagnostics resulting from operations and postprocessing.
|
|
146
161
|
dim : int
|
|
147
162
|
The dimension of the diagnostic.
|
|
148
163
|
ndump : int
|
|
@@ -189,22 +204,27 @@ class Diagnostic:
|
|
|
189
204
|
|
|
190
205
|
"""
|
|
191
206
|
|
|
192
|
-
def __init__(
|
|
207
|
+
def __init__(
|
|
208
|
+
self,
|
|
209
|
+
simulation_folder: str | None = None,
|
|
210
|
+
species: Species = None,
|
|
211
|
+
input_deck: InputDeckIO | None = None,
|
|
212
|
+
) -> None:
|
|
193
213
|
self._species = species if species else None
|
|
194
214
|
|
|
195
|
-
self._dx:
|
|
196
|
-
self._nx:
|
|
197
|
-
self._x:
|
|
198
|
-
self._dt:
|
|
199
|
-
self._grid:
|
|
200
|
-
self._axis:
|
|
201
|
-
self._units:
|
|
202
|
-
self._name:
|
|
203
|
-
self._label:
|
|
204
|
-
self._dim:
|
|
205
|
-
self._ndump:
|
|
206
|
-
self._maxiter:
|
|
207
|
-
self._tunits:
|
|
215
|
+
self._dx: float | np.ndarray | None = None # grid spacing in each direction
|
|
216
|
+
self._nx: int | np.ndarray | None = None # number of grid points in each direction
|
|
217
|
+
self._x: np.ndarray | None = None # grid points
|
|
218
|
+
self._dt: float | None = None # time step
|
|
219
|
+
self._grid: np.ndarray | None = None # grid boundaries
|
|
220
|
+
self._axis: Any | None = None # axis information
|
|
221
|
+
self._units: str | None = None # units of the diagnostic
|
|
222
|
+
self._name: str | None = None
|
|
223
|
+
self._label: str | None = None
|
|
224
|
+
self._dim: int | None = None
|
|
225
|
+
self._ndump: int | None = None
|
|
226
|
+
self._maxiter: int | None = None
|
|
227
|
+
self._tunits: str | None = None # time units
|
|
208
228
|
|
|
209
229
|
if simulation_folder:
|
|
210
230
|
self._simulation_folder = simulation_folder
|
|
@@ -219,7 +239,7 @@ class Diagnostic:
|
|
|
219
239
|
self._input_deck = None
|
|
220
240
|
|
|
221
241
|
self._all_loaded: bool = False # if the data is already loaded into memory
|
|
222
|
-
self._quantity:
|
|
242
|
+
self._quantity: str | None = None
|
|
223
243
|
|
|
224
244
|
#########################################
|
|
225
245
|
#
|
|
@@ -228,13 +248,13 @@ class Diagnostic:
|
|
|
228
248
|
#########################################
|
|
229
249
|
|
|
230
250
|
def get_quantity(self, quantity: str) -> None:
|
|
231
|
-
"""
|
|
232
|
-
Get the data for a given quantity.
|
|
251
|
+
"""Get the data for a given quantity.
|
|
233
252
|
|
|
234
253
|
Parameters
|
|
235
254
|
----------
|
|
236
255
|
quantity : str
|
|
237
256
|
The quantity to get the data.
|
|
257
|
+
|
|
238
258
|
"""
|
|
239
259
|
self._quantity = quantity
|
|
240
260
|
|
|
@@ -260,11 +280,18 @@ class Diagnostic:
|
|
|
260
280
|
self._get_density(self._species.name, "charge")
|
|
261
281
|
else:
|
|
262
282
|
raise ValueError(
|
|
263
|
-
f"Invalid quantity {self._quantity}. Or it's not implemented yet (this may happen for phase space quantities)."
|
|
283
|
+
f"Invalid quantity {self._quantity}. Or it's not implemented yet (this may happen for phase space quantities).",
|
|
264
284
|
)
|
|
265
285
|
|
|
266
286
|
def _scan_files(self, pattern: str) -> None:
|
|
267
|
-
"""Populate _file_list and related attributes from a glob pattern.
|
|
287
|
+
"""Populate _file_list and related attributes from a glob pattern.
|
|
288
|
+
|
|
289
|
+
Parameters
|
|
290
|
+
----------
|
|
291
|
+
pattern : str
|
|
292
|
+
The glob pattern to search for HDF5 files.
|
|
293
|
+
|
|
294
|
+
"""
|
|
268
295
|
self._file_list = sorted(glob.glob(pattern))
|
|
269
296
|
if not self._file_list:
|
|
270
297
|
raise FileNotFoundError(f"No HDF5 files match {pattern}")
|
|
@@ -272,6 +299,16 @@ class Diagnostic:
|
|
|
272
299
|
self._maxiter = len(self._file_list)
|
|
273
300
|
|
|
274
301
|
def _get_moment(self, species: str, moment: str) -> None:
|
|
302
|
+
"""Get the moment data for a given species and moment.
|
|
303
|
+
|
|
304
|
+
Parameters
|
|
305
|
+
----------
|
|
306
|
+
species : str
|
|
307
|
+
The species to get the moment data.
|
|
308
|
+
moment : str
|
|
309
|
+
The moment to get the data.
|
|
310
|
+
|
|
311
|
+
"""
|
|
275
312
|
if self._simulation_folder is None:
|
|
276
313
|
raise ValueError("Simulation folder not set. If you're using CustomDiagnostic, this method is not available.")
|
|
277
314
|
self._path = f"{self._simulation_folder}/MS/UDIST/{species}/{moment}/"
|
|
@@ -279,6 +316,14 @@ class Diagnostic:
|
|
|
279
316
|
self._load_attributes(self._file_template, self._input_deck)
|
|
280
317
|
|
|
281
318
|
def _get_field(self, field: str) -> None:
|
|
319
|
+
"""Get the field data for a given field.
|
|
320
|
+
|
|
321
|
+
Parameters
|
|
322
|
+
----------
|
|
323
|
+
field : str
|
|
324
|
+
The field to get the data.
|
|
325
|
+
|
|
326
|
+
"""
|
|
282
327
|
if self._simulation_folder is None:
|
|
283
328
|
raise ValueError("Simulation folder not set. If you're using CustomDiagnostic, this method is not available.")
|
|
284
329
|
self._path = f"{self._simulation_folder}/MS/FLD/{field}/"
|
|
@@ -286,6 +331,16 @@ class Diagnostic:
|
|
|
286
331
|
self._load_attributes(self._file_template, self._input_deck)
|
|
287
332
|
|
|
288
333
|
def _get_density(self, species: str, quantity: str) -> None:
|
|
334
|
+
"""Get the density data for a given species and quantity.
|
|
335
|
+
|
|
336
|
+
Parameters
|
|
337
|
+
----------
|
|
338
|
+
species : str
|
|
339
|
+
The species to get the density data.
|
|
340
|
+
quantity : str
|
|
341
|
+
The quantity to get the data.
|
|
342
|
+
|
|
343
|
+
"""
|
|
289
344
|
if self._simulation_folder is None:
|
|
290
345
|
raise ValueError("Simulation folder not set. If you're using CustomDiagnostic, this method is not available.")
|
|
291
346
|
self._path = f"{self._simulation_folder}/MS/DENSITY/{species}/{quantity}/"
|
|
@@ -293,13 +348,35 @@ class Diagnostic:
|
|
|
293
348
|
self._load_attributes(self._file_template, self._input_deck)
|
|
294
349
|
|
|
295
350
|
def _get_phase_space(self, species: str, type: str) -> None:
|
|
351
|
+
"""Get the phase space data for a given species and type.
|
|
352
|
+
|
|
353
|
+
Parameters
|
|
354
|
+
----------
|
|
355
|
+
species : str
|
|
356
|
+
The species to get the phase space data.
|
|
357
|
+
type : str
|
|
358
|
+
The type of phase space to get the data.
|
|
359
|
+
|
|
360
|
+
"""
|
|
296
361
|
if self._simulation_folder is None:
|
|
297
362
|
raise ValueError("Simulation folder not set. If you're using CustomDiagnostic, this method is not available.")
|
|
298
363
|
self._path = f"{self._simulation_folder}/MS/PHA/{type}/{species}/"
|
|
299
364
|
self._scan_files(os.path.join(self._path, "*.h5"))
|
|
300
365
|
self._load_attributes(self._file_template, self._input_deck)
|
|
301
366
|
|
|
302
|
-
def _load_attributes(self, file_template: str, input_deck:
|
|
367
|
+
def _load_attributes(self, file_template: str, input_deck: dict | None) -> None: # this will be replaced by reading the input deck
|
|
368
|
+
"""Load diagnostic attributes from the first available file or input deck.
|
|
369
|
+
|
|
370
|
+
Parameters
|
|
371
|
+
----------
|
|
372
|
+
file_template : str
|
|
373
|
+
The file template to load the attributes from.
|
|
374
|
+
This is the path to the file without the iteration number and extension.
|
|
375
|
+
(e.g., /path/to/diagnostic/000001.h5 -> /path/to/diagnostic/)
|
|
376
|
+
input_deck : dict, optional
|
|
377
|
+
The input deck to load the diagnostic attributes. If None, the attributes are loaded from the files.
|
|
378
|
+
|
|
379
|
+
"""
|
|
303
380
|
# This can go wrong! NDUMP
|
|
304
381
|
# if input_deck is not None:
|
|
305
382
|
# self._dt = float(input_deck["time_step"][0]["dt"])
|
|
@@ -339,9 +416,9 @@ class Diagnostic:
|
|
|
339
416
|
break
|
|
340
417
|
|
|
341
418
|
if not found_file:
|
|
342
|
-
warnings.warn(f"No valid data files found in {self._path} to read metadata from.")
|
|
419
|
+
warnings.warn(f"No valid data files found in {self._path} to read metadata from.", stacklevel=2)
|
|
343
420
|
except Exception as e:
|
|
344
|
-
warnings.warn(f"Error loading diagnostic attributes: {
|
|
421
|
+
warnings.warn(f"Error loading diagnostic attributes: {e!s}. Please verify it there's any file in the folder.", stacklevel=2)
|
|
345
422
|
|
|
346
423
|
##########################################
|
|
347
424
|
#
|
|
@@ -349,26 +426,53 @@ class Diagnostic:
|
|
|
349
426
|
#
|
|
350
427
|
##########################################
|
|
351
428
|
|
|
352
|
-
def _data_generator(self, index: int) ->
|
|
429
|
+
def _data_generator(self, index: int, data_slice: tuple | None = None) -> Iterator[np.ndarray]:
|
|
430
|
+
"""Data generator for a given index or slice.
|
|
431
|
+
|
|
432
|
+
Parameters
|
|
433
|
+
----------
|
|
434
|
+
index : int
|
|
435
|
+
The index of the file to load.
|
|
436
|
+
data_slice : tuple, optional
|
|
437
|
+
The slice to apply to the data. This is a tuple of slices, one for each dimension.
|
|
438
|
+
|
|
439
|
+
Returns
|
|
440
|
+
-------
|
|
441
|
+
Iterator[np.ndarray]
|
|
442
|
+
An iterator that yields the data for the given index or slice.
|
|
443
|
+
|
|
444
|
+
"""
|
|
353
445
|
if self._simulation_folder is None:
|
|
354
446
|
raise ValueError("Simulation folder not set.")
|
|
355
447
|
if self._file_list is None:
|
|
356
448
|
raise RuntimeError("File list not initialized. Call get_quantity() first.")
|
|
357
449
|
try:
|
|
358
450
|
file = self._file_list[index]
|
|
359
|
-
except IndexError:
|
|
360
|
-
raise RuntimeError(
|
|
361
|
-
|
|
451
|
+
except IndexError as err:
|
|
452
|
+
raise RuntimeError(
|
|
453
|
+
f"File index {index} out of range (max {self._maxiter - 1}).",
|
|
454
|
+
) from err
|
|
455
|
+
# Pass data_slice to OsirisGridFile - HDF5 will efficiently read only the requested slice from disk
|
|
456
|
+
data_object = OsirisGridFile(file, data_slice=data_slice)
|
|
362
457
|
yield (data_object.data if self._quantity not in OSIRIS_DENSITY else np.sign(self._species.rqm) * data_object.data)
|
|
363
458
|
|
|
364
|
-
def load_all(self) -> np.ndarray:
|
|
365
|
-
"""
|
|
366
|
-
|
|
459
|
+
def load_all(self, n_workers: int | None = None, use_parallel: bool | None = None) -> np.ndarray:
|
|
460
|
+
"""Load all data into memory (all iterations), in a pre-allocated array.
|
|
461
|
+
|
|
462
|
+
Parameters
|
|
463
|
+
----------
|
|
464
|
+
n_workers : int, optional
|
|
465
|
+
Number of parallel workers for loading data. If None, uses CPU count.
|
|
466
|
+
Only used if use_parallel=True.
|
|
467
|
+
use_parallel : bool, optional
|
|
468
|
+
If True, force parallel loading. If False, force sequential.
|
|
469
|
+
If None (default), automatically choose based on data size.
|
|
367
470
|
|
|
368
471
|
Returns
|
|
369
472
|
-------
|
|
370
473
|
data : np.ndarray
|
|
371
474
|
The data for all iterations. Also stored in self._data.
|
|
475
|
+
|
|
372
476
|
"""
|
|
373
477
|
if getattr(self, "_all_loaded", False) and self._data is not None:
|
|
374
478
|
logger.debug("Data already loaded into memory.")
|
|
@@ -381,27 +485,58 @@ class Diagnostic:
|
|
|
381
485
|
try:
|
|
382
486
|
first = self[0]
|
|
383
487
|
except Exception as e:
|
|
384
|
-
raise RuntimeError(
|
|
488
|
+
raise RuntimeError("Failed to load first timestep") from e
|
|
385
489
|
slice_shape = first.shape
|
|
386
490
|
dtype = first.dtype
|
|
387
491
|
|
|
388
492
|
data = np.empty((size, *slice_shape), dtype=dtype)
|
|
389
493
|
data[0] = first
|
|
390
494
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
495
|
+
# Auto-detect whether to use parallel loading
|
|
496
|
+
if use_parallel is None:
|
|
497
|
+
# Use parallel for large datasets: >10 timesteps AND >1MB per file
|
|
498
|
+
bytes_per_timestep = first.nbytes
|
|
499
|
+
use_parallel = (size > 10) and (bytes_per_timestep > 1_000_000)
|
|
500
|
+
|
|
501
|
+
# Parallel loading for significant performance improvement
|
|
502
|
+
if use_parallel and size > 1:
|
|
503
|
+
import concurrent.futures
|
|
504
|
+
import os
|
|
505
|
+
|
|
506
|
+
if n_workers is None:
|
|
507
|
+
n_workers = min(os.cpu_count() or 4, size - 1)
|
|
508
|
+
|
|
509
|
+
def load_single(i):
|
|
510
|
+
"""Helper to load a single timestep"""
|
|
511
|
+
try:
|
|
512
|
+
return i, self[i]
|
|
513
|
+
except Exception as e:
|
|
514
|
+
raise RuntimeError(f"Error loading timestep {i}") from e
|
|
515
|
+
|
|
516
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=n_workers) as executor:
|
|
517
|
+
# Submit all loading tasks
|
|
518
|
+
futures = {executor.submit(load_single, i): i for i in range(1, size)}
|
|
519
|
+
|
|
520
|
+
# Collect results with progress bar
|
|
521
|
+
with tqdm.tqdm(total=size - 1, desc="Loading data (parallel)") as pbar:
|
|
522
|
+
for future in concurrent.futures.as_completed(futures):
|
|
523
|
+
i, result = future.result()
|
|
524
|
+
data[i] = result
|
|
525
|
+
pbar.update(1)
|
|
526
|
+
else:
|
|
527
|
+
# Sequential loading (fallback or for small files)
|
|
528
|
+
for i in tqdm.trange(1, size, desc="Loading data"):
|
|
529
|
+
try:
|
|
530
|
+
data[i] = self[i]
|
|
531
|
+
except Exception as e:
|
|
532
|
+
raise RuntimeError(f"Error loading timestep {i}") from e
|
|
396
533
|
|
|
397
534
|
self._data = data
|
|
398
535
|
self._all_loaded = True
|
|
399
536
|
return self._data
|
|
400
537
|
|
|
401
538
|
def unload(self) -> None:
|
|
402
|
-
"""
|
|
403
|
-
Unload data from memory. This is useful to free memory when the data is not needed anymore.
|
|
404
|
-
"""
|
|
539
|
+
"""Unload data from memory. This is useful to free memory when the data is not needed anymore."""
|
|
405
540
|
logger.info("Unloading data from memory.")
|
|
406
541
|
if self._all_loaded is False:
|
|
407
542
|
logger.warning("Data is not loaded.")
|
|
@@ -419,20 +554,22 @@ class Diagnostic:
|
|
|
419
554
|
"""Return the number of timesteps available."""
|
|
420
555
|
return getattr(self, "_maxiter", 0)
|
|
421
556
|
|
|
422
|
-
def __getitem__(self, index: int) -> np.ndarray:
|
|
423
|
-
"""
|
|
424
|
-
Retrieve timestep data.
|
|
557
|
+
def __getitem__(self, index: int | slice | tuple) -> np.ndarray:
|
|
558
|
+
"""Retrieve timestep data with optional spatial slicing.
|
|
425
559
|
|
|
426
560
|
Parameters
|
|
427
561
|
----------
|
|
428
|
-
index : int or
|
|
429
|
-
- If int,
|
|
430
|
-
- If slice, supports start:stop:step. Zero-length slices return an empty array of shape (0, ...).
|
|
562
|
+
index : int, slice, or tuple
|
|
563
|
+
- If int, loads full spatial data for that timestep.
|
|
564
|
+
- If slice, supports start:stop:step for time. Zero-length slices return an empty array of shape (0, ...).
|
|
565
|
+
- If tuple, first element is time index/slice, remaining elements are spatial slices for each dimension.
|
|
566
|
+
Example: diag[5, :, 100:200] loads timestep 5 with spatial slicing applied.
|
|
567
|
+
**Efficient**: HDF5 reads only the requested spatial slice from disk, not the full array.
|
|
431
568
|
|
|
432
569
|
Returns
|
|
433
570
|
-------
|
|
434
571
|
np.ndarray
|
|
435
|
-
Array for that timestep (or stacked array for a slice).
|
|
572
|
+
Array for that timestep (or stacked array for a slice), optionally spatially sliced.
|
|
436
573
|
|
|
437
574
|
Raises
|
|
438
575
|
------
|
|
@@ -440,33 +577,59 @@ class Diagnostic:
|
|
|
440
577
|
If the index is out of range or no data generator is available.
|
|
441
578
|
RuntimeError
|
|
442
579
|
If loading a specific timestep fails.
|
|
580
|
+
|
|
581
|
+
Examples
|
|
582
|
+
--------
|
|
583
|
+
>>> data = diag[5] # Load full spatial data for timestep 5
|
|
584
|
+
>>> data = diag[5, :, 100:200] # Load timestep 5 with spatial slice (only reads slice from disk!)
|
|
585
|
+
>>> data = diag[0:10] # Load timesteps 0-9 (full spatial domain)
|
|
586
|
+
>>> data = diag[0:10, :, 50:] # Load timesteps 0-9 with spatial slice
|
|
587
|
+
|
|
443
588
|
"""
|
|
589
|
+
# Parse tuple indexing: (time_index, spatial_slice1, spatial_slice2, ...)
|
|
590
|
+
data_slice = None
|
|
591
|
+
if isinstance(index, tuple):
|
|
592
|
+
if len(index) == 0:
|
|
593
|
+
raise IndexError("Empty tuple index not supported")
|
|
594
|
+
time_index = index[0]
|
|
595
|
+
# Remaining indices are for spatial dimensions
|
|
596
|
+
if len(index) > 1:
|
|
597
|
+
data_slice = index[1:]
|
|
598
|
+
else:
|
|
599
|
+
time_index = index
|
|
600
|
+
|
|
444
601
|
# Quick path: all data already in memory
|
|
445
602
|
if getattr(self, "_all_loaded", False) and self._data is not None:
|
|
446
|
-
|
|
603
|
+
if data_slice is not None:
|
|
604
|
+
# Apply spatial slicing to loaded data
|
|
605
|
+
return self._data[(time_index,) + data_slice]
|
|
606
|
+
return self._data[time_index]
|
|
447
607
|
|
|
448
608
|
# Data generator must exist
|
|
449
609
|
data_gen = getattr(self, "_data_generator", None)
|
|
450
610
|
if not callable(data_gen):
|
|
451
|
-
raise IndexError(
|
|
611
|
+
raise IndexError("No data available for indexing; you did something wrong!")
|
|
452
612
|
|
|
453
613
|
# Handle int indices (including negatives)
|
|
454
|
-
if isinstance(
|
|
455
|
-
if
|
|
456
|
-
|
|
457
|
-
if not (0 <=
|
|
458
|
-
raise IndexError(f"Index {
|
|
614
|
+
if isinstance(time_index, int):
|
|
615
|
+
if time_index < 0:
|
|
616
|
+
time_index += self._maxiter
|
|
617
|
+
if not (0 <= time_index < self._maxiter):
|
|
618
|
+
raise IndexError(f"Index {time_index} out of range (0..{self._maxiter - 1})")
|
|
619
|
+
|
|
620
|
+
# Load data immediately with optional spatial slicing
|
|
621
|
+
# HDF5 will read only the requested slice from disk (efficient!)
|
|
459
622
|
try:
|
|
460
|
-
gen = data_gen(
|
|
623
|
+
gen = data_gen(time_index, data_slice=data_slice)
|
|
461
624
|
return next(gen)
|
|
462
625
|
except Exception as e:
|
|
463
|
-
raise RuntimeError(f"Error loading data at index {
|
|
626
|
+
raise RuntimeError(f"Error loading data at index {time_index}") from e
|
|
464
627
|
|
|
465
628
|
# Handle slice indices
|
|
466
|
-
if isinstance(
|
|
467
|
-
start =
|
|
468
|
-
stop =
|
|
469
|
-
step =
|
|
629
|
+
if isinstance(time_index, slice):
|
|
630
|
+
start = time_index.start or 0
|
|
631
|
+
stop = time_index.stop if time_index.stop is not None else self._maxiter
|
|
632
|
+
step = time_index.step or 1
|
|
470
633
|
# Normalize negatives
|
|
471
634
|
if start < 0:
|
|
472
635
|
start += self._maxiter
|
|
@@ -481,7 +644,7 @@ class Diagnostic:
|
|
|
481
644
|
if not indices:
|
|
482
645
|
# Determine single-step shape by peeking at index 0 (if possible)
|
|
483
646
|
try:
|
|
484
|
-
dummy = next(data_gen(0))
|
|
647
|
+
dummy = next(data_gen(0, data_slice=data_slice))
|
|
485
648
|
empty_shape = (0,) + dummy.shape
|
|
486
649
|
return np.empty(empty_shape, dtype=dummy.dtype)
|
|
487
650
|
except Exception:
|
|
@@ -491,46 +654,16 @@ class Diagnostic:
|
|
|
491
654
|
data_list = []
|
|
492
655
|
for i in indices:
|
|
493
656
|
try:
|
|
494
|
-
data_list.append(next(data_gen(i)))
|
|
657
|
+
data_list.append(next(data_gen(i, data_slice=data_slice)))
|
|
495
658
|
except Exception as e:
|
|
496
|
-
raise RuntimeError(f"Error loading
|
|
497
|
-
return np.stack(data_list)
|
|
498
|
-
|
|
499
|
-
# Unsupported index type
|
|
500
|
-
raise IndexError(f"Invalid index type {type(index)}; must be int or slice")
|
|
501
|
-
|
|
502
|
-
def __iter__(self) -> Iterator[np.ndarray]:
|
|
503
|
-
# If this is a file-based diagnostic
|
|
504
|
-
if self._simulation_folder is not None:
|
|
505
|
-
for i in range(len(sorted(glob.glob(f"{self._path}/*.h5")))):
|
|
506
|
-
yield next(self._data_generator(i))
|
|
507
|
-
|
|
508
|
-
# If this is a derived diagnostic and data is already loaded
|
|
509
|
-
elif self._all_loaded and self._data is not None:
|
|
510
|
-
for i in range(self._data.shape[0]):
|
|
511
|
-
yield self._data[i]
|
|
512
|
-
|
|
513
|
-
# If this is a derived diagnostic with custom generator but no loaded data
|
|
514
|
-
elif hasattr(self, "_data_generator") and callable(self._data_generator):
|
|
515
|
-
# Determine how many iterations to go through
|
|
516
|
-
max_iter = self._maxiter
|
|
517
|
-
if max_iter is None:
|
|
518
|
-
if hasattr(self, "_diag") and hasattr(self._diag, "_maxiter"):
|
|
519
|
-
max_iter = self._diag._maxiter
|
|
520
|
-
else:
|
|
521
|
-
max_iter = 100 # Default if we can't determine
|
|
522
|
-
logger.warning(f"Could not determine iteration count for iteration, using {max_iter}.")
|
|
523
|
-
|
|
524
|
-
for i in range(max_iter):
|
|
525
|
-
yield next(self._data_generator(i))
|
|
659
|
+
raise RuntimeError(f"Error loading timestep {i}") from e
|
|
660
|
+
return np.stack(data_list, axis=0)
|
|
526
661
|
|
|
527
662
|
# If we don't know how to handle this
|
|
528
|
-
|
|
529
|
-
raise ValueError("Cannot iterate over this diagnostic. No data loaded and no generator available.")
|
|
663
|
+
raise ValueError("Cannot index this diagnostic. No data loaded and no generator available.")
|
|
530
664
|
|
|
531
665
|
def _clone_meta(self) -> Diagnostic:
|
|
532
|
-
"""
|
|
533
|
-
Create a new Diagnostic instance that carries over metadata only.
|
|
666
|
+
"""Create a new Diagnostic instance that carries over metadata only.
|
|
534
667
|
No data is copied, and no constructor edits are required because we
|
|
535
668
|
assign attributes dynamically.
|
|
536
669
|
"""
|
|
@@ -544,9 +677,8 @@ class Diagnostic:
|
|
|
544
677
|
clone._file_list = self._file_list
|
|
545
678
|
return clone
|
|
546
679
|
|
|
547
|
-
def _binary_op(self, other:
|
|
548
|
-
"""
|
|
549
|
-
Universal helper for `self (op) other`.
|
|
680
|
+
def _binary_op(self, other: Diagnostic | float | np.ndarray, op_func: Callable) -> Diagnostic:
|
|
681
|
+
"""Universal helper for `self (op) other`.
|
|
550
682
|
- If both operands are fully loaded, does eager numpy arithmetic.
|
|
551
683
|
- Otherwise builds a lazy generator that applies op_func on each timestep.
|
|
552
684
|
"""
|
|
@@ -583,9 +715,8 @@ class Diagnostic:
|
|
|
583
715
|
seq1 = gen1(idx)
|
|
584
716
|
if callable(gen2):
|
|
585
717
|
seq2 = gen2(idx)
|
|
586
|
-
return (op_func(a, b) for a, b in zip(seq1, seq2))
|
|
587
|
-
|
|
588
|
-
return (op_func(a, gen2) for a in seq1)
|
|
718
|
+
return (op_func(a, b) for a, b in zip(seq1, seq2, strict=False))
|
|
719
|
+
return (op_func(a, gen2) for a in seq1)
|
|
589
720
|
|
|
590
721
|
result._data_generator = _make_gen
|
|
591
722
|
result._all_loaded = False
|
|
@@ -593,38 +724,37 @@ class Diagnostic:
|
|
|
593
724
|
|
|
594
725
|
# Now define each operator in one line:
|
|
595
726
|
|
|
596
|
-
def __add__(self, other:
|
|
727
|
+
def __add__(self, other: Diagnostic | float | np.ndarray) -> Diagnostic:
|
|
597
728
|
return self._binary_op(other, operator.add)
|
|
598
729
|
|
|
599
|
-
def __radd__(self, other:
|
|
730
|
+
def __radd__(self, other: Diagnostic | float | np.ndarray) -> Diagnostic:
|
|
600
731
|
return self + other
|
|
601
732
|
|
|
602
|
-
def __sub__(self, other:
|
|
733
|
+
def __sub__(self, other: Diagnostic | float | np.ndarray) -> Diagnostic:
|
|
603
734
|
return self._binary_op(other, operator.sub)
|
|
604
735
|
|
|
605
|
-
def __rsub__(self, other:
|
|
736
|
+
def __rsub__(self, other: Diagnostic | float | np.ndarray) -> Diagnostic:
|
|
606
737
|
# swap args for reversed subtraction
|
|
607
738
|
return self._binary_op(other, lambda x, y: operator.sub(y, x))
|
|
608
739
|
|
|
609
|
-
def __mul__(self, other:
|
|
740
|
+
def __mul__(self, other: Diagnostic | float | np.ndarray) -> Diagnostic:
|
|
610
741
|
return self._binary_op(other, operator.mul)
|
|
611
742
|
|
|
612
|
-
def __rmul__(self, other:
|
|
743
|
+
def __rmul__(self, other: Diagnostic | float | np.ndarray) -> Diagnostic:
|
|
613
744
|
return self * other
|
|
614
745
|
|
|
615
|
-
def __truediv__(self, other:
|
|
746
|
+
def __truediv__(self, other: Diagnostic | float | np.ndarray) -> Diagnostic:
|
|
616
747
|
return self._binary_op(other, operator.truediv)
|
|
617
748
|
|
|
618
|
-
def __rtruediv__(self, other:
|
|
749
|
+
def __rtruediv__(self, other: Diagnostic | float | np.ndarray) -> Diagnostic:
|
|
619
750
|
return self._binary_op(other, lambda x, y: operator.truediv(y, x))
|
|
620
751
|
|
|
621
752
|
def __neg__(self) -> Diagnostic:
|
|
622
753
|
# unary minus as multiplication by -1
|
|
623
754
|
return self._binary_op(-1, operator.mul)
|
|
624
755
|
|
|
625
|
-
def __pow__(self, other:
|
|
626
|
-
"""
|
|
627
|
-
Power operation. Raises the diagnostic data to the power of `other`.
|
|
756
|
+
def __pow__(self, other: Diagnostic | float | np.ndarray) -> Diagnostic:
|
|
757
|
+
"""Power operation. Raises the diagnostic data to the power of `other`.
|
|
628
758
|
If `other` is a Diagnostic, it raises each timestep's data to the corresponding timestep's power.
|
|
629
759
|
If `other` is a scalar or ndarray, it raises all data to that power.
|
|
630
760
|
"""
|
|
@@ -632,14 +762,13 @@ class Diagnostic:
|
|
|
632
762
|
|
|
633
763
|
def to_h5(
|
|
634
764
|
self,
|
|
635
|
-
savename:
|
|
636
|
-
index:
|
|
765
|
+
savename: str | None = None,
|
|
766
|
+
index: int | list[int] | None = None,
|
|
637
767
|
all: bool = False,
|
|
638
768
|
verbose: bool = False,
|
|
639
|
-
path:
|
|
769
|
+
path: str | None = None,
|
|
640
770
|
) -> None:
|
|
641
|
-
"""
|
|
642
|
-
Save the diagnostic data to HDF5 files.
|
|
771
|
+
"""Save the diagnostic data to HDF5 files.
|
|
643
772
|
|
|
644
773
|
Parameters
|
|
645
774
|
----------
|
|
@@ -653,6 +782,7 @@ class Diagnostic:
|
|
|
653
782
|
If True, print messages about the saving process.
|
|
654
783
|
path : str, optional
|
|
655
784
|
The path to save the HDF5 files. If None, uses the default save path (in simulation folder).
|
|
785
|
+
|
|
656
786
|
"""
|
|
657
787
|
if path is None:
|
|
658
788
|
path = self._simulation_folder
|
|
@@ -724,15 +854,13 @@ class Diagnostic:
|
|
|
724
854
|
axis_dataset.attrs.create("NAME", [np.bytes_(axis_shortnames[i].encode())])
|
|
725
855
|
axis_dataset.attrs.create("UNITS", [np.bytes_(axis_units[i].encode())])
|
|
726
856
|
axis_dataset.attrs.create("LONG_NAME", [np.bytes_(axis_longnames[i].encode())])
|
|
727
|
-
axis_dataset.attrs.create("TYPE", [np.bytes_("linear"
|
|
857
|
+
axis_dataset.attrs.create("TYPE", [np.bytes_(b"linear")])
|
|
728
858
|
|
|
729
859
|
if verbose:
|
|
730
860
|
logger.info(f"File created: {filename}")
|
|
731
861
|
|
|
732
|
-
logger.info(
|
|
733
|
-
|
|
734
|
-
)
|
|
735
|
-
|
|
862
|
+
logger.info(f"The savename of the diagnostic is {savename}.")
|
|
863
|
+
logger.info(f"Files will be saved as {savename}-000001.h5, {savename}-000002.h5, etc.")
|
|
736
864
|
logger.info("If you desire a different name, please set it with the 'name' method (setter).")
|
|
737
865
|
|
|
738
866
|
if self._name is None:
|
|
@@ -764,138 +892,18 @@ class Diagnostic:
|
|
|
764
892
|
scale_type: Literal["zero_centered", "pos", "neg", "default"] = "default",
|
|
765
893
|
boundaries: np.ndarray = None,
|
|
766
894
|
):
|
|
767
|
-
"""
|
|
768
|
-
*****************************************************************************************************
|
|
769
|
-
THIS SHOULD BE REMOVED FROM THE BASE CLASS AND MOVED TO A SEPARATED CLASS DESIGNATED FOR THIS PURPOSE
|
|
770
|
-
*****************************************************************************************************
|
|
895
|
+
"""**DEPRECATED**: Use `osiris_utils.vis.plot_3d` instead.
|
|
771
896
|
|
|
772
897
|
Plots a 3D scatter plot of the diagnostic data (grid data).
|
|
773
|
-
|
|
774
|
-
Parameters
|
|
775
|
-
----------
|
|
776
|
-
idx : int
|
|
777
|
-
Index of the data to plot.
|
|
778
|
-
scale_type : Literal["zero_centered", "pos", "neg", "default"], optional
|
|
779
|
-
Type of scaling for the colormap:
|
|
780
|
-
- "zero_centered": Center colormap around zero.
|
|
781
|
-
- "pos": Colormap for positive values.
|
|
782
|
-
- "neg": Colormap for negative values.
|
|
783
|
-
- "default": Standard colormap.
|
|
784
|
-
boundaries : np.ndarray, optional
|
|
785
|
-
Boundaries to plot part of the data. (3,2) If None, uses the default grid boundaries.
|
|
786
|
-
|
|
787
|
-
Returns
|
|
788
|
-
-------
|
|
789
|
-
fig : matplotlib.figure.Figure
|
|
790
|
-
The figure object containing the plot.
|
|
791
|
-
ax : matplotlib.axes._subplots.Axes3DSubplot
|
|
792
|
-
The 3D axes object of the plot.
|
|
793
|
-
|
|
794
|
-
Example
|
|
795
|
-
-------
|
|
796
|
-
sim = ou.Simulation("electrons", "path/to/simulation")
|
|
797
|
-
fig, ax = sim["b3"].plot_3d(55, scale_type="zero_centered", boundaries= [[0, 40], [0, 40], [0, 20]])
|
|
798
|
-
plt.show()
|
|
799
898
|
"""
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
if boundaries is None:
|
|
805
|
-
boundaries = self._grid
|
|
806
|
-
|
|
807
|
-
if not isinstance(boundaries, np.ndarray):
|
|
808
|
-
try:
|
|
809
|
-
boundaries = np.array(boundaries)
|
|
810
|
-
except Exception:
|
|
811
|
-
boundaries = self._grid
|
|
812
|
-
warnings.warn("boundaries cannot be accessed as a numpy array with shape (3, 2), using default instead")
|
|
813
|
-
|
|
814
|
-
if boundaries.shape != (3, 2):
|
|
815
|
-
warnings.warn("boundaries should have shape (3, 2), using default instead")
|
|
816
|
-
boundaries = self._grid
|
|
817
|
-
|
|
818
|
-
# Load data
|
|
819
|
-
if self._all_loaded:
|
|
820
|
-
data = self._data[idx]
|
|
821
|
-
else:
|
|
822
|
-
data = self[idx]
|
|
823
|
-
|
|
824
|
-
X, Y, Z = np.meshgrid(self._x[0], self._x[1], self._x[2], indexing="ij")
|
|
825
|
-
|
|
826
|
-
# Flatten arrays for scatter plot
|
|
827
|
-
(
|
|
828
|
-
X_flat,
|
|
829
|
-
Y_flat,
|
|
830
|
-
Z_flat,
|
|
831
|
-
) = (
|
|
832
|
-
X.ravel(),
|
|
833
|
-
Y.ravel(),
|
|
834
|
-
Z.ravel(),
|
|
835
|
-
)
|
|
836
|
-
data_flat = data.ravel()
|
|
837
|
-
|
|
838
|
-
# Apply filter: Keep only chosen points
|
|
839
|
-
mask = (
|
|
840
|
-
(X_flat > boundaries[0][0])
|
|
841
|
-
& (X_flat < boundaries[0][1])
|
|
842
|
-
& (Y_flat > boundaries[1][0])
|
|
843
|
-
& (Y_flat < boundaries[1][1])
|
|
844
|
-
& (Z_flat > boundaries[2][0])
|
|
845
|
-
& (Z_flat < boundaries[2][1])
|
|
846
|
-
)
|
|
847
|
-
X_cut, Y_cut, Z_cut, data_cut = (
|
|
848
|
-
X_flat[mask],
|
|
849
|
-
Y_flat[mask],
|
|
850
|
-
Z_flat[mask],
|
|
851
|
-
data_flat[mask],
|
|
899
|
+
_msg = (
|
|
900
|
+
"Diagnostic.plot_3d is deprecated and will be removed in a future version. "
|
|
901
|
+
"Please use osiris_utils.vis.plot_3d(diagnostic, idx, ...) instead."
|
|
852
902
|
)
|
|
903
|
+
warnings.warn(_msg, DeprecationWarning, stacklevel=2)
|
|
904
|
+
from ..vis.plot3d import plot_3d
|
|
853
905
|
|
|
854
|
-
|
|
855
|
-
# Center colormap around zero
|
|
856
|
-
cmap = "seismic"
|
|
857
|
-
vmax = np.max(np.abs(data_flat)) # Find max absolute value
|
|
858
|
-
vmin = -vmax
|
|
859
|
-
elif scale_type == "pos":
|
|
860
|
-
cmap = "plasma"
|
|
861
|
-
vmax = np.max(data_flat)
|
|
862
|
-
vmin = 0
|
|
863
|
-
|
|
864
|
-
elif scale_type == "neg":
|
|
865
|
-
cmap = "plasma"
|
|
866
|
-
vmax = 0
|
|
867
|
-
vmin = np.min(data_flat)
|
|
868
|
-
else:
|
|
869
|
-
cmap = "plasma"
|
|
870
|
-
vmax = np.max(data_flat)
|
|
871
|
-
vmin = np.min(data_flat)
|
|
872
|
-
|
|
873
|
-
norm = plt.Normalize(vmin=vmin, vmax=vmax)
|
|
874
|
-
|
|
875
|
-
# Plot
|
|
876
|
-
fig = plt.figure(figsize=(10, 7))
|
|
877
|
-
ax = fig.add_subplot(111, projection="3d")
|
|
878
|
-
|
|
879
|
-
# Scatter plot with seismic colormap
|
|
880
|
-
sc = ax.scatter(X_cut, Y_cut, Z_cut, c=data_cut, cmap=cmap, norm=norm, alpha=1)
|
|
881
|
-
|
|
882
|
-
# Set limits to maintain full background
|
|
883
|
-
ax.set_xlim(*self._grid[0])
|
|
884
|
-
ax.set_ylim(*self._grid[1])
|
|
885
|
-
ax.set_zlim(*self._grid[2])
|
|
886
|
-
|
|
887
|
-
# Colorbar
|
|
888
|
-
cbar = plt.colorbar(sc, ax=ax, shrink=0.6)
|
|
889
|
-
|
|
890
|
-
# Labels
|
|
891
|
-
# TODO try to use a latex label instaead of _name
|
|
892
|
-
cbar.set_label(r"${}$".format(self._name) + r"$\ [{}]$".format(self._units))
|
|
893
|
-
ax.set_title(r"$t={:.2f}$".format(self.time(idx)[0]) + r"$\ [{}]$".format(self.time(idx)[1]))
|
|
894
|
-
ax.set_xlabel(r"${}$".format(self.axis[0]["long_name"]) + r"$\ [{}]$".format(self.axis[0]["units"]))
|
|
895
|
-
ax.set_ylabel(r"${}$".format(self.axis[1]["long_name"]) + r"$\ [{}]$".format(self.axis[1]["units"]))
|
|
896
|
-
ax.set_zlabel(r"${}$".format(self.axis[2]["long_name"]) + r"$\ [{}]$".format(self.axis[2]["units"]))
|
|
897
|
-
|
|
898
|
-
return fig, ax
|
|
906
|
+
return plot_3d(self, idx, scale_type, boundaries)
|
|
899
907
|
|
|
900
908
|
def __str__(self):
|
|
901
909
|
"""String representation of the diagnostic."""
|
|
@@ -903,10 +911,15 @@ class Diagnostic:
|
|
|
903
911
|
|
|
904
912
|
def __repr__(self):
|
|
905
913
|
"""Detailed string representation of the diagnostic."""
|
|
906
|
-
|
|
907
|
-
f"
|
|
908
|
-
f"
|
|
909
|
-
|
|
914
|
+
parts = [
|
|
915
|
+
f"species={self._species}",
|
|
916
|
+
f"name={self._name}",
|
|
917
|
+
f"quantity={self._quantity}",
|
|
918
|
+
f"dim={self._dim}",
|
|
919
|
+
f"maxiter={self._maxiter}",
|
|
920
|
+
f"all_loaded={self._all_loaded}",
|
|
921
|
+
]
|
|
922
|
+
return f"Diagnostic({', '.join(parts)})"
|
|
910
923
|
|
|
911
924
|
# Getters
|
|
912
925
|
@property
|
|
@@ -996,9 +1009,7 @@ class Diagnostic:
|
|
|
996
1009
|
return [index * self._dt * self._ndump, self._tunits]
|
|
997
1010
|
|
|
998
1011
|
def attributes_to_save(self, index: int = 0) -> None:
|
|
999
|
-
"""
|
|
1000
|
-
Prints the attributes of the diagnostic.
|
|
1001
|
-
"""
|
|
1012
|
+
"""Prints the attributes of the diagnostic."""
|
|
1002
1013
|
logger.info(
|
|
1003
1014
|
f"dt: {self._dt}\n"
|
|
1004
1015
|
f"dim: {self._dim}\n"
|
|
@@ -1008,7 +1019,7 @@ class Diagnostic:
|
|
|
1008
1019
|
f"name: {self._name}\n"
|
|
1009
1020
|
f"type: {self._type}\n"
|
|
1010
1021
|
f"label: {self._label}\n"
|
|
1011
|
-
f"units: {self._units}"
|
|
1022
|
+
f"units: {self._units}",
|
|
1012
1023
|
)
|
|
1013
1024
|
|
|
1014
1025
|
@dx.setter
|