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.
@@ -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 Literal
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
- def get_quantity(self, quantity):
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 _get_moment(self, species, moment):
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._file_template = glob.glob(f"{self._path}/*.h5")[0][:-9]
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._file_template = glob.glob(f"{self._path}/*.h5")[0][:-9]
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._file_template = glob.glob(f"{self._path}/*.h5")[0][:-9]
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._file_template = glob.glob(f"{self._path}/*.h5")[0][:-9]
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
- self._ndump = int(input_deck["time_step"][0]["ndump"])
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
- def _data_generator(self, index):
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
- file = os.path.join(self._file_template + f"{index:06d}.h5")
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 the attribute data.
371
+ The data for all iterations. Also stored in self._data.
323
372
  """
324
- # If data is already loaded, don't do anything
325
- if self._all_loaded and self._data is not None:
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
- # If this is a derived diagnostic without files
330
- if hasattr(self, "postprocess_name") or hasattr(self, "created_diagnostic_name"):
331
- # If it has a data generator but no direct files
332
- try:
333
- print("This appears to be a derived diagnostic. Loading data from generators...")
334
- # Get the maximum size from the diagnostic attributes
335
- if hasattr(self, "_maxiter") and self._maxiter is not None:
336
- size = self._maxiter
337
- else:
338
- # Try to infer from a related diagnostic
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 ValueError(f"Could not load derived diagnostic data: {str(e)}")
395
+ raise RuntimeError(f"Error loading timestep {i}: {e}")
353
396
 
354
- # Original implementation for file-based diagnostics
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
- print("Unloading data from memory.")
405
+ logger.info("Unloading data from memory.")
366
406
  if self._all_loaded is False:
367
- print("Data is not loaded.")
407
+ logger.warning("Data is not loaded.")
368
408
  return
369
409
  self._data = None
370
410
  self._all_loaded = False
371
411
 
372
- def load(self, index):
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
- Load data for a given index into memory. Not recommended. Use load_all for all data or access via generator or index for better performance.
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
- self._data = next(self._data_generator(index))
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
- has_gen = callable(data_gen)
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 has_gen:
429
- try:
430
- return next(data_gen(index))
431
- except Exception as e:
432
- raise RuntimeError(f"Error loading data at index {index}: {e}")
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
- elif isinstance(index, slice):
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
- if has_gen:
441
- data_list = []
442
- for i in indices:
443
- try:
444
- data_list.append(next(data_gen(i)))
445
- except Exception as e:
446
- raise RuntimeError(f"Error loading slice at index {i}: {e}")
447
- return np.stack(data_list)
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
- raise ValueError(f"Cannot retrieve data for index {index}. No data loaded and no generator available.")
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
- print(f"Warning: Could not determine iteration count for iteration, using {max_iter}.")
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 __add__(self, other):
481
- if isinstance(other, (int, float, np.ndarray)):
482
- result = Diagnostic(species=self._species)
483
-
484
- for attr in [
485
- "_dx",
486
- "_nx",
487
- "_x",
488
- "_dt",
489
- "_grid",
490
- "_axis",
491
- "_dim",
492
- "_ndump",
493
- "_maxiter",
494
- "_tunits",
495
- "_type",
496
- "_simulation_folder",
497
- ]:
498
- if hasattr(self, attr):
499
- setattr(result, attr, getattr(self, attr))
500
-
501
- # Make sure _maxiter is set even for derived diagnostics
502
- if not hasattr(result, "_maxiter") or result._maxiter is None:
503
- if hasattr(self, "_maxiter") and self._maxiter is not None:
504
- result._maxiter = self._maxiter
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
- elif isinstance(other, Diagnostic):
786
- result = Diagnostic(species=self._species)
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
- original_generator = self._data_generator
822
- other_generator = other._data_generator
823
- result._data_generator = lambda index: gen_diag_div(original_generator(index), other_generator(index))
824
-
825
- result.created_diagnostic_name = "MISC"
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
- def __pow__(self, other):
830
- # power by scalar
831
- if isinstance(other, (int, float)):
832
- result = Diagnostic(species=self._species)
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
- def gen_scalar_pow(original_gen, scalar):
864
- for val in original_gen:
865
- yield val**scalar
590
+ result._data_generator = _make_gen
591
+ result._all_loaded = False
592
+ return result
866
593
 
867
- original_generator = self._data_generator
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
- # power by another diagnostic
875
- elif isinstance(other, Diagnostic):
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 __rsub__(self, other): # I don't know if this is correct because I'm not sure if the order of the subtraction is correct
882
- return -self + other
602
+ def __sub__(self, other: Union["Diagnostic", int, float, np.ndarray]) -> Diagnostic:
603
+ return self._binary_op(other, operator.sub)
883
604
 
884
- def __rmul__(self, other):
885
- return self * other
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
- original_generator = self._data_generator
924
- result._data_generator = lambda index: gen_scalar_rdiv(other, original_generator(index))
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
- elif isinstance(other, Diagnostic):
931
- result = Diagnostic(species=self._species)
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
- def gen_diag_div(original_gen1, original_gen2):
963
- for val1, val2 in zip(original_gen1, original_gen2):
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
- original_generator = self._data_generator
967
- other_generator = other._data_generator
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
- result.created_diagnostic_name = "MISC"
621
+ def __neg__(self) -> Diagnostic:
622
+ # unary minus as multiplication by -1
623
+ return self._binary_op(-1, operator.mul)
971
624
 
972
- return result
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(self, savename=None, index=None, all=False, verbose=False, path=None):
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
- print(f"No savename provided. Using {self._name}.")
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
- print(f"Created folder {self._save_path}")
677
+ logger.info(f"Created folder {self._save_path}")
1012
678
 
1013
679
  if verbose:
1014
- print(f"Save Path: {self._save_path}")
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
- print(f"File created: {filename}")
730
+ logger.info(f"File created: {filename}")
1065
731
 
1066
- print(f"The savename of the diagnostic is {savename}. Files will be saves as {savename}-000001.h5, {savename}-000002.h5, etc.")
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
- print("If you desire a different name, please set it with the 'name' method (setter).")
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
- print(f"Creating folder {path}...")
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
- def time(self, index):
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
- print(
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