pymodaq_data 5.0.3__tar.gz → 5.0.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. {pymodaq_data-5.0.3 → pymodaq_data-5.0.5}/.gitignore +2 -0
  2. {pymodaq_data-5.0.3 → pymodaq_data-5.0.5}/PKG-INFO +1 -1
  3. {pymodaq_data-5.0.3 → pymodaq_data-5.0.5}/_version.py +2 -2
  4. {pymodaq_data-5.0.3 → pymodaq_data-5.0.5}/src/pymodaq_data/data.py +268 -39
  5. {pymodaq_data-5.0.3 → pymodaq_data-5.0.5}/src/pymodaq_data/h5modules/data_saving.py +2 -2
  6. {pymodaq_data-5.0.3 → pymodaq_data-5.0.5}/src/pymodaq_data/numpy_func.py +14 -0
  7. {pymodaq_data-5.0.3 → pymodaq_data-5.0.5}/src/pymodaq_data/slicing.py +8 -5
  8. {pymodaq_data-5.0.3 → pymodaq_data-5.0.5}/LICENSE +0 -0
  9. {pymodaq_data-5.0.3 → pymodaq_data-5.0.5}/README.rst +0 -0
  10. {pymodaq_data-5.0.3 → pymodaq_data-5.0.5}/pyproject.toml +0 -0
  11. {pymodaq_data-5.0.3 → pymodaq_data-5.0.5}/src/pymodaq_data/__init__.py +0 -0
  12. {pymodaq_data-5.0.3 → pymodaq_data-5.0.5}/src/pymodaq_data/h5modules/__init__.py +0 -0
  13. {pymodaq_data-5.0.3 → pymodaq_data-5.0.5}/src/pymodaq_data/h5modules/backends.py +0 -0
  14. {pymodaq_data-5.0.3 → pymodaq_data-5.0.5}/src/pymodaq_data/h5modules/browsing.py +0 -0
  15. {pymodaq_data-5.0.3 → pymodaq_data-5.0.5}/src/pymodaq_data/h5modules/exporter.py +0 -0
  16. {pymodaq_data-5.0.3 → pymodaq_data-5.0.5}/src/pymodaq_data/h5modules/exporters/__init__.py +0 -0
  17. {pymodaq_data-5.0.3 → pymodaq_data-5.0.5}/src/pymodaq_data/h5modules/exporters/base.py +0 -0
  18. {pymodaq_data-5.0.3 → pymodaq_data-5.0.5}/src/pymodaq_data/h5modules/exporters/flimj.py +0 -0
  19. {pymodaq_data-5.0.3 → pymodaq_data-5.0.5}/src/pymodaq_data/h5modules/exporters/hyperspy.py +0 -0
  20. {pymodaq_data-5.0.3 → pymodaq_data-5.0.5}/src/pymodaq_data/h5modules/saving.py +0 -0
  21. {pymodaq_data-5.0.3 → pymodaq_data-5.0.5}/src/pymodaq_data/h5modules/utils.py +0 -0
  22. {pymodaq_data-5.0.3 → pymodaq_data-5.0.5}/src/pymodaq_data/icon.ico +0 -0
  23. {pymodaq_data-5.0.3 → pymodaq_data-5.0.5}/src/pymodaq_data/plotting/__init__.py +0 -0
  24. {pymodaq_data-5.0.3 → pymodaq_data-5.0.5}/src/pymodaq_data/plotting/plotter/plotter.py +0 -0
  25. {pymodaq_data-5.0.3 → pymodaq_data-5.0.5}/src/pymodaq_data/plotting/plotter/plotters/__init__.py +0 -0
  26. {pymodaq_data-5.0.3 → pymodaq_data-5.0.5}/src/pymodaq_data/plotting/plotter/plotters/matplotlib_plotters.py +0 -0
  27. {pymodaq_data-5.0.3 → pymodaq_data-5.0.5}/src/pymodaq_data/post_treatment/__init__.py +0 -0
  28. {pymodaq_data-5.0.3 → pymodaq_data-5.0.5}/src/pymodaq_data/post_treatment/process_to_scalar.py +0 -0
  29. {pymodaq_data-5.0.3 → pymodaq_data-5.0.5}/src/pymodaq_data/splash.png +0 -0
@@ -1,6 +1,8 @@
1
1
  # Compiled python modules.
2
2
  *.pyc
3
3
 
4
+ _version.py
5
+
4
6
  # Byte-compiled / optimized / DLL files
5
7
  __pycache__/
6
8
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: pymodaq_data
3
- Version: 5.0.3
3
+ Version: 5.0.5
4
4
  Summary: Modular Data Acquisition with Python
5
5
  Project-URL: Homepage, http://pymodaq.cnrs.fr
