osiris-utils 1.1.9__py3-none-any.whl → 1.1.10__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.
- docs/source/conf.py +3 -3
- examples/example_Derivatives.ipynb +33 -11
- osiris_utils/data/diagnostic.py +345 -661
- {osiris_utils-1.1.9.dist-info → osiris_utils-1.1.10.dist-info}/METADATA +1 -4
- {osiris_utils-1.1.9.dist-info → osiris_utils-1.1.10.dist-info}/RECORD +8 -8
- {osiris_utils-1.1.9.dist-info → osiris_utils-1.1.10.dist-info}/licenses/LICENSE.txt +1 -1
- {osiris_utils-1.1.9.dist-info → osiris_utils-1.1.10.dist-info}/WHEEL +0 -0
- {osiris_utils-1.1.9.dist-info → osiris_utils-1.1.10.dist-info}/top_level.txt +0 -0
osiris_utils/data/diagnostic.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
"""
|
|
2
4
|
The utilities on data.py are cool but not useful when you want to work with whole data of a simulation instead
|
|
3
5
|
of just a single file. This is what this file is for - deal with ''folders'' of data.
|
|
@@ -8,9 +10,11 @@ This would be awsome to compute time derivatives.
|
|
|
8
10
|
"""
|
|
9
11
|
|
|
10
12
|
import glob
|
|
13
|
+
import logging
|
|
14
|
+
import operator
|
|
11
15
|
import os
|
|
12
16
|
import warnings
|
|
13
|
-
from typing import
|
|
17
|
+
from typing import Any, Callable, Iterator, Optional, Tuple, Union
|
|
14
18
|
|
|
15
19
|
import h5py
|
|
16
20
|
import matplotlib.pyplot as plt
|
|
@@ -19,6 +23,9 @@ import tqdm
|
|
|
19
23
|
|
|
20
24
|
from .data import OsirisGridFile
|
|
21
25
|
|
|
26
|
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(name)s %(levelname)8s │ %(message)s")
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
22
29
|
OSIRIS_DENSITY = ["n"]
|
|
23
30
|
OSIRIS_SPECIE_REPORTS = ["charge", "q1", "q2", "q3", "j1", "j2", "j3"]
|
|
24
31
|
OSIRIS_SPECIE_REP_UDIST = [
|
|
@@ -77,6 +84,25 @@ OSIRIS_PHA = [
|
|
|
77
84
|
] # there may be more that I don't know
|
|
78
85
|
OSIRIS_ALL = OSIRIS_DENSITY + OSIRIS_SPECIE_REPORTS + OSIRIS_SPECIE_REP_UDIST + OSIRIS_FLD + OSIRIS_PHA
|
|
79
86
|
|
|
87
|
+
_ATTRS_TO_CLONE = [
|
|
88
|
+
"_dx",
|
|
89
|
+
"_nx",
|
|
90
|
+
"_x",
|
|
91
|
+
"_dt",
|
|
92
|
+
"_grid",
|
|
93
|
+
"_axis",
|
|
94
|
+
"_units",
|
|
95
|
+
"_name",
|
|
96
|
+
"_label",
|
|
97
|
+
"_dim",
|
|
98
|
+
"_ndump",
|
|
99
|
+
"_maxiter",
|
|
100
|
+
"_tunits",
|
|
101
|
+
"_type",
|
|
102
|
+
"_simulation_folder",
|
|
103
|
+
"_quantity",
|
|
104
|
+
]
|
|
105
|
+
|
|
80
106
|
|
|
81
107
|
def which_quantities():
|
|
82
108
|
print("Available quantities:")
|
|
@@ -163,22 +189,22 @@ class Diagnostic:
|
|
|
163
189
|
|
|
164
190
|
"""
|
|
165
191
|
|
|
166
|
-
def __init__(self, simulation_folder=None, species=None, input_deck=None):
|
|
192
|
+
def __init__(self, simulation_folder: Optional[str] = None, species: Any = None, input_deck: Optional[str | None] = None) -> None:
|
|
167
193
|
self._species = species if species else None
|
|
168
194
|
|
|
169
|
-
self._dx = None
|
|
170
|
-
self._nx = None
|
|
171
|
-
self._x = None
|
|
172
|
-
self._dt = None
|
|
173
|
-
self._grid = None
|
|
174
|
-
self._axis = None
|
|
175
|
-
self._units = None
|
|
176
|
-
self._name = None
|
|
177
|
-
self._label = None
|
|
178
|
-
self._dim = None
|
|
179
|
-
self._ndump = None
|
|
180
|
-
self._maxiter = None
|
|
181
|
-
self._tunits = None
|
|
195
|
+
self._dx: Optional[Union[float, np.ndarray]] = None # grid spacing in each direction
|
|
196
|
+
self._nx: Optional[Union[int, np.ndarray]] = None # number of grid points in each direction
|
|
197
|
+
self._x: Optional[np.ndarray] = None # grid points
|
|
198
|
+
self._dt: Optional[float] = None # time step
|
|
199
|
+
self._grid: Optional[np.ndarray] = None # grid boundaries
|
|
200
|
+
self._axis: Optional[Any] = None # axis information
|
|
201
|
+
self._units: Optional[str] = None # units of the diagnostic
|
|
202
|
+
self._name: Optional[str] = None
|
|
203
|
+
self._label: Optional[str] = None
|
|
204
|
+
self._dim: Optional[int] = None
|
|
205
|
+
self._ndump: Optional[int] = None
|
|
206
|
+
self._maxiter: Optional[int] = None
|
|
207
|
+
self._tunits: Optional[str] = None # time units
|
|
182
208
|
|
|
183
209
|
if simulation_folder:
|
|
184
210
|
self._simulation_folder = simulation_folder
|
|
@@ -186,17 +212,22 @@ class Diagnostic:
|
|
|
186
212
|
raise FileNotFoundError(f"Simulation folder {simulation_folder} not found.")
|
|
187
213
|
else:
|
|
188
214
|
self._simulation_folder = None
|
|
189
|
-
|
|
190
215
|
# load input deck if available
|
|
191
216
|
if input_deck:
|
|
192
217
|
self._input_deck = input_deck
|
|
193
218
|
else:
|
|
194
219
|
self._input_deck = None
|
|
195
220
|
|
|
196
|
-
self._all_loaded = False
|
|
197
|
-
self._quantity = None
|
|
221
|
+
self._all_loaded: bool = False # if the data is already loaded into memory
|
|
222
|
+
self._quantity: Optional[str] = None
|
|
198
223
|
|
|
199
|
-
|
|
224
|
+
#########################################
|
|
225
|
+
#
|
|
226
|
+
# Diagnostic metadata and attributes
|
|
227
|
+
#
|
|
228
|
+
#########################################
|
|
229
|
+
|
|
230
|
+
def get_quantity(self, quantity: str) -> None:
|
|
200
231
|
"""
|
|
201
232
|
Get the data for a given quantity.
|
|
202
233
|
|
|
@@ -232,39 +263,43 @@ class Diagnostic:
|
|
|
232
263
|
f"Invalid quantity {self._quantity}. Or it's not implemented yet (this may happen for phase space quantities)."
|
|
233
264
|
)
|
|
234
265
|
|
|
235
|
-
def
|
|
266
|
+
def _scan_files(self, pattern: str) -> None:
|
|
267
|
+
"""Populate _file_list and related attributes from a glob pattern."""
|
|
268
|
+
self._file_list = sorted(glob.glob(pattern))
|
|
269
|
+
if not self._file_list:
|
|
270
|
+
raise FileNotFoundError(f"No HDF5 files match {pattern}")
|
|
271
|
+
self._file_template = self._file_list[0][:-9] # keep old “template” idea
|
|
272
|
+
self._maxiter = len(self._file_list)
|
|
273
|
+
|
|
274
|
+
def _get_moment(self, species: str, moment: str) -> None:
|
|
236
275
|
if self._simulation_folder is None:
|
|
237
276
|
raise ValueError("Simulation folder not set. If you're using CustomDiagnostic, this method is not available.")
|
|
238
277
|
self._path = f"{self._simulation_folder}/MS/UDIST/{species}/{moment}/"
|
|
239
|
-
self.
|
|
240
|
-
self._maxiter = len(glob.glob(f"{self._path}/*.h5"))
|
|
278
|
+
self._scan_files(os.path.join(self._path, "*.h5"))
|
|
241
279
|
self._load_attributes(self._file_template, self._input_deck)
|
|
242
280
|
|
|
243
|
-
def _get_field(self, field):
|
|
281
|
+
def _get_field(self, field: str) -> None:
|
|
244
282
|
if self._simulation_folder is None:
|
|
245
283
|
raise ValueError("Simulation folder not set. If you're using CustomDiagnostic, this method is not available.")
|
|
246
284
|
self._path = f"{self._simulation_folder}/MS/FLD/{field}/"
|
|
247
|
-
self.
|
|
248
|
-
self._maxiter = len(glob.glob(f"{self._path}/*.h5"))
|
|
285
|
+
self._scan_files(os.path.join(self._path, "*.h5"))
|
|
249
286
|
self._load_attributes(self._file_template, self._input_deck)
|
|
250
287
|
|
|
251
|
-
def _get_density(self, species, quantity):
|
|
288
|
+
def _get_density(self, species: str, quantity: str) -> None:
|
|
252
289
|
if self._simulation_folder is None:
|
|
253
290
|
raise ValueError("Simulation folder not set. If you're using CustomDiagnostic, this method is not available.")
|
|
254
291
|
self._path = f"{self._simulation_folder}/MS/DENSITY/{species}/{quantity}/"
|
|
255
|
-
self.
|
|
256
|
-
self._maxiter = len(glob.glob(f"{self._path}/*.h5"))
|
|
292
|
+
self._scan_files(os.path.join(self._path, "*.h5"))
|
|
257
293
|
self._load_attributes(self._file_template, self._input_deck)
|
|
258
294
|
|
|
259
|
-
def _get_phase_space(self, species, type):
|
|
295
|
+
def _get_phase_space(self, species: str, type: str) -> None:
|
|
260
296
|
if self._simulation_folder is None:
|
|
261
297
|
raise ValueError("Simulation folder not set. If you're using CustomDiagnostic, this method is not available.")
|
|
262
298
|
self._path = f"{self._simulation_folder}/MS/PHA/{type}/{species}/"
|
|
263
|
-
self.
|
|
264
|
-
self._maxiter = len(glob.glob(f"{self._path}/*.h5"))
|
|
299
|
+
self._scan_files(os.path.join(self._path, "*.h5"))
|
|
265
300
|
self._load_attributes(self._file_template, self._input_deck)
|
|
266
301
|
|
|
267
|
-
def _load_attributes(self, file_template, input_deck): # this will be replaced by reading the input deck
|
|
302
|
+
def _load_attributes(self, file_template: str, input_deck: Optional[dict]) -> None: # this will be replaced by reading the input deck
|
|
268
303
|
# This can go wrong! NDUMP
|
|
269
304
|
# if input_deck is not None:
|
|
270
305
|
# self._dt = float(input_deck["time_step"][0]["dt"])
|
|
@@ -275,7 +310,10 @@ class Diagnostic:
|
|
|
275
310
|
# self._dx = (self._grid[:,1] - self._grid[:,0])/self._nx
|
|
276
311
|
# self._x = [np.arange(self._grid[i,0], self._grid[i,1], self._dx[i]) for i in range(self._dim)]
|
|
277
312
|
|
|
278
|
-
|
|
313
|
+
if input_deck is not None:
|
|
314
|
+
self._ndump = int(input_deck["time_step"][0]["ndump"])
|
|
315
|
+
elif input_deck is None:
|
|
316
|
+
self._ndump = 1
|
|
279
317
|
|
|
280
318
|
try:
|
|
281
319
|
# Try files 000001, 000002, etc. until one is found
|
|
@@ -305,150 +343,163 @@ class Diagnostic:
|
|
|
305
343
|
except Exception as e:
|
|
306
344
|
warnings.warn(f"Error loading diagnostic attributes: {str(e)}. Please verify it there's any file in the folder.")
|
|
307
345
|
|
|
308
|
-
|
|
346
|
+
##########################################
|
|
347
|
+
#
|
|
348
|
+
# Data loading and processing
|
|
349
|
+
#
|
|
350
|
+
##########################################
|
|
351
|
+
|
|
352
|
+
def _data_generator(self, index: int) -> None:
|
|
309
353
|
if self._simulation_folder is None:
|
|
310
354
|
raise ValueError("Simulation folder not set.")
|
|
311
|
-
|
|
355
|
+
if self._file_list is None:
|
|
356
|
+
raise RuntimeError("File list not initialized. Call get_quantity() first.")
|
|
357
|
+
try:
|
|
358
|
+
file = self._file_list[index]
|
|
359
|
+
except IndexError:
|
|
360
|
+
raise RuntimeError(f"File index {index} out of range (max {self._maxiter - 1}).")
|
|
312
361
|
data_object = OsirisGridFile(file)
|
|
313
|
-
yield (data_object.data if self._quantity not in OSIRIS_DENSITY else self._species.rqm * data_object.data)
|
|
362
|
+
yield (data_object.data if self._quantity not in OSIRIS_DENSITY else np.sign(self._species.rqm) * data_object.data)
|
|
314
363
|
|
|
315
|
-
def load_all(self):
|
|
364
|
+
def load_all(self) -> np.ndarray:
|
|
316
365
|
"""
|
|
317
|
-
Load all data into memory (all iterations).
|
|
366
|
+
Load all data into memory (all iterations), in a pre-allocated array.
|
|
318
367
|
|
|
319
368
|
Returns
|
|
320
369
|
-------
|
|
321
370
|
data : np.ndarray
|
|
322
|
-
The data for all iterations. Also stored in
|
|
371
|
+
The data for all iterations. Also stored in self._data.
|
|
323
372
|
"""
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
print("Data already loaded.")
|
|
373
|
+
if getattr(self, "_all_loaded", False) and self._data is not None:
|
|
374
|
+
logger.debug("Data already loaded into memory.")
|
|
327
375
|
return self._data
|
|
328
376
|
|
|
329
|
-
|
|
330
|
-
if
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
if hasattr(self, "_diag") and hasattr(self._diag, "_maxiter"):
|
|
340
|
-
size = self._diag._maxiter
|
|
341
|
-
else:
|
|
342
|
-
# Default to a reasonable number if we can't determine
|
|
343
|
-
size = 100
|
|
344
|
-
print(f"Warning: Could not determine timestep count, using {size}.")
|
|
345
|
-
|
|
346
|
-
# Load data for all timesteps using the generator - this may take a while
|
|
347
|
-
self._data = np.stack([self[i] for i in tqdm.tqdm(range(size), desc="Loading data")])
|
|
348
|
-
self._all_loaded = True
|
|
349
|
-
return self._data
|
|
377
|
+
size = getattr(self, "_maxiter", None)
|
|
378
|
+
if size is None:
|
|
379
|
+
raise RuntimeError("Cannot determine iteration count (no _maxiter).")
|
|
380
|
+
|
|
381
|
+
try:
|
|
382
|
+
first = self[0]
|
|
383
|
+
except Exception as e:
|
|
384
|
+
raise RuntimeError(f"Failed to load first timestep: {e}")
|
|
385
|
+
slice_shape = first.shape
|
|
386
|
+
dtype = first.dtype
|
|
350
387
|
|
|
388
|
+
data = np.empty((size, *slice_shape), dtype=dtype)
|
|
389
|
+
data[0] = first
|
|
390
|
+
|
|
391
|
+
for i in tqdm.trange(1, size, desc="Loading data"):
|
|
392
|
+
try:
|
|
393
|
+
data[i] = self[i]
|
|
351
394
|
except Exception as e:
|
|
352
|
-
raise
|
|
395
|
+
raise RuntimeError(f"Error loading timestep {i}: {e}")
|
|
353
396
|
|
|
354
|
-
|
|
355
|
-
print("Loading all data from files. This may take a while.")
|
|
356
|
-
size = len(sorted(glob.glob(f"{self._path}/*.h5")))
|
|
357
|
-
self._data = np.stack([self[i] for i in tqdm.tqdm(range(size), desc="Loading data")])
|
|
397
|
+
self._data = data
|
|
358
398
|
self._all_loaded = True
|
|
359
399
|
return self._data
|
|
360
400
|
|
|
361
|
-
def unload(self):
|
|
401
|
+
def unload(self) -> None:
|
|
362
402
|
"""
|
|
363
403
|
Unload data from memory. This is useful to free memory when the data is not needed anymore.
|
|
364
404
|
"""
|
|
365
|
-
|
|
405
|
+
logger.info("Unloading data from memory.")
|
|
366
406
|
if self._all_loaded is False:
|
|
367
|
-
|
|
407
|
+
logger.warning("Data is not loaded.")
|
|
368
408
|
return
|
|
369
409
|
self._data = None
|
|
370
410
|
self._all_loaded = False
|
|
371
411
|
|
|
372
|
-
|
|
412
|
+
###########################################
|
|
413
|
+
#
|
|
414
|
+
# Data access and iteration
|
|
415
|
+
#
|
|
416
|
+
###########################################
|
|
417
|
+
|
|
418
|
+
def __len__(self) -> int:
|
|
419
|
+
"""Return the number of timesteps available."""
|
|
420
|
+
return getattr(self, "_maxiter", 0)
|
|
421
|
+
|
|
422
|
+
def __getitem__(self, index: int) -> np.ndarray:
|
|
373
423
|
"""
|
|
374
|
-
|
|
424
|
+
Retrieve timestep data.
|
|
425
|
+
|
|
426
|
+
Parameters
|
|
427
|
+
----------
|
|
428
|
+
index : int or slice
|
|
429
|
+
- If int, may be negative (Python-style).
|
|
430
|
+
- If slice, supports start:stop:step. Zero-length slices return an empty array of shape (0, ...).
|
|
431
|
+
|
|
432
|
+
Returns
|
|
433
|
+
-------
|
|
434
|
+
np.ndarray
|
|
435
|
+
Array for that timestep (or stacked array for a slice).
|
|
436
|
+
|
|
437
|
+
Raises
|
|
438
|
+
------
|
|
439
|
+
IndexError
|
|
440
|
+
If the index is out of range or no data generator is available.
|
|
441
|
+
RuntimeError
|
|
442
|
+
If loading a specific timestep fails.
|
|
375
443
|
"""
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
# def __getitem__(self, index):
|
|
379
|
-
# # For derived diagnostics with cached data
|
|
380
|
-
# if self._all_loaded and self._data is not None:
|
|
381
|
-
# return self._data[index]
|
|
382
|
-
|
|
383
|
-
# # For standard diagnostics with files
|
|
384
|
-
# if isinstance(index, int):
|
|
385
|
-
# if self._simulation_folder is not None and hasattr(self, "_data_generator"):
|
|
386
|
-
# return next(self._data_generator(index))
|
|
387
|
-
|
|
388
|
-
# # For derived diagnostics with custom generators
|
|
389
|
-
# if hasattr(self, "_data_generator") and callable(self._data_generator):
|
|
390
|
-
# return next(self._data_generator(index))
|
|
391
|
-
|
|
392
|
-
# elif isinstance(index, slice):
|
|
393
|
-
# start = 0 if index.start is None else index.start
|
|
394
|
-
# step = 1 if index.step is None else index.step
|
|
395
|
-
|
|
396
|
-
# if index.stop is None:
|
|
397
|
-
# if hasattr(self, "_maxiter") and self._maxiter is not None:
|
|
398
|
-
# stop = self._maxiter
|
|
399
|
-
# elif self._simulation_folder is not None and hasattr(self, "_path"):
|
|
400
|
-
# stop = len(sorted(glob.glob(f"{self._path}/*.h5")))
|
|
401
|
-
# else:
|
|
402
|
-
# stop = 100 # Default if we can't determine
|
|
403
|
-
# print(
|
|
404
|
-
# f"Warning: Could not determine iteration count for iteration, using {stop}."
|
|
405
|
-
# )
|
|
406
|
-
# else:
|
|
407
|
-
# stop = index.stop
|
|
408
|
-
|
|
409
|
-
# indices = range(start, stop, step)
|
|
410
|
-
# if self._simulation_folder is not None and hasattr(self, "_data_generator"):
|
|
411
|
-
# return np.stack([next(self._data_generator(i)) for i in indices])
|
|
412
|
-
# elif hasattr(self, "_data_generator") and callable(self._data_generator):
|
|
413
|
-
# return np.stack([next(self._data_generator(i)) for i in indices])
|
|
414
|
-
|
|
415
|
-
# # If we get here, we don't know how to get data for this index
|
|
416
|
-
# raise ValueError(
|
|
417
|
-
# f"Cannot retrieve data for this diagnostic at index {index}. No data loaded and no generator available."
|
|
418
|
-
# )
|
|
419
|
-
|
|
420
|
-
def __getitem__(self, index):
|
|
421
|
-
if self._all_loaded and self._data is not None:
|
|
444
|
+
# Quick path: all data already in memory
|
|
445
|
+
if getattr(self, "_all_loaded", False) and self._data is not None:
|
|
422
446
|
return self._data[index]
|
|
423
447
|
|
|
448
|
+
# Data generator must exist
|
|
424
449
|
data_gen = getattr(self, "_data_generator", None)
|
|
425
|
-
|
|
450
|
+
if not callable(data_gen):
|
|
451
|
+
raise IndexError(f"No data available for indexing; you did something wrong!")
|
|
426
452
|
|
|
453
|
+
# Handle int indices (including negatives)
|
|
427
454
|
if isinstance(index, int):
|
|
428
|
-
if
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
455
|
+
if index < 0:
|
|
456
|
+
index += self._maxiter
|
|
457
|
+
if not (0 <= index < self._maxiter):
|
|
458
|
+
raise IndexError(f"Index {index} out of range (0..{self._maxiter - 1})")
|
|
459
|
+
try:
|
|
460
|
+
gen = data_gen(index)
|
|
461
|
+
return next(gen)
|
|
462
|
+
except Exception as e:
|
|
463
|
+
raise RuntimeError(f"Error loading data at index {index}: {e}")
|
|
433
464
|
|
|
434
|
-
|
|
465
|
+
# Handle slice indices
|
|
466
|
+
if isinstance(index, slice):
|
|
435
467
|
start = index.start or 0
|
|
436
|
-
step = index.step or 1
|
|
437
468
|
stop = index.stop if index.stop is not None else self._maxiter
|
|
469
|
+
step = index.step or 1
|
|
470
|
+
# Normalize negatives
|
|
471
|
+
if start < 0:
|
|
472
|
+
start += self._maxiter
|
|
473
|
+
if stop < 0:
|
|
474
|
+
stop += self._maxiter
|
|
475
|
+
# Clip to bounds
|
|
476
|
+
start = max(0, min(start, self._maxiter))
|
|
477
|
+
stop = max(0, min(stop, self._maxiter))
|
|
438
478
|
indices = range(start, stop, step)
|
|
439
479
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
480
|
+
# Empty slice
|
|
481
|
+
if not indices:
|
|
482
|
+
# Determine single-step shape by peeking at index 0 (if possible)
|
|
483
|
+
try:
|
|
484
|
+
dummy = next(data_gen(0))
|
|
485
|
+
empty_shape = (0,) + dummy.shape
|
|
486
|
+
return np.empty(empty_shape, dtype=dummy.dtype)
|
|
487
|
+
except Exception:
|
|
488
|
+
return np.empty((0,))
|
|
489
|
+
|
|
490
|
+
# Collect and stack
|
|
491
|
+
data_list = []
|
|
492
|
+
for i in indices:
|
|
493
|
+
try:
|
|
494
|
+
data_list.append(next(data_gen(i)))
|
|
495
|
+
except Exception as e:
|
|
496
|
+
raise RuntimeError(f"Error loading slice at index {i}: {e}")
|
|
497
|
+
return np.stack(data_list)
|
|
448
498
|
|
|
449
|
-
|
|
499
|
+
# Unsupported index type
|
|
500
|
+
raise IndexError(f"Invalid index type {type(index)}; must be int or slice")
|
|
450
501
|
|
|
451
|
-
def __iter__(self):
|
|
502
|
+
def __iter__(self) -> Iterator[np.ndarray]:
|
|
452
503
|
# If this is a file-based diagnostic
|
|
453
504
|
if self._simulation_folder is not None:
|
|
454
505
|
for i in range(len(sorted(glob.glob(f"{self._path}/*.h5")))):
|
|
@@ -468,7 +519,7 @@ class Diagnostic:
|
|
|
468
519
|
max_iter = self._diag._maxiter
|
|
469
520
|
else:
|
|
470
521
|
max_iter = 100 # Default if we can't determine
|
|
471
|
-
|
|
522
|
+
logger.warning(f"Could not determine iteration count for iteration, using {max_iter}.")
|
|
472
523
|
|
|
473
524
|
for i in range(max_iter):
|
|
474
525
|
yield next(self._data_generator(i))
|
|
@@ -477,501 +528,116 @@ class Diagnostic:
|
|
|
477
528
|
else:
|
|
478
529
|
raise ValueError("Cannot iterate over this diagnostic. No data loaded and no generator available.")
|
|
479
530
|
|
|
480
|
-
def
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
# result._name = self._name + " + " + str(other) if isinstance(other, (int, float)) else self._name + " + np.ndarray"
|
|
507
|
-
|
|
508
|
-
if self._all_loaded:
|
|
509
|
-
result._data = self._data + other
|
|
510
|
-
result._all_loaded = True
|
|
511
|
-
else:
|
|
512
|
-
|
|
513
|
-
def gen_scalar_add(original_gen, scalar):
|
|
514
|
-
for val in original_gen:
|
|
515
|
-
yield val + scalar
|
|
516
|
-
|
|
517
|
-
original_generator = self._data_generator
|
|
518
|
-
result._data_generator = lambda index: gen_scalar_add(original_generator(index), other)
|
|
519
|
-
|
|
520
|
-
result.created_diagnostic_name = "MISC"
|
|
521
|
-
|
|
522
|
-
return result
|
|
523
|
-
|
|
524
|
-
elif isinstance(other, Diagnostic):
|
|
525
|
-
result = Diagnostic(species=self._species)
|
|
526
|
-
|
|
527
|
-
for attr in [
|
|
528
|
-
"_dx",
|
|
529
|
-
"_nx",
|
|
530
|
-
"_x",
|
|
531
|
-
"_dt",
|
|
532
|
-
"_grid",
|
|
533
|
-
"_axis",
|
|
534
|
-
"_dim",
|
|
535
|
-
"_ndump",
|
|
536
|
-
"_maxiter",
|
|
537
|
-
"_tunits",
|
|
538
|
-
"_type",
|
|
539
|
-
"_simulation_folder",
|
|
540
|
-
]:
|
|
541
|
-
if hasattr(self, attr):
|
|
542
|
-
setattr(result, attr, getattr(self, attr))
|
|
543
|
-
|
|
544
|
-
if not hasattr(result, "_maxiter") or result._maxiter is None:
|
|
545
|
-
if hasattr(self, "_maxiter") and self._maxiter is not None:
|
|
546
|
-
result._maxiter = self._maxiter
|
|
547
|
-
|
|
548
|
-
# result._name = self._name + " + " + str(other._name)
|
|
549
|
-
|
|
550
|
-
if self._all_loaded:
|
|
551
|
-
other.load_all()
|
|
552
|
-
result._data = self._data + other._data
|
|
553
|
-
result._all_loaded = True
|
|
554
|
-
else:
|
|
555
|
-
|
|
556
|
-
def gen_diag_add(original_gen1, original_gen2):
|
|
557
|
-
for val1, val2 in zip(original_gen1, original_gen2):
|
|
558
|
-
yield val1 + val2
|
|
559
|
-
|
|
560
|
-
original_generator = self._data_generator
|
|
561
|
-
other_generator = other._data_generator
|
|
562
|
-
result._data_generator = lambda index: gen_diag_add(original_generator(index), other_generator(index))
|
|
563
|
-
|
|
564
|
-
result.created_diagnostic_name = "MISC"
|
|
565
|
-
|
|
566
|
-
return result
|
|
567
|
-
|
|
568
|
-
def __sub__(self, other):
|
|
569
|
-
if isinstance(other, (int, float, np.ndarray)):
|
|
570
|
-
result = Diagnostic(species=self._species)
|
|
571
|
-
|
|
572
|
-
for attr in [
|
|
573
|
-
"_dx",
|
|
574
|
-
"_nx",
|
|
575
|
-
"_x",
|
|
576
|
-
"_dt",
|
|
577
|
-
"_grid",
|
|
578
|
-
"_axis",
|
|
579
|
-
"_dim",
|
|
580
|
-
"_ndump",
|
|
581
|
-
"_maxiter",
|
|
582
|
-
"_tunits",
|
|
583
|
-
"_type",
|
|
584
|
-
"_simulation_folder",
|
|
585
|
-
]:
|
|
586
|
-
if hasattr(self, attr):
|
|
587
|
-
setattr(result, attr, getattr(self, attr))
|
|
588
|
-
|
|
589
|
-
if not hasattr(result, "_maxiter") or result._maxiter is None:
|
|
590
|
-
if hasattr(self, "_maxiter") and self._maxiter is not None:
|
|
591
|
-
result._maxiter = self._maxiter
|
|
592
|
-
|
|
593
|
-
# result._name = self._name + " - " + str(other) if isinstance(other, (int, float)) else self._name + " - np.ndarray"
|
|
594
|
-
|
|
595
|
-
if self._all_loaded:
|
|
596
|
-
result._data = self._data - other
|
|
597
|
-
result._all_loaded = True
|
|
598
|
-
else:
|
|
599
|
-
|
|
600
|
-
def gen_scalar_sub(original_gen, scalar):
|
|
601
|
-
for val in original_gen:
|
|
602
|
-
yield val - scalar
|
|
603
|
-
|
|
604
|
-
original_generator = self._data_generator
|
|
605
|
-
result._data_generator = lambda index: gen_scalar_sub(original_generator(index), other)
|
|
606
|
-
|
|
607
|
-
result.created_diagnostic_name = "MISC"
|
|
608
|
-
|
|
609
|
-
return result
|
|
610
|
-
|
|
611
|
-
elif isinstance(other, Diagnostic):
|
|
612
|
-
result = Diagnostic(species=self._species)
|
|
613
|
-
|
|
614
|
-
for attr in [
|
|
615
|
-
"_dx",
|
|
616
|
-
"_nx",
|
|
617
|
-
"_x",
|
|
618
|
-
"_dt",
|
|
619
|
-
"_grid",
|
|
620
|
-
"_axis",
|
|
621
|
-
"_dim",
|
|
622
|
-
"_ndump",
|
|
623
|
-
"_maxiter",
|
|
624
|
-
"_tunits",
|
|
625
|
-
"_type",
|
|
626
|
-
"_simulation_folder",
|
|
627
|
-
]:
|
|
628
|
-
if hasattr(self, attr):
|
|
629
|
-
setattr(result, attr, getattr(self, attr))
|
|
630
|
-
|
|
631
|
-
if not hasattr(result, "_maxiter") or result._maxiter is None:
|
|
632
|
-
if hasattr(self, "_maxiter") and self._maxiter is not None:
|
|
633
|
-
result._maxiter = self._maxiter
|
|
634
|
-
|
|
635
|
-
# result._name = self._name + " - " + str(other._name)
|
|
636
|
-
|
|
637
|
-
if self._all_loaded:
|
|
638
|
-
other.load_all()
|
|
639
|
-
result._data = self._data - other._data
|
|
640
|
-
result._all_loaded = True
|
|
641
|
-
else:
|
|
642
|
-
|
|
643
|
-
def gen_diag_sub(original_gen1, original_gen2):
|
|
644
|
-
for val1, val2 in zip(original_gen1, original_gen2):
|
|
645
|
-
yield val1 - val2
|
|
646
|
-
|
|
647
|
-
original_generator = self._data_generator
|
|
648
|
-
other_generator = other._data_generator
|
|
649
|
-
result._data_generator = lambda index: gen_diag_sub(original_generator(index), other_generator(index))
|
|
650
|
-
|
|
651
|
-
result.created_diagnostic_name = "MISC"
|
|
652
|
-
|
|
653
|
-
return result
|
|
654
|
-
|
|
655
|
-
def __mul__(self, other):
|
|
656
|
-
if isinstance(other, (int, float, np.ndarray)):
|
|
657
|
-
result = Diagnostic(species=self._species)
|
|
658
|
-
|
|
659
|
-
for attr in [
|
|
660
|
-
"_dx",
|
|
661
|
-
"_nx",
|
|
662
|
-
"_x",
|
|
663
|
-
"_dt",
|
|
664
|
-
"_grid",
|
|
665
|
-
"_axis",
|
|
666
|
-
"_dim",
|
|
667
|
-
"_ndump",
|
|
668
|
-
"_maxiter",
|
|
669
|
-
"_tunits",
|
|
670
|
-
"_type",
|
|
671
|
-
"_simulation_folder",
|
|
672
|
-
]:
|
|
673
|
-
if hasattr(self, attr):
|
|
674
|
-
setattr(result, attr, getattr(self, attr))
|
|
675
|
-
|
|
676
|
-
if not hasattr(result, "_maxiter") or result._maxiter is None:
|
|
677
|
-
if hasattr(self, "_maxiter") and self._maxiter is not None:
|
|
678
|
-
result._maxiter = self._maxiter
|
|
679
|
-
|
|
680
|
-
# result._name = self._name + " * " + str(other) if isinstance(other, (int, float)) else self._name + " * np.ndarray"
|
|
681
|
-
|
|
682
|
-
if self._all_loaded:
|
|
683
|
-
result._data = self._data * other
|
|
684
|
-
result._all_loaded = True
|
|
685
|
-
else:
|
|
686
|
-
|
|
687
|
-
def gen_scalar_mul(original_gen, scalar):
|
|
688
|
-
for val in original_gen:
|
|
689
|
-
yield val * scalar
|
|
690
|
-
|
|
691
|
-
original_generator = self._data_generator
|
|
692
|
-
result._data_generator = lambda index: gen_scalar_mul(original_generator(index), other)
|
|
693
|
-
|
|
694
|
-
result.created_diagnostic_name = "MISC"
|
|
695
|
-
|
|
696
|
-
return result
|
|
697
|
-
|
|
698
|
-
elif isinstance(other, Diagnostic):
|
|
699
|
-
result = Diagnostic(species=self._species)
|
|
700
|
-
|
|
701
|
-
for attr in [
|
|
702
|
-
"_dx",
|
|
703
|
-
"_nx",
|
|
704
|
-
"_x",
|
|
705
|
-
"_dt",
|
|
706
|
-
"_grid",
|
|
707
|
-
"_axis",
|
|
708
|
-
"_dim",
|
|
709
|
-
"_ndump",
|
|
710
|
-
"_maxiter",
|
|
711
|
-
"_tunits",
|
|
712
|
-
"_type",
|
|
713
|
-
"_simulation_folder",
|
|
714
|
-
]:
|
|
715
|
-
if hasattr(self, attr):
|
|
716
|
-
setattr(result, attr, getattr(self, attr))
|
|
717
|
-
|
|
718
|
-
if not hasattr(result, "_maxiter") or result._maxiter is None:
|
|
719
|
-
if hasattr(self, "_maxiter") and self._maxiter is not None:
|
|
720
|
-
result._maxiter = self._maxiter
|
|
721
|
-
|
|
722
|
-
# result._name = self._name + " * " + str(other._name)
|
|
723
|
-
|
|
724
|
-
if self._all_loaded:
|
|
725
|
-
other.load_all()
|
|
726
|
-
result._data = self._data * other._data
|
|
727
|
-
result._all_loaded = True
|
|
728
|
-
else:
|
|
729
|
-
|
|
730
|
-
def gen_diag_mul(original_gen1, original_gen2):
|
|
731
|
-
for val1, val2 in zip(original_gen1, original_gen2):
|
|
732
|
-
yield val1 * val2
|
|
733
|
-
|
|
734
|
-
original_generator = self._data_generator
|
|
735
|
-
other_generator = other._data_generator
|
|
736
|
-
result._data_generator = lambda index: gen_diag_mul(original_generator(index), other_generator(index))
|
|
737
|
-
|
|
738
|
-
result.created_diagnostic_name = "MISC"
|
|
739
|
-
|
|
740
|
-
return result
|
|
741
|
-
|
|
742
|
-
def __truediv__(self, other):
|
|
743
|
-
if isinstance(other, (int, float, np.ndarray)):
|
|
744
|
-
result = Diagnostic(species=self._species)
|
|
745
|
-
|
|
746
|
-
for attr in [
|
|
747
|
-
"_dx",
|
|
748
|
-
"_nx",
|
|
749
|
-
"_x",
|
|
750
|
-
"_dt",
|
|
751
|
-
"_grid",
|
|
752
|
-
"_axis",
|
|
753
|
-
"_dim",
|
|
754
|
-
"_ndump",
|
|
755
|
-
"_maxiter",
|
|
756
|
-
"_tunits",
|
|
757
|
-
"_type",
|
|
758
|
-
"_simulation_folder",
|
|
759
|
-
]:
|
|
760
|
-
if hasattr(self, attr):
|
|
761
|
-
setattr(result, attr, getattr(self, attr))
|
|
762
|
-
|
|
763
|
-
if not hasattr(result, "_maxiter") or result._maxiter is None:
|
|
764
|
-
if hasattr(self, "_maxiter") and self._maxiter is not None:
|
|
765
|
-
result._maxiter = self._maxiter
|
|
766
|
-
|
|
767
|
-
# result._name = self._name + " / " + str(other) if isinstance(other, (int, float)) else self._name + " / np.ndarray"
|
|
768
|
-
|
|
769
|
-
if self._all_loaded:
|
|
770
|
-
result._data = self._data / other
|
|
771
|
-
result._all_loaded = True
|
|
772
|
-
else:
|
|
773
|
-
|
|
774
|
-
def gen_scalar_div(original_gen, scalar):
|
|
775
|
-
for val in original_gen:
|
|
776
|
-
yield val / scalar
|
|
777
|
-
|
|
778
|
-
original_generator = self._data_generator
|
|
779
|
-
result._data_generator = lambda index: gen_scalar_div(original_generator(index), other)
|
|
780
|
-
|
|
781
|
-
result.created_diagnostic_name = "MISC"
|
|
531
|
+
def _clone_meta(self) -> Diagnostic:
|
|
532
|
+
"""
|
|
533
|
+
Create a new Diagnostic instance that carries over metadata only.
|
|
534
|
+
No data is copied, and no constructor edits are required because we
|
|
535
|
+
assign attributes dynamically.
|
|
536
|
+
"""
|
|
537
|
+
clone = Diagnostic(species=getattr(self, "_species", None)) # keep species link
|
|
538
|
+
for attr in _ATTRS_TO_CLONE:
|
|
539
|
+
if hasattr(self, attr):
|
|
540
|
+
setattr(clone, attr, getattr(self, attr))
|
|
541
|
+
# If this diagnostic already discovered a _file_list via _scan_files,
|
|
542
|
+
# copy it too (harmless for virtual diags).
|
|
543
|
+
if hasattr(self, "_file_list"):
|
|
544
|
+
clone._file_list = self._file_list
|
|
545
|
+
return clone
|
|
546
|
+
|
|
547
|
+
def _binary_op(self, other: Union["Diagnostic", int, float, np.ndarray], op_func: Callable) -> Diagnostic:
|
|
548
|
+
"""
|
|
549
|
+
Universal helper for `self (op) other`.
|
|
550
|
+
- If both operands are fully loaded, does eager numpy arithmetic.
|
|
551
|
+
- Otherwise builds a lazy generator that applies op_func on each timestep.
|
|
552
|
+
"""
|
|
553
|
+
# 1) Prepare the metadata clone
|
|
554
|
+
result = self._clone_meta()
|
|
555
|
+
result.created_diagnostic_name = "MISC"
|
|
782
556
|
|
|
557
|
+
# 2) Determine iteration count
|
|
558
|
+
if isinstance(other, Diagnostic):
|
|
559
|
+
result._maxiter = min(self._maxiter, other._maxiter)
|
|
560
|
+
else:
|
|
561
|
+
result._maxiter = self._maxiter
|
|
562
|
+
|
|
563
|
+
# 3) Eager path: both in RAM (or scalar/ndarray + self in RAM)
|
|
564
|
+
self_loaded = getattr(self, "_all_loaded", False)
|
|
565
|
+
other_loaded = isinstance(other, Diagnostic) and getattr(other, "_all_loaded", False)
|
|
566
|
+
if self_loaded and (other_loaded or not isinstance(other, Diagnostic)):
|
|
567
|
+
lhs = self._data
|
|
568
|
+
rhs = other._data if other_loaded else other
|
|
569
|
+
result._data = op_func(lhs, rhs)
|
|
570
|
+
result._all_loaded = True
|
|
783
571
|
return result
|
|
784
572
|
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
for attr in [
|
|
789
|
-
"_dx",
|
|
790
|
-
"_nx",
|
|
791
|
-
"_x",
|
|
792
|
-
"_dt",
|
|
793
|
-
"_grid",
|
|
794
|
-
"_axis",
|
|
795
|
-
"_dim",
|
|
796
|
-
"_ndump",
|
|
797
|
-
"_maxiter",
|
|
798
|
-
"_tunits",
|
|
799
|
-
"_type",
|
|
800
|
-
"_simulation_folder",
|
|
801
|
-
]:
|
|
802
|
-
if hasattr(self, attr):
|
|
803
|
-
setattr(result, attr, getattr(self, attr))
|
|
804
|
-
|
|
805
|
-
if not hasattr(result, "_maxiter") or result._maxiter is None:
|
|
806
|
-
if hasattr(self, "_maxiter") and self._maxiter is not None:
|
|
807
|
-
result._maxiter = self._maxiter
|
|
808
|
-
|
|
809
|
-
# result._name = self._name + " / " + str(other._name)
|
|
810
|
-
|
|
811
|
-
if self._all_loaded:
|
|
812
|
-
other.load_all()
|
|
813
|
-
result._data = self._data / other._data
|
|
814
|
-
result._all_loaded = True
|
|
815
|
-
else:
|
|
816
|
-
|
|
817
|
-
def gen_diag_div(original_gen1, original_gen2):
|
|
818
|
-
for val1, val2 in zip(original_gen1, original_gen2):
|
|
819
|
-
yield val1 / val2
|
|
573
|
+
def _wrap(arr):
|
|
574
|
+
return lambda idx: (arr[idx],)
|
|
820
575
|
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
return result
|
|
576
|
+
gen1 = _wrap(self._data) if self_loaded else self._data_generator
|
|
577
|
+
if isinstance(other, Diagnostic):
|
|
578
|
+
gen2 = _wrap(other._data) if other_loaded else other._data_generator
|
|
579
|
+
else:
|
|
580
|
+
gen2 = other # scalar or ndarray
|
|
828
581
|
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
for attr in [
|
|
835
|
-
"_dx",
|
|
836
|
-
"_nx",
|
|
837
|
-
"_x",
|
|
838
|
-
"_dt",
|
|
839
|
-
"_grid",
|
|
840
|
-
"_axis",
|
|
841
|
-
"_dim",
|
|
842
|
-
"_ndump",
|
|
843
|
-
"_maxiter",
|
|
844
|
-
"_tunits",
|
|
845
|
-
"_type",
|
|
846
|
-
"_simulation_folder",
|
|
847
|
-
]:
|
|
848
|
-
if hasattr(self, attr):
|
|
849
|
-
setattr(result, attr, getattr(self, attr))
|
|
850
|
-
|
|
851
|
-
if not hasattr(result, "_maxiter") or result._maxiter is None:
|
|
852
|
-
if hasattr(self, "_maxiter") and self._maxiter is not None:
|
|
853
|
-
result._maxiter = self._maxiter
|
|
854
|
-
|
|
855
|
-
# result._name = self._name + " ^(" + str(other) + ")"
|
|
856
|
-
# result._label = self._label + rf"$ ^{other}$"
|
|
857
|
-
|
|
858
|
-
if self._all_loaded:
|
|
859
|
-
result._data = self._data**other
|
|
860
|
-
result._all_loaded = True
|
|
582
|
+
def _make_gen(idx):
|
|
583
|
+
seq1 = gen1(idx)
|
|
584
|
+
if callable(gen2):
|
|
585
|
+
seq2 = gen2(idx)
|
|
586
|
+
return (op_func(a, b) for a, b in zip(seq1, seq2))
|
|
861
587
|
else:
|
|
588
|
+
return (op_func(a, gen2) for a in seq1)
|
|
862
589
|
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
590
|
+
result._data_generator = _make_gen
|
|
591
|
+
result._all_loaded = False
|
|
592
|
+
return result
|
|
866
593
|
|
|
867
|
-
|
|
868
|
-
result._data_generator = lambda index: gen_scalar_pow(original_generator(index), other)
|
|
869
|
-
|
|
870
|
-
result.created_diagnostic_name = "MISC"
|
|
871
|
-
|
|
872
|
-
return result
|
|
594
|
+
# Now define each operator in one line:
|
|
873
595
|
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
raise ValueError("Power by another diagnostic is not supported. Why would you do that?")
|
|
596
|
+
def __add__(self, other: Union["Diagnostic", int, float, np.ndarray]) -> Diagnostic:
|
|
597
|
+
return self._binary_op(other, operator.add)
|
|
877
598
|
|
|
878
|
-
def __radd__(self, other):
|
|
599
|
+
def __radd__(self, other: Union["Diagnostic", int, float, np.ndarray]) -> Diagnostic:
|
|
879
600
|
return self + other
|
|
880
601
|
|
|
881
|
-
def
|
|
882
|
-
return
|
|
602
|
+
def __sub__(self, other: Union["Diagnostic", int, float, np.ndarray]) -> Diagnostic:
|
|
603
|
+
return self._binary_op(other, operator.sub)
|
|
883
604
|
|
|
884
|
-
def
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
def __rtruediv__(self, other): # division is not commutative
|
|
888
|
-
if isinstance(other, (int, float, np.ndarray)):
|
|
889
|
-
result = Diagnostic(species=self._species)
|
|
890
|
-
|
|
891
|
-
for attr in [
|
|
892
|
-
"_dx",
|
|
893
|
-
"_nx",
|
|
894
|
-
"_x",
|
|
895
|
-
"_dt",
|
|
896
|
-
"_grid",
|
|
897
|
-
"_axis",
|
|
898
|
-
"_dim",
|
|
899
|
-
"_ndump",
|
|
900
|
-
"_maxiter",
|
|
901
|
-
"_tunits",
|
|
902
|
-
"_type",
|
|
903
|
-
"_simulation_folder",
|
|
904
|
-
]:
|
|
905
|
-
if hasattr(self, attr):
|
|
906
|
-
setattr(result, attr, getattr(self, attr))
|
|
907
|
-
|
|
908
|
-
if not hasattr(result, "_maxiter") or result._maxiter is None:
|
|
909
|
-
if hasattr(self, "_maxiter") and self._maxiter is not None:
|
|
910
|
-
result._maxiter = self._maxiter
|
|
911
|
-
|
|
912
|
-
# result._name = str(other) + " / " + self._name if isinstance(other, (int, float)) else "np.ndarray / " + self._name
|
|
913
|
-
|
|
914
|
-
if self._all_loaded:
|
|
915
|
-
result._data = other / self._data
|
|
916
|
-
result._all_loaded = True
|
|
917
|
-
else:
|
|
918
|
-
|
|
919
|
-
def gen_scalar_rdiv(scalar, original_gen):
|
|
920
|
-
for val in original_gen:
|
|
921
|
-
yield scalar / val
|
|
605
|
+
def __rsub__(self, other: Union["Diagnostic", int, float, np.ndarray]) -> Diagnostic:
|
|
606
|
+
# swap args for reversed subtraction
|
|
607
|
+
return self._binary_op(other, lambda x, y: operator.sub(y, x))
|
|
922
608
|
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
result.created_diagnostic_name = "MISC"
|
|
927
|
-
|
|
928
|
-
return result
|
|
609
|
+
def __mul__(self, other: Union["Diagnostic", int, float, np.ndarray]) -> Diagnostic:
|
|
610
|
+
return self._binary_op(other, operator.mul)
|
|
929
611
|
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
for attr in [
|
|
934
|
-
"_dx",
|
|
935
|
-
"_nx",
|
|
936
|
-
"_x",
|
|
937
|
-
"_dt",
|
|
938
|
-
"_grid",
|
|
939
|
-
"_axis",
|
|
940
|
-
"_dim",
|
|
941
|
-
"_ndump",
|
|
942
|
-
"_maxiter",
|
|
943
|
-
"_tunits",
|
|
944
|
-
"_type",
|
|
945
|
-
"_simulation_folder",
|
|
946
|
-
]:
|
|
947
|
-
if hasattr(self, attr):
|
|
948
|
-
setattr(result, attr, getattr(self, attr))
|
|
949
|
-
|
|
950
|
-
if not hasattr(result, "_maxiter") or result._maxiter is None:
|
|
951
|
-
if hasattr(self, "_maxiter") and self._maxiter is not None:
|
|
952
|
-
result._maxiter = self._maxiter
|
|
953
|
-
|
|
954
|
-
# result._name = str(other._name) + " / " + self._name
|
|
955
|
-
|
|
956
|
-
if self._all_loaded:
|
|
957
|
-
other.load_all()
|
|
958
|
-
result._data = other._data / self._data
|
|
959
|
-
result._all_loaded = True
|
|
960
|
-
else:
|
|
612
|
+
def __rmul__(self, other: Union["Diagnostic", int, float, np.ndarray]) -> Diagnostic:
|
|
613
|
+
return self * other
|
|
961
614
|
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
yield val2 / val1
|
|
615
|
+
def __truediv__(self, other: Union["Diagnostic", int, float, np.ndarray]) -> Diagnostic:
|
|
616
|
+
return self._binary_op(other, operator.truediv)
|
|
965
617
|
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
result._data_generator = lambda index: gen_diag_div(original_generator(index), other_generator(index))
|
|
618
|
+
def __rtruediv__(self, other: Union["Diagnostic", int, float, np.ndarray]) -> Diagnostic:
|
|
619
|
+
return self._binary_op(other, lambda x, y: operator.truediv(y, x))
|
|
969
620
|
|
|
970
|
-
|
|
621
|
+
def __neg__(self) -> Diagnostic:
|
|
622
|
+
# unary minus as multiplication by -1
|
|
623
|
+
return self._binary_op(-1, operator.mul)
|
|
971
624
|
|
|
972
|
-
|
|
625
|
+
def __pow__(self, other: Union["Diagnostic", int, float, np.ndarray]) -> Diagnostic:
|
|
626
|
+
"""
|
|
627
|
+
Power operation. Raises the diagnostic data to the power of `other`.
|
|
628
|
+
If `other` is a Diagnostic, it raises each timestep's data to the corresponding timestep's power.
|
|
629
|
+
If `other` is a scalar or ndarray, it raises all data to that power.
|
|
630
|
+
"""
|
|
631
|
+
return self._binary_op(other, operator.pow)
|
|
973
632
|
|
|
974
|
-
def to_h5(
|
|
633
|
+
def to_h5(
|
|
634
|
+
self,
|
|
635
|
+
savename: Optional[str] = None,
|
|
636
|
+
index: Optional[Union[int, List[int]]] = None,
|
|
637
|
+
all: bool = False,
|
|
638
|
+
verbose: bool = False,
|
|
639
|
+
path: Optional[str] = None,
|
|
640
|
+
) -> None:
|
|
975
641
|
"""
|
|
976
642
|
Save the diagnostic data to HDF5 files.
|
|
977
643
|
|
|
@@ -995,7 +661,7 @@ class Diagnostic:
|
|
|
995
661
|
self._save_path = path
|
|
996
662
|
# Check if is has attribute created_diagnostic_name or postprocess_name
|
|
997
663
|
if savename is None:
|
|
998
|
-
|
|
664
|
+
logger.warning(f"No savename provided. Using {self._name}.")
|
|
999
665
|
savename = self._name
|
|
1000
666
|
|
|
1001
667
|
if hasattr(self, "created_diagnostic_name"):
|
|
@@ -1008,10 +674,10 @@ class Diagnostic:
|
|
|
1008
674
|
if not os.path.exists(self._save_path):
|
|
1009
675
|
os.makedirs(self._save_path)
|
|
1010
676
|
if verbose:
|
|
1011
|
-
|
|
677
|
+
logger.info(f"Created folder {self._save_path}")
|
|
1012
678
|
|
|
1013
679
|
if verbose:
|
|
1014
|
-
|
|
680
|
+
logger.info(f"Save Path: {self._save_path}")
|
|
1015
681
|
|
|
1016
682
|
def savefile(filename, i):
|
|
1017
683
|
with h5py.File(filename, "w") as f:
|
|
@@ -1061,16 +727,18 @@ class Diagnostic:
|
|
|
1061
727
|
axis_dataset.attrs.create("TYPE", [np.bytes_("linear".encode())])
|
|
1062
728
|
|
|
1063
729
|
if verbose:
|
|
1064
|
-
|
|
730
|
+
logger.info(f"File created: {filename}")
|
|
1065
731
|
|
|
1066
|
-
|
|
732
|
+
logger.info(
|
|
733
|
+
f"The savename of the diagnostic is {savename}. Files will be saves as {savename}-000001.h5, {savename}-000002.h5, etc."
|
|
734
|
+
)
|
|
1067
735
|
|
|
1068
|
-
|
|
736
|
+
logger.info("If you desire a different name, please set it with the 'name' method (setter).")
|
|
1069
737
|
|
|
1070
738
|
if self._name is None:
|
|
1071
739
|
raise ValueError("Diagnostic name is not set. Cannot save to HDF5.")
|
|
1072
740
|
if not os.path.exists(path):
|
|
1073
|
-
|
|
741
|
+
logger.info(f"Creating folder {path}...")
|
|
1074
742
|
os.makedirs(path)
|
|
1075
743
|
if not os.path.isdir(path):
|
|
1076
744
|
raise ValueError(f"{path} is not a directory.")
|
|
@@ -1097,6 +765,10 @@ class Diagnostic:
|
|
|
1097
765
|
boundaries: np.ndarray = None,
|
|
1098
766
|
):
|
|
1099
767
|
"""
|
|
768
|
+
*****************************************************************************************************
|
|
769
|
+
THIS SHOULD BE REMOVED FROM THE BASE CLASS AND MOVED TO A SEPARATED CLASS DESIGNATED FOR THIS PURPOSE
|
|
770
|
+
*****************************************************************************************************
|
|
771
|
+
|
|
1100
772
|
Plots a 3D scatter plot of the diagnostic data (grid data).
|
|
1101
773
|
|
|
1102
774
|
Parameters
|
|
@@ -1225,97 +897,109 @@ class Diagnostic:
|
|
|
1225
897
|
|
|
1226
898
|
return fig, ax
|
|
1227
899
|
|
|
900
|
+
def __str__(self):
|
|
901
|
+
"""String representation of the diagnostic."""
|
|
902
|
+
return f"Diagnostic: {self._name}, Species: {self._species}, Quantity: {self._quantity}"
|
|
903
|
+
|
|
904
|
+
def __repr__(self):
|
|
905
|
+
"""Detailed string representation of the diagnostic."""
|
|
906
|
+
return (
|
|
907
|
+
f"Diagnostic(species={self._species}, name={self._name}, quantity={self._quantity}, "
|
|
908
|
+
f"dim={self._dim}, maxiter={self._maxiter}, all_loaded={self._all_loaded})"
|
|
909
|
+
)
|
|
910
|
+
|
|
1228
911
|
# Getters
|
|
1229
912
|
@property
|
|
1230
|
-
def data(self):
|
|
913
|
+
def data(self) -> np.ndarray:
|
|
1231
914
|
if self._data is None:
|
|
1232
915
|
raise ValueError("Data not loaded into memory. Use get_* method with load_all=True or access via generator/index.")
|
|
1233
916
|
return self._data
|
|
1234
917
|
|
|
1235
918
|
@property
|
|
1236
|
-
def dx(self):
|
|
919
|
+
def dx(self) -> float:
|
|
1237
920
|
return self._dx
|
|
1238
921
|
|
|
1239
922
|
@property
|
|
1240
|
-
def nx(self):
|
|
923
|
+
def nx(self) -> int | np.ndarray:
|
|
1241
924
|
return self._nx
|
|
1242
925
|
|
|
1243
926
|
@property
|
|
1244
|
-
def x(self):
|
|
927
|
+
def x(self) -> np.ndarray:
|
|
1245
928
|
return self._x
|
|
1246
929
|
|
|
1247
930
|
@property
|
|
1248
|
-
def dt(self):
|
|
931
|
+
def dt(self) -> float:
|
|
1249
932
|
return self._dt
|
|
1250
933
|
|
|
1251
934
|
@property
|
|
1252
|
-
def grid(self):
|
|
935
|
+
def grid(self) -> np.ndarray:
|
|
1253
936
|
return self._grid
|
|
1254
937
|
|
|
1255
938
|
@property
|
|
1256
|
-
def axis(self):
|
|
939
|
+
def axis(self) -> list[dict]:
|
|
1257
940
|
return self._axis
|
|
1258
941
|
|
|
1259
942
|
@property
|
|
1260
|
-
def units(self):
|
|
943
|
+
def units(self) -> str:
|
|
1261
944
|
return self._units
|
|
1262
945
|
|
|
1263
946
|
@property
|
|
1264
|
-
def tunits(self):
|
|
947
|
+
def tunits(self) -> str:
|
|
1265
948
|
return self._tunits
|
|
1266
949
|
|
|
1267
950
|
@property
|
|
1268
|
-
def name(self):
|
|
951
|
+
def name(self) -> str:
|
|
1269
952
|
return self._name
|
|
1270
953
|
|
|
1271
954
|
@property
|
|
1272
|
-
def dim(self):
|
|
955
|
+
def dim(self) -> int:
|
|
1273
956
|
return self._dim
|
|
1274
957
|
|
|
1275
958
|
@property
|
|
1276
|
-
def path(self):
|
|
1277
|
-
return self
|
|
959
|
+
def path(self) -> str:
|
|
960
|
+
return self._path
|
|
1278
961
|
|
|
1279
962
|
@property
|
|
1280
|
-
def simulation_folder(self):
|
|
963
|
+
def simulation_folder(self) -> str:
|
|
1281
964
|
return self._simulation_folder
|
|
1282
965
|
|
|
1283
966
|
@property
|
|
1284
|
-
def ndump(self):
|
|
967
|
+
def ndump(self) -> int:
|
|
1285
968
|
return self._ndump
|
|
1286
969
|
|
|
1287
|
-
# @property
|
|
1288
|
-
# def iter(self):
|
|
1289
|
-
# return self._iter
|
|
1290
|
-
|
|
1291
970
|
@property
|
|
1292
|
-
def all_loaded(self):
|
|
971
|
+
def all_loaded(self) -> bool:
|
|
1293
972
|
return self._all_loaded
|
|
1294
973
|
|
|
1295
974
|
@property
|
|
1296
|
-
def maxiter(self):
|
|
975
|
+
def maxiter(self) -> int:
|
|
1297
976
|
return self._maxiter
|
|
1298
977
|
|
|
1299
978
|
@property
|
|
1300
|
-
def label(self):
|
|
979
|
+
def label(self) -> str:
|
|
1301
980
|
return self._label
|
|
1302
981
|
|
|
1303
982
|
@property
|
|
1304
|
-
def type(self):
|
|
983
|
+
def type(self) -> str:
|
|
1305
984
|
return self._type
|
|
1306
985
|
|
|
1307
986
|
@property
|
|
1308
|
-
def quantity(self):
|
|
987
|
+
def quantity(self) -> str:
|
|
1309
988
|
return self._quantity
|
|
1310
989
|
|
|
1311
|
-
|
|
990
|
+
@property
|
|
991
|
+
def file_list(self) -> list[str] | None:
|
|
992
|
+
"""Return the cached list of HDF5 file paths (read-only)."""
|
|
993
|
+
return self._file_list
|
|
994
|
+
|
|
995
|
+
def time(self, index) -> list[float | str]:
|
|
1312
996
|
return [index * self._dt * self._ndump, self._tunits]
|
|
1313
997
|
|
|
1314
|
-
def attributes_to_save(self, index):
|
|
998
|
+
def attributes_to_save(self, index: int = 0) -> None:
|
|
1315
999
|
"""
|
|
1316
1000
|
Prints the attributes of the diagnostic.
|
|
1317
1001
|
"""
|
|
1318
|
-
|
|
1002
|
+
logger.info(
|
|
1319
1003
|
f"dt: {self._dt}\n"
|
|
1320
1004
|
f"dim: {self._dim}\n"
|
|
1321
1005
|
f"time: {self.time(index)[0]}\n"
|
|
@@ -1328,57 +1012,57 @@ class Diagnostic:
|
|
|
1328
1012
|
)
|
|
1329
1013
|
|
|
1330
1014
|
@dx.setter
|
|
1331
|
-
def dx(self, value):
|
|
1015
|
+
def dx(self, value: float) -> None:
|
|
1332
1016
|
self._dx = value
|
|
1333
1017
|
|
|
1334
1018
|
@nx.setter
|
|
1335
|
-
def nx(self, value):
|
|
1019
|
+
def nx(self, value: int | np.ndarray) -> None:
|
|
1336
1020
|
self._nx = value
|
|
1337
1021
|
|
|
1338
1022
|
@x.setter
|
|
1339
|
-
def x(self, value):
|
|
1023
|
+
def x(self, value: np.ndarray) -> None:
|
|
1340
1024
|
self._x = value
|
|
1341
1025
|
|
|
1342
1026
|
@dt.setter
|
|
1343
|
-
def dt(self, value):
|
|
1027
|
+
def dt(self, value: float) -> None:
|
|
1344
1028
|
self._dt = value
|
|
1345
1029
|
|
|
1346
1030
|
@grid.setter
|
|
1347
|
-
def grid(self, value):
|
|
1031
|
+
def grid(self, value: np.ndarray) -> None:
|
|
1348
1032
|
self._grid = value
|
|
1349
1033
|
|
|
1350
1034
|
@axis.setter
|
|
1351
|
-
def axis(self, value):
|
|
1035
|
+
def axis(self, value: list[dict]) -> None:
|
|
1352
1036
|
self._axis = value
|
|
1353
1037
|
|
|
1354
1038
|
@units.setter
|
|
1355
|
-
def units(self, value):
|
|
1039
|
+
def units(self, value: str) -> None:
|
|
1356
1040
|
self._units = value
|
|
1357
1041
|
|
|
1358
1042
|
@tunits.setter
|
|
1359
|
-
def tunits(self, value):
|
|
1043
|
+
def tunits(self, value: str) -> None:
|
|
1360
1044
|
self._tunits = value
|
|
1361
1045
|
|
|
1362
1046
|
@name.setter
|
|
1363
|
-
def name(self, value):
|
|
1047
|
+
def name(self, value: str) -> None:
|
|
1364
1048
|
self._name = value
|
|
1365
1049
|
|
|
1366
1050
|
@dim.setter
|
|
1367
|
-
def dim(self, value):
|
|
1051
|
+
def dim(self, value: int) -> None:
|
|
1368
1052
|
self._dim = value
|
|
1369
1053
|
|
|
1370
1054
|
@ndump.setter
|
|
1371
|
-
def ndump(self, value):
|
|
1055
|
+
def ndump(self, value: int) -> None:
|
|
1372
1056
|
self._ndump = value
|
|
1373
1057
|
|
|
1374
1058
|
@data.setter
|
|
1375
|
-
def data(self, value):
|
|
1059
|
+
def data(self, value: np.ndarray) -> None:
|
|
1376
1060
|
self._data = value
|
|
1377
1061
|
|
|
1378
1062
|
@quantity.setter
|
|
1379
|
-
def quantity(self, key):
|
|
1063
|
+
def quantity(self, key: str) -> None:
|
|
1380
1064
|
self._quantity = key
|
|
1381
1065
|
|
|
1382
1066
|
@label.setter
|
|
1383
|
-
def label(self, value):
|
|
1067
|
+
def label(self, value: str) -> None:
|
|
1384
1068
|
self._label = value
|