6
6
  Project-URL: Source, https://github.com/PyMoDAQ/PyMoDAQ
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '5.0.3'
16
- __version_tuple__ = version_tuple = (5, 0, 3)
15
+ __version__ = version = '5.0.5'
16
+ __version_tuple__ = version_tuple = (5, 0, 5)
@@ -20,6 +20,7 @@ import warnings
20
20
  from time import time
21
21
  import copy
22
22
  import pint
23
+ from pint.compat import upcast_type_map
23
24
 
24
25
  from multipledispatch import dispatch
25
26
 
@@ -158,6 +159,24 @@ class DataDistribution(BaseEnum):
158
159
  spread = 1
159
160
 
160
161
 
162
+ def _compute_slices_from_axis(axis: Axis, _slice, *ignored, is_index=True, **ignored_also):
163
+ if not is_index:
164
+ if isinstance(_slice, numbers.Number):
165
+ if not is_index:
166
+ _slice = axis.find_index(_slice)
167
+ elif _slice is Ellipsis:
168
+ return _slice
169
+ elif isinstance(_slice, slice):
170
+ if not (_slice.start is None and
171
+ _slice.stop is None and _slice.step is None):
172
+ start = axis.find_index(
173
+ _slice.start if _slice.start is not None else axis.get_data()[0])
174
+ stop = axis.find_index(
175
+ _slice.stop if _slice.stop is not None else axis.get_data()[-1])
176
+ _slice = slice(start, stop)
177
+ return _slice
178
+
179
+
161
180
  class Axis:
162
181
  """Object holding info and data about physical axis of some data
163
182
 
@@ -195,6 +214,7 @@ class Axis:
195
214
  super().__init__()
196
215
 
197
216
  self.iaxis: Axis = SpecialSlicersData(self, False)
217
+ self.vaxis: Axis = SpecialSlicersData(self, False, False)
198
218
 
199
219
  self._size = size
200
220
  self._data = None
@@ -212,13 +232,22 @@ class Axis:
212
232
  if (scaling is None or offset is None or size is None) and data is not None:
213
233
  self.get_scale_offset_from_data(data)
214
234
 
235
+ @staticmethod
236
+ def from_quantity(quantity: Q_[np.ndarray], label='axis', index=0) -> Axis:
237
+ return Axis(label, str(quantity.units), data=quantity.magnitude,
238
+ index=index)
239
+
215
240
  def copy(self):
216
241
  return copy.copy(self)
217
242
 
218
- def as_dwa(self) -> DataWithAxes:
219
- dwa = DataRaw(self.label, data=[self.get_data()],
243
+ def as_dwa(self, set_itself_as_axis=False) -> DataWithAxes:
244
+ dwa = DataRaw(self.label, units=self.units,
245
+ data=[self.get_data()],
220
246
  labels=[f'{self.label}_{self.units}'])
221
- dwa.create_missing_axes()
247
+ if not set_itself_as_axis:
248
+ dwa.create_missing_axes()
249
+ else:
250
+ dwa.axes = [self.copy()]
222
251
  return dwa
223
252
 
224
253
  @property
@@ -234,16 +263,54 @@ class Axis:
234
263
 
235
264
  @property
236
265
  def units(self) -> str:
237
- """str: get/set the units for this axis"""
266
+ """str: get/set the units for this axis without conversion (equivalent to
267
+ force_units)"""
238
268
  return self._units
239
269
 
240
270
  @units.setter
241
271
  def units(self, units: str):
242
- if not isinstance(units, str):
243
- raise TypeError('units for the Axis class should be a string')
272
+ self.force_units(units)
273
+
274
+ def force_units(self, units: str):
275
+ """ Change immediately the units to whatever else. Use this with care!"""
244
276
  units = check_units(units)
245
277
  self._units = units
246
278
 
279
+ def units_as(self, units: str, inplace=True, context: str = None, **context_kwargs):
280
+ if inplace:
281
+ #self._units = units
282
+ if context is None:
283
+ self.data = self.get_quantity().to(units).magnitude
284
+ else:
285
+ self.data = self.get_quantity().to(units, context, **context_kwargs).magnitude
286
+ self.force_units(units)
287
+ return
288
+ else:
289
+ if context is not None:
290
+ return Axis(self.label, units,
291
+ data=self.get_quantity().to(units, context, **context_kwargs).magnitude)
292
+ else:
293
+ return Axis(self.label, units,
294
+ data=self.get_quantity().to(units))
295
+
296
+ def to_reduced_units(self, inplace=False):
297
+ quantity = self.get_quantity().to_reduced_units()
298
+ if inplace:
299
+ self.data = quantity.magnitude
300
+ self.force_units(str(quantity.units))
301
+ else:
302
+ return Axis(self.label, units=str(quantity.units),
303
+ data=quantity.magnitude)
304
+
305
+ def to_base_units(self, inplace=False):
306
+ quantity = self.get_quantity().to_base_units()
307
+ if inplace:
308
+ self.data = quantity.magnitude
309
+ self.force_units(str(quantity.units))
310
+ else:
311
+ return Axis(self.label, units=str(quantity.units),
312
+ data=quantity.magnitude)
313
+
247
314
  @property
248
315
  def index(self) -> int:
249
316
  """int: get/set the index this axis corresponds to in a DataWithAxis object"""
@@ -260,7 +327,7 @@ class Axis:
260
327
  return self._data
261
328
 
262
329
  @data.setter
263
- def data(self, data: np.ndarray):
330
+ def data(self, data: Union[np.ndarray, Q_]):
264
331
  if data is not None:
265
332
  self._check_data_valid(data)
266
333
  self.get_scale_offset_from_data(data)
@@ -273,6 +340,10 @@ class Axis:
273
340
  """Convenience method to obtain the axis data (usually None because scaling and offset are used)"""
274
341
  return self._data if self._data is not None else self._linear_data(self.size)
275
342
 
343
+ def get_quantity(self) -> Q_:
344
+ """ Convenience method to obtain the numerical data as a quantity array"""
345
+ return Q_(self.get_data(), self.units)
346
+
276
347
  def get_data_at(self, indexes: Union[int, IterableType, slice]) -> np.ndarray:
277
348
  """ Get data at specified indexes
278
349
 
@@ -346,8 +417,10 @@ class Axis:
346
417
  elif index < 0:
347
418
  raise ValueError('index for the Axis class should be a positive integer')
348
419
 
349
- @staticmethod
350
- def _check_data_valid(data):
420
+ def _check_data_valid(self, data: Union[np.ndarray, Q_]):
421
+ if isinstance(data, Q_):
422
+ self.units = str(data.units)
423
+ data = data.magnitude
351
424
  if not isinstance(data, np.ndarray):
352
425
  raise TypeError(f'data for the Axis class should be a 1D numpy array')
353
426
  elif len(data.shape) != 1:
@@ -368,12 +441,14 @@ class Axis:
368
441
  def __len__(self):
369
442
  return self.size
370
443
 
371
- def _compute_slices(self, slices, *ignored, **ignored_also):
372
- return slices
444
+ def _compute_slices(self, _slice, *ignored, is_index=True, **ignored_also):
445
+ _slice = _compute_slices_from_axis(self, _slice, is_index=is_index)
446
+ return _slice, _slice
373
447
 
374
- def _slicer(self, _slice, *ignored, **ignored_also):
448
+ def _slicer(self, _slice, *ignored, is_index=True, **ignored_also):
375
449
  ax: Axis = copy.deepcopy(self)
376
- if isinstance(_slice, int):
450
+ _slice, _slice = self._compute_slices(_slice, is_index=is_index)
451
+ if isinstance(_slice, numbers.Number):
377
452
  ax.data = np.array([ax.get_data()[_slice]])
378
453
  return ax
379
454
  elif _slice is Ellipsis:
@@ -442,6 +517,11 @@ class Axis:
442
517
  else:
443
518
  return self.offset + self.size / 2 * self.scaling
444
519
 
520
+ def flip(self):
521
+ """ flip the direction of the axis"""
522
+ self.data = self.get_data()[::-1]
523
+
524
+
445
525
  def min(self):
446
526
  if self._data is not None:
447
527
  return np.min(self._data)
@@ -533,8 +613,9 @@ class DataBase(DataLowLevel, NDArrayOperatorsMixin):
533
613
  distribution: DataDistribution or str
534
614
  The distribution type of the data: uniform if distributed on a regular grid or spread if on
535
615
  specific unordered points
536
- data: list of ndarray
537
- The data the object is storing
616
+ data: list of ndarray or Quantities
617
+ The data the object is storing. In case of Quantities, the object units attribute will
618
+ be forced to the unit of this quantity, ignoring the units argument.
538
619
  labels: list of str
539
620
  The labels of the data nd-arrays
540
621
  origin: str
@@ -601,6 +682,13 @@ class DataBase(DataLowLevel, NDArrayOperatorsMixin):
601
682
 
602
683
  base_type = 'Data'
603
684
 
685
+ def __new__(cls, *args, **kwargs):
686
+ if not pint.compat.is_upcast_type(cls):
687
+ upcast_type_map.update({f'{cls.__module__}.{cls.__name__}': None})
688
+ # this will make sure, these
689
+ # objects are not wrapped by pint
690
+ return super().__new__(cls)
691
+
604
692
  def __init__(self, name: str,
605
693
  source: DataSource = None, dim: DataDim = None,
606
694
  distribution: DataDistribution = DataDistribution.uniform,
@@ -642,7 +730,8 @@ class DataBase(DataLowLevel, NDArrayOperatorsMixin):
642
730
  units = check_units(units)
643
731
  self.units_as(units, inplace=True)
644
732
 
645
- def units_as(self, units: str, inplace=True) -> 'DataBase':
733
+ def units_as(self, units: str, inplace=True, context: str = None,
734
+ **context_kwargs) -> 'DataBase':
646
735
  """ Set the object units to the new one (if possible)
647
736
 
648
737
  Parameters
@@ -653,11 +742,18 @@ class DataBase(DataLowLevel, NDArrayOperatorsMixin):
653
742
  default True.
654
743
  If True replace the data's arrays by array in the new units
655
744
  If False, return a new data object
745
+ context: str
746
+ See pint documentation
656
747
  """
657
748
  arrays = []
658
749
  try:
659
750
  for ind_array in range(len(self)):
660
- arrays.append(self.quantities[ind_array].m_as(units))
751
+ if context is None:
752
+ arrays.append(
753
+ self.quantities[ind_array].to(units).magnitude)
754
+ else:
755
+ arrays.append(
756
+ self.quantities[ind_array].to(units, context, **context_kwargs).magnitude)
661
757
 
662
758
  except pint.errors.DimensionalityError as e:
663
759
  raise DataUnitError(
@@ -678,6 +774,49 @@ class DataBase(DataLowLevel, NDArrayOperatorsMixin):
678
774
  """ Change immediately the units to whatever else. Use this with care!"""
679
775
  self._units = units
680
776
 
777
+ def value(self, units: str = None) -> float:
778
+ """Returns the underlying float value (of the first elt in the data list) if this data
779
+ holds only a float otherwise returns a mean of the underlying data
780
+
781
+ Parameters
782
+ ----------
783
+
784
+ units: str
785
+ if unit is compatible with self.units, convert the data to these new units before
786
+ getting the value
787
+
788
+
789
+ """
790
+ if self.length == 1 and self.size == 1:
791
+ if units is not None:
792
+ data = Q_(float(self.data[0][0]), self.units)
793
+ return data.m_as(units)
794
+ else:
795
+ return float(self.data[0][0])
796
+ else:
797
+ if units is not None:
798
+ data = Q_(float(np.mean(self.data[0])), self.units)
799
+ return data.m_as(units)
800
+ else:
801
+ return float(np.mean(self.data[0]))
802
+
803
+ def values(self, units: str = None) -> List[float]:
804
+ """Returns the underlying float value (for each data array in the data list) if this data
805
+ holds only a float otherwise returns a mean of the underlying data"""
806
+ if self.length == 1 and self.size == 1:
807
+ if units is not None:
808
+ return [float(Q_(data_array[0], self.units).m_as(units))
809
+ for data_array in self.data]
810
+ else:
811
+ return [float(data_array[0])
812
+ for data_array in self.data]
813
+ else:
814
+ if units is not None:
815
+ return [float(Q_(np.mean(data_array), self.units).m_as(units))
816
+ for data_array in self.data]
817
+ else:
818
+ return [float(np.mean(data_array)) for data_array in self.data]
819
+
681
820
  def as_dte(self, name: str = 'mydte') -> DataToExport:
682
821
  """Convenience method to wrap the DataWithAxes object into a DataToExport"""
683
822
  return DataToExport(name, data=[self])
@@ -745,6 +884,7 @@ class DataBase(DataLowLevel, NDArrayOperatorsMixin):
745
884
  units = dwa.units
746
885
  ufunc_results = [ufunc(*zipped, **kwargs) for zipped in list(zip(*elts))]
747
886
  if isinstance(ufunc_results[0], Q_):
887
+ ufunc_results = [ufunc_result.to_reduced_units() for ufunc_result in ufunc_results]
748
888
  units = str(ufunc_results[0].units)
749
889
  ufunc_results = [ufunc_result.magnitude for ufunc_result in ufunc_results]
750
890
  dwa.data = ufunc_results
@@ -838,7 +978,13 @@ class DataBase(DataLowLevel, NDArrayOperatorsMixin):
838
978
 
839
979
  def angle(self):
840
980
  """ Take the phase value of itself"""
841
- return np.angle(self)
981
+ dwa_angle = np.angle(self)
982
+ dwa_angle.force_units('rad')
983
+ return dwa_angle
984
+
985
+ def unwrap(self):
986
+ """ unwrap the underlying array (should be angles otherwise meaningless)"""
987
+ return np.unwrap(self)
842
988
 
843
989
  def real(self):
844
990
  """ Take the real part of itself"""
@@ -959,15 +1105,17 @@ class DataBase(DataLowLevel, NDArrayOperatorsMixin):
959
1105
  """Get the data by its index in the list, same as self[index]"""
960
1106
  return self.data[index]
961
1107
 
962
- @staticmethod
963
- def _check_data_type(data: List[np.ndarray]) -> List[np.ndarray]:
1108
+ def _check_data_type(self, data: List[Union[np.ndarray, Q_]]) -> List[np.ndarray]:
964
1109
  """make sure data is a list of nd-arrays"""
965
1110
  is_valid = True
966
1111
  if data is None:
967
1112
  is_valid = False
968
1113
  if not isinstance(data, list):
969
1114
  # try to transform the data to regular type
970
- if isinstance(data, np.ndarray):
1115
+ if isinstance(data, Q_):
1116
+ self.force_units(str(data.units))
1117
+ data = [data.magnitude]
1118
+ elif isinstance(data, np.ndarray):
971
1119
  warnings.warn(DataTypeWarning(f'Your data should be a list of numpy arrays not just a single numpy'
972
1120
  f' array, wrapping them with a list'))
973
1121
  data = [data]
@@ -980,12 +1128,16 @@ class DataBase(DataLowLevel, NDArrayOperatorsMixin):
980
1128
  if isinstance(data, list):
981
1129
  if len(data) == 0:
982
1130
  is_valid = False
983
- elif not isinstance(data[0], np.ndarray):
1131
+ elif not (isinstance(data[0], np.ndarray) or
1132
+ isinstance(data[0], Q_)):
984
1133
  is_valid = False
985
1134
  elif len(data[0].shape) == 0:
986
1135
  is_valid = False
987
1136
  if not is_valid:
988
1137
  raise TypeError(f'Data should be an non-empty list of non-empty numpy arrays')
1138
+ if isinstance(data[0], Q_):
1139
+ self.force_units(str(data[0].units))
1140
+ data = [array.magnitude for array in data]
989
1141
  return data
990
1142
 
991
1143
  def check_shape_from_data(self, data: List[np.ndarray]):
@@ -1050,7 +1202,7 @@ class DataBase(DataLowLevel, NDArrayOperatorsMixin):
1050
1202
  return self._data
1051
1203
 
1052
1204
  @data.setter
1053
- def data(self, data: List[np.ndarray]):
1205
+ def data(self, data: List[Union[np.ndarray, Q_]]):
1054
1206
  data = self._check_data_type(data)
1055
1207
  self._check_shape_dim_consistency(data)
1056
1208
  self._check_same_shape(data)
@@ -1661,8 +1813,11 @@ class DataWithAxes(DataBase):
1661
1813
 
1662
1814
  self.set_axes_manager(self.shape, axes=axes, nav_indexes=nav_indexes, **other_kwargs)
1663
1815
 
1664
- self.inav: Iterable[DataWithAxes] = SpecialSlicersData(self, True)
1665
- self.isig: Iterable[DataWithAxes] = SpecialSlicersData(self, False)
1816
+ self.inav = SpecialSlicersData(self, True)
1817
+ self.isig = SpecialSlicersData(self, False)
1818
+
1819
+ self.vnav = SpecialSlicersData(self, True, is_index=False)
1820
+ self.vsig = SpecialSlicersData(self, False, is_index=False)
1666
1821
 
1667
1822
  self.get_dim_from_data_axes() # in DataBase, dim is processed from the shape of data, but if axes are provided
1668
1823
  #then use get_dim_from axes
@@ -1848,7 +2003,32 @@ class DataWithAxes(DataBase):
1848
2003
  mean = np.array([mean])
1849
2004
  dat_mean.append(mean)
1850
2005
  return self.deepcopy_with_new_data(dat_mean, remove_axes_index=axis)
1851
-
2006
+
2007
+ def moment(self) -> Tuple[DataWithAxes, DataWithAxes]:
2008
+ """ returns the two first moments of the data over the axis
2009
+
2010
+ only valid for Data1D data
2011
+
2012
+ Returns
2013
+ -------
2014
+ DataCalculated: containing the moment of order 0 (mean)
2015
+ DataCalculated: containing the moment of order 1 (std)
2016
+ """
2017
+ if self.dim != DataDim.Data1D:
2018
+ raise DataDimError('the moment method is only valid for 1D data')
2019
+ arrays_mean = []
2020
+ arrays_std = []
2021
+ for data in self:
2022
+ mean, std = mutils.my_moment(self.axes[0].get_data(), data)
2023
+ arrays_mean.append(np.array([mean]))
2024
+ arrays_std.append(np.array([std]))
2025
+
2026
+ return (DataCalculated('mean', data=arrays_mean),
2027
+ DataCalculated('std', data=arrays_std))
2028
+
2029
+
2030
+
2031
+
1852
2032
  def sum(self, axis: int = 0) -> DataWithAxes:
1853
2033
  """Process the sum of the data on the specified axis and returns the new data
1854
2034
 
@@ -1862,7 +2042,7 @@ class DataWithAxes(DataBase):
1862
2042
  """
1863
2043
  dat_sum = []
1864
2044
  for dat in self.data:
1865
- dat_sum.append(np.sum(dat, axis=axis))
2045
+ dat_sum.append(np.atleast_1d(np.sum(dat, axis=axis)))
1866
2046
  return self.deepcopy_with_new_data(dat_sum, remove_axes_index=axis)
1867
2047
 
1868
2048
  def interp(self, new_axis_data: Union[Axis, np.ndarray], **kwargs) -> DataWithAxes:
@@ -1902,12 +2082,20 @@ class DataWithAxes(DataBase):
1902
2082
  labels=self.labels)
1903
2083
  return new_data
1904
2084
 
1905
- def ft(self, axis: int = 0) -> DataWithAxes:
2085
+ def ft(self, axis: int = 0, axis_label: str = None,
2086
+ axis_units: str = None, labels: List[str] = None) -> DataWithAxes:
1906
2087
  """Process the Fourier Transform of the data on the specified axis and returns the new data
1907
2088
 
1908
2089
  Parameters
1909
2090
  ----------
1910
2091
  axis: int
2092
+ Apply the FT on this axis index
2093
+ axis_label: str
2094
+ A new label for the FT computed axis
2095
+ axis_units: str
2096
+ New units (without conversion on top of the one from the FT) for the computed axis
2097
+ labels: List[str]
2098
+ list of string for new labels
1911
2099
 
1912
2100
  Returns
1913
2101
  -------
@@ -1924,19 +2112,34 @@ class DataWithAxes(DataBase):
1924
2112
  for dat in self.data:
1925
2113
  dat_ft.append(mutils.ft(dat, dim=axis))
1926
2114
  new_data = self.deepcopy_with_new_data(dat_ft)
2115
+ if labels is not None:
2116
+ new_data.labels = labels
1927
2117
  axis_obj = new_data.get_axis_from_index(axis)[0]
1928
2118
  axis_obj.data = omega_grid
1929
- axis_obj.label = f'ft({axis_obj.label})'
1930
- axis_obj.units = f'rad/{axis_obj.units}'
2119
+ if axis_label is not None:
2120
+ axis_obj.label = axis_label
2121
+ else:
2122
+ axis_obj.label = f'ft({axis_obj.label})'
2123
+ if axis_units is not None:
2124
+ axis_obj.force_units(axis_units)
2125
+ else:
2126
+ axis_obj.units = f'rad/{axis_obj.units}'
1931
2127
  return new_data
1932
2128
 
1933
- def ift(self, axis: int = 0) -> DataWithAxes:
2129
+ def ift(self, axis: int = 0, axis_label: str = None,
2130
+ axis_units: str = None, labels: List[str] = None) -> DataWithAxes:
1934
2131
  """Process the inverse Fourier Transform of the data on the specified axis and returns the
1935
2132
  new data
1936
2133
 
1937
2134
  Parameters
1938
2135
  ----------
1939
2136
  axis: int
2137
+ axis_label: str
2138
+ A new label for the FT computed axis
2139
+ axis_units: str
2140
+ New units (without conversion on top of the one from the FT) for the computed axis
2141
+ labels: List[str]
2142
+ list of string for new labels
1940
2143
 
1941
2144
  Returns
1942
2145
  -------
@@ -1953,10 +2156,18 @@ class DataWithAxes(DataBase):
1953
2156
  for dat in self.data:
1954
2157
  dat_ift.append(mutils.ift(dat, dim=axis))
1955
2158
  new_data = self.deepcopy_with_new_data(dat_ift)
2159
+ if labels is not None:
2160
+ new_data.labels = labels
1956
2161
  axis_obj = new_data.get_axis_from_index(axis)[0]
1957
2162
  axis_obj.data = omega_grid
1958
- axis_obj.label = f'ift({axis_obj.label})'
1959
- axis_obj.units = str(Unit(f'rad/({axis_obj.units})'))
2163
+ if axis_label is not None:
2164
+ axis_obj.label = axis_label
2165
+ else:
2166
+ axis_obj.label = f'ift({axis_obj.label})'
2167
+ if axis_units is not None:
2168
+ axis_obj.force_units(axis_units)
2169
+ else:
2170
+ axis_obj.units = str(Unit(f'rad/({axis_obj.units})'))
1960
2171
  return new_data
1961
2172
 
1962
2173
  def fit(self, function: Callable, initial_guess: IterableType, data_index: int = None,
@@ -2156,11 +2367,22 @@ class DataWithAxes(DataBase):
2156
2367
  axes.append(ax)
2157
2368
  self.axes = axes
2158
2369
 
2159
- def _compute_slices(self, slices, is_navigation=True):
2370
+ def _compute_slices(self, slices, is_navigation=True, is_index=True):
2160
2371
  """Compute the total slice to apply to the data
2161
2372
 
2162
2373
  Filling in Ellipsis when no slicing should be done
2374
+ Parameters
2375
+ ----------
2376
+ slices: List of slice
2377
+ is_navigation: bool
2378
+ is_index: bool
2379
+ if False, the slice are on the values of the underlying axes
2380
+ Returns
2381
+ -------
2382
+ list(slice): the computed slices as index (eventually for all axes)
2383
+ list(slice): a version as index of the input argument
2163
2384
  """
2385
+ _slices_as_index = []
2164
2386
  if isinstance(slices, numbers.Number) or isinstance(slices, slice):
2165
2387
  slices = [slices]
2166
2388
  if is_navigation:
@@ -2171,13 +2393,18 @@ class DataWithAxes(DataBase):
2171
2393
  slices = list(slices)
2172
2394
  for ind in range(len(self.shape)):
2173
2395
  if ind in indexes:
2174
- total_slices.append(slices.pop(0))
2396
+ _slice = slices.pop(0)
2397
+ if not is_index:
2398
+ axis = self.get_axis_from_index(ind)[0]
2399
+ _slice = _compute_slices_from_axis(axis, _slice, is_index=is_index)
2400
+ _slices_as_index.append(_slice)
2401
+ total_slices.append(_slice)
2175
2402
  elif len(total_slices) == 0:
2176
2403
  total_slices.append(Ellipsis)
2177
2404
  elif not (Ellipsis in total_slices and total_slices[-1] is Ellipsis):
2178
2405
  total_slices.append(slice(None))
2179
2406
  total_slices = tuple(total_slices)
2180
- return total_slices
2407
+ return total_slices, _slices_as_index
2181
2408
 
2182
2409
  def check_squeeze(self, total_slices: List[slice], is_navigation: bool):
2183
2410
 
@@ -2189,7 +2416,7 @@ class DataWithAxes(DataBase):
2189
2416
  do_squeeze = False
2190
2417
  return do_squeeze
2191
2418
 
2192
- def _slicer(self, slices, is_navigation=True):
2419
+ def _slicer(self, slices, is_navigation=True, is_index=True):
2193
2420
  """Apply a given slice to the data either navigation or signal dimension
2194
2421
 
2195
2422
  Parameters
@@ -2198,6 +2425,8 @@ class DataWithAxes(DataBase):
2198
2425
  the slices to apply to the data
2199
2426
  is_navigation: bool
2200
2427
  if True apply the slices to the navigation dimension else to the signal ones
2428
+ is_index: bool
2429
+ if True the slices are indexes otherwise the slices are axes values to be indexed first
2201
2430
 
2202
2431
  Returns
2203
2432
  -------
@@ -2205,10 +2434,10 @@ class DataWithAxes(DataBase):
2205
2434
  Object of the same type as the initial data, derived from DataWithAxes. But with lower
2206
2435
  data size due to the slicing and with eventually less axes.
2207
2436
  """
2208
-
2209
2437
  if isinstance(slices, numbers.Number) or isinstance(slices, slice):
2210
2438
  slices = [slices]
2211
- total_slices = self._compute_slices(slices, is_navigation)
2439
+
2440
+ total_slices, slices = self._compute_slices(slices, is_navigation, is_index=is_index)
2212
2441
 
2213
2442
  do_squeeze = self.check_squeeze(total_slices, is_navigation)
2214
2443
  new_arrays_data = [squeeze(dat[total_slices], do_squeeze) for dat in self.data]
@@ -737,7 +737,7 @@ class DataToExportSaver:
737
737
  def __init__(self, h5saver: Union[H5SaverLowLevel, Path, str]):
738
738
  if isinstance(h5saver, Path) or isinstance(h5saver, str):
739
739
  h5saver_tmp = H5SaverLowLevel()
740
- h5saver_tmp.init_file(addhoc_file_path=Path(h5saver))
740
+ h5saver_tmp.init_file(file_name=Path(h5saver))
741
741
  h5saver = h5saver_tmp
742
742
 
743
743
  self._h5saver = h5saver
@@ -748,7 +748,7 @@ class DataToExportSaver:
748
748
  return self._h5saver.get_node(where)
749
749
 
750
750
  def close(self):
751
- self._h5saver.close()
751
+ self.close_file()
752
752
 
753
753
  def close_file(self):
754
754
  self._h5saver.close_file()
@@ -84,11 +84,17 @@ def _min(dwa: 'DataWithAxes', *args, axis: Optional[Union[int, Iterable[int]]] =
84
84
  def _std(dwa: 'DataWithAxes', *args, axis: Optional[Union[int, Iterable[int]]] = None, **kwargs):
85
85
  return process_with_reduced_dimensions(np.std, dwa, *args, axis=axis, **kwargs)
86
86
 
87
+
87
88
  @implements("mean")
88
89
  def _mean(dwa: 'DataWithAxes', *args, axis: Optional[Union[int, Iterable[int]]] = None, **kwargs):
89
90
  return process_with_reduced_dimensions(np.mean, dwa, *args, axis=axis, **kwargs)
90
91
 
91
92
 
93
+ @implements("sum")
94
+ def _sum(dwa: 'DataWithAxes', *args, axis: Optional[Union[int, Iterable[int]]] = None, **kwargs):
95
+ return process_with_reduced_dimensions(np.sum, dwa, *args, axis=axis, **kwargs)
96
+
97
+
92
98
  # ************* FUNCTIONS that apply with units ********
93
99
 
94
100
  @implements('angle')
@@ -100,6 +106,14 @@ def _angle(dwa: 'DataWithAxes', *args, **kwargs):
100
106
  return dwa_func
101
107
 
102
108
 
109
+ @implements('unwrap')
110
+ def _unwrap(dwa: 'DataWithAxes', *args, **kwargs):
111
+ dwa_func = dwa.deepcopy_with_new_data(
112
+ data=[np.unwrap(array, *args, **kwargs) for array in dwa.data])
113
+ dwa_func.name += f"_{'unwrap'}"
114
+ return dwa_func
115
+
116
+
103
117
  @implements('real')
104
118
  def _real(dwa: 'DataWithAxes', *args, **kwargs):
105
119
 
@@ -13,12 +13,15 @@ if TYPE_CHECKING:
13
13
 
14
14
  class SpecialSlicers(object):
15
15
  """make it elegant to apply a slice to navigation or signal dimensions"""
16
- def __init__(self, obj: Union['DataWithAxes', 'Axis'], is_navigation):
16
+ def __init__(self, obj: Union['DataWithAxes', 'Axis'], is_navigation, is_index=True):
17
17
  self.is_navigation = is_navigation
18
+ self.is_index = is_index
18
19
  self.obj = obj
19
20
 
20
21
  def __getitem__(self, slices) -> Union['DataWithAxes', 'Axis']:
21
- return self.obj._slicer(slices, self.is_navigation)
22
+ if hasattr(self.obj, 'axes') and self.obj.distribution.name == 'spread' and not self.is_index:
23
+ raise NotImplementedError('Impossible to slice by value a spread data object')
24
+ return self.obj._slicer(slices, self.is_navigation, is_index=self.is_index)
22
25
 
23
26
 
24
27
  class SpecialSlicersData(SpecialSlicers):
@@ -26,7 +29,7 @@ class SpecialSlicersData(SpecialSlicers):
26
29
  def __setitem__(self, slices, data: Union[np.ndarray, 'DataWithAxes', 'Axis']):
27
30
  """x.__setitem__(slices, data) <==> x[slices]=data
28
31
  """
29
- slices = self.obj._compute_slices(slices, self.is_navigation)
32
+ total_slices, slices_index = self.obj._compute_slices(slices, self.is_navigation, is_index=self.is_index)
30
33
 
31
34
  if hasattr(self.obj, 'base_type') and self.obj.base_type == 'Axis':
32
35
  if isinstance(data, np.ndarray):
@@ -35,14 +38,14 @@ class SpecialSlicersData(SpecialSlicers):
35
38
  data_to_replace = data.get_data()
36
39
  if hasattr(self.obj, 'units') and self.obj.data is None:
37
40
  self.obj.create_linear_data(len(self.obj))
38
- self.obj.data[slices] = data_to_replace
41
+ self.obj.data[total_slices] = data_to_replace
39
42
  else:
40
43
  for ind in range(len(self.obj)):
41
44
  if isinstance(data, np.ndarray):
42
45
  data_to_replace = data
43
46
  else: # means it's a DataWithAxes
44
47
  data_to_replace = data[ind]
45
- self.obj[ind][slices] = data_to_replace
48
+ self.obj[ind][total_slices] = data_to_replace
46
49
 
47
50
  def __len__(self):
48
51
  return self.obj.axes_manager.sig_shape[0]
File without changes
File without changes