modusa 0.2.21__py3-none-any.whl → 0.2.23__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.
Files changed (60) hide show
  1. modusa/decorators.py +4 -4
  2. modusa/devtools/docs/source/generators/audio_waveforms.rst +8 -0
  3. modusa/devtools/docs/source/generators/base.rst +8 -0
  4. modusa/devtools/docs/source/generators/index.rst +8 -0
  5. modusa/devtools/docs/source/io/audio_loader.rst +8 -0
  6. modusa/devtools/docs/source/io/base.rst +8 -0
  7. modusa/devtools/docs/source/io/index.rst +8 -0
  8. modusa/devtools/docs/source/plugins/base.rst +8 -0
  9. modusa/devtools/docs/source/plugins/index.rst +7 -0
  10. modusa/devtools/docs/source/signals/audio_signal.rst +8 -0
  11. modusa/devtools/docs/source/signals/base.rst +8 -0
  12. modusa/devtools/docs/source/signals/frequency_domain_signal.rst +8 -0
  13. modusa/devtools/docs/source/signals/index.rst +11 -0
  14. modusa/devtools/docs/source/signals/spectrogram.rst +8 -0
  15. modusa/devtools/docs/source/signals/time_domain_signal.rst +8 -0
  16. modusa/devtools/docs/source/tools/audio_converter.rst +8 -0
  17. modusa/devtools/docs/source/tools/audio_player.rst +8 -0
  18. modusa/devtools/docs/source/tools/base.rst +8 -0
  19. modusa/devtools/docs/source/tools/fourier_tranform.rst +8 -0
  20. modusa/devtools/docs/source/tools/index.rst +13 -0
  21. modusa/devtools/docs/source/tools/math_ops.rst +8 -0
  22. modusa/devtools/docs/source/tools/plotter.rst +8 -0
  23. modusa/devtools/docs/source/tools/youtube_downloader.rst +8 -0
  24. modusa/devtools/generate_doc_source.py +96 -0
  25. modusa/devtools/generate_template.py +8 -8
  26. modusa/devtools/main.py +3 -2
  27. modusa/devtools/templates/test.py +2 -3
  28. modusa/devtools/templates/{engine.py → tool.py} +3 -8
  29. modusa/generators/__init__.py +0 -2
  30. modusa/generators/audio_waveforms.py +22 -13
  31. modusa/generators/base.py +1 -1
  32. modusa/io/__init__.py +1 -5
  33. modusa/io/audio_loader.py +3 -33
  34. modusa/main.py +0 -30
  35. modusa/signals/__init__.py +1 -5
  36. modusa/signals/audio_signal.py +181 -124
  37. modusa/signals/base.py +1 -8
  38. modusa/signals/frequency_domain_signal.py +140 -93
  39. modusa/signals/spectrogram.py +197 -98
  40. modusa/signals/time_domain_signal.py +177 -74
  41. modusa/tools/__init__.py +2 -0
  42. modusa/{io → tools}/audio_converter.py +12 -4
  43. modusa/tools/audio_player.py +114 -0
  44. modusa/tools/base.py +43 -0
  45. modusa/tools/fourier_tranform.py +24 -0
  46. modusa/tools/math_ops.py +232 -0
  47. modusa/{io → tools}/plotter.py +155 -42
  48. modusa/{io → tools}/youtube_downloader.py +2 -2
  49. modusa/utils/excp.py +9 -42
  50. {modusa-0.2.21.dist-info → modusa-0.2.23.dist-info}/METADATA +2 -1
  51. modusa-0.2.23.dist-info/RECORD +70 -0
  52. modusa/engines/.DS_Store +0 -0
  53. modusa/engines/__init__.py +0 -3
  54. modusa/engines/base.py +0 -14
  55. modusa/io/audio_player.py +0 -72
  56. modusa/signals/signal_ops.py +0 -158
  57. modusa-0.2.21.dist-info/RECORD +0 -47
  58. {modusa-0.2.21.dist-info → modusa-0.2.23.dist-info}/WHEEL +0 -0
  59. {modusa-0.2.21.dist-info → modusa-0.2.23.dist-info}/entry_points.txt +0 -0
  60. {modusa-0.2.21.dist-info → modusa-0.2.23.dist-info}/licenses/LICENSE.md +0 -0
@@ -4,6 +4,7 @@
4
4
  from modusa import excp
5
5
  from modusa.decorators import immutable_property, validate_args_type
6
6
  from modusa.signals.base import ModusaSignal
7
+ from modusa.tools.math_ops import MathOps
7
8
  from typing import Self, Any
8
9
  import numpy as np
9
10
  import matplotlib.pyplot as plt
@@ -33,35 +34,30 @@ class Spectrogram(ModusaSignal):
33
34
  #----------------------------------
34
35
 
35
36
  @validate_args_type()
36
- def __init__(self, S: np.ndarray, f: np.ndarray, t: np.ndarray, title: str | None = None):
37
- super().__init__() # Instantiating `ModusaSignal` class
37
+ def __init__(self, S: np.ndarray, f: np.ndarray, frame_rate: float, t0: float = 0.0, title: str | None = None):
38
+ super().__init__()
38
39
 
39
40
  if S.ndim != 2:
40
- raise excp.InputValueError(f"`S` must have 2 dimension, got {S.ndim}.")
41
+ raise excp.InputValueError(f"`S` must have 2 dimensions, got {S.ndim}.")
41
42
  if f.ndim != 1:
42
43
  raise excp.InputValueError(f"`f` must have 1 dimension, got {f.ndim}.")
43
- if t.ndim != 1:
44
- raise excp.InputValueError(f"`t` must have 1 dimension, got {t.ndim}.")
45
-
46
- if t.shape[0] != S.shape[1] or f.shape[0] != S.shape[0]:
47
- raise excp.InputValueError(f"`f` and `t` shape do not match with `S` {S.shape}, got {(f.shape[0], t.shape[0])}")
48
-
44
+ if f.shape[0] != S.shape[0]:
45
+ raise excp.InputValueError(
46
+ f"Shape mismatch between `S` and `f`: expected {S.shape[0]}, got {f.shape[0]}"
47
+ )
49
48
  if S.shape[1] == 0:
50
- raise excp.InputValueError("`S` must have at least one time frame")
49
+ raise excp.InputValueError("`S` must have at least one time frame.")
51
50
 
52
- if t.shape[0] >= 2:
53
- dts = np.diff(t)
54
- if not np.allclose(dts, dts[0]):
55
- raise excp.InputValueError("`t` must be equally spaced")
56
-
57
51
  self._S = S
58
52
  self._f = f
59
- self._t = t
53
+ self._frame_rate = float(frame_rate)
54
+ self._t0 = float(t0)
60
55
  self.title = title or self._name
61
-
56
+
62
57
  #----------------------
63
58
  # Properties
64
59
  #----------------------
60
+
65
61
  @immutable_property("Create a new object instead.")
66
62
  def S(self) -> np.ndarray:
67
63
  """Spectrogram matrix (freq × time)."""
@@ -72,10 +68,25 @@ class Spectrogram(ModusaSignal):
72
68
  """Frequency axis."""
73
69
  return self._f
74
70
 
71
+ @immutable_property("Create a new object instead.")
72
+ def frame_rate(self) -> np.ndarray:
73
+ """Frequency axis."""
74
+ return self._frame_rate
75
+
76
+ @immutable_property("Create a new object instead.")
77
+ def t0(self) -> np.ndarray:
78
+ """Frequency axis."""
79
+ return self._t0
80
+
81
+ #----------------------
82
+ # Derived Properties
83
+ #----------------------
84
+
75
85
  @immutable_property("Create a new object instead.")
76
86
  def t(self) -> np.ndarray:
77
87
  """Time axis."""
78
- return self._t
88
+ n_frames = self._S.shape[1]
89
+ return np.arange(n_frames) / self.frame_rate + self.t0
79
90
 
80
91
  @immutable_property("Read only property.")
81
92
  def shape(self) -> np.ndarray:
@@ -87,12 +98,51 @@ class Spectrogram(ModusaSignal):
87
98
  """Number of dimensions (always 2)."""
88
99
  return self.S.ndim
89
100
 
90
- @immutable_property("Mutation not allowed.")
91
- def info(self) -> None:
101
+ @property
102
+ def magnitude(self) -> "Spectrogram":
103
+ """Return a new Spectrogram with magnitude values."""
104
+ mag = np.abs(self.S)
105
+ return self.__class__(S=mag, f=self.f, frame_rate=self.frame_rate, title=self.title)
106
+
107
+ @property
108
+ def power(self) -> "Spectrogram":
109
+ """Return a new Spectrogram with power (magnitude squared)."""
110
+ power = np.abs(self.S) ** 2
111
+ return self.__class__(S=power, f=self.f, frame_rate=self.frame_rate, title=self.title)
112
+
113
+ @property
114
+ def angle(self) -> "Spectrogram":
115
+ """Return a new Spectrogram with phase angle (in radians)."""
116
+ angle = np.angle(self.S)
117
+ return self.__class__(S=angle, f=self.f, frame_rate=self.frame_rate, title=self.title)
118
+
119
+ @property
120
+ def real(self) -> "Spectrogram":
121
+ """Return a new Spectrogram with real part."""
122
+ return self.__class__(S=self.S.real, f=self.f, frame_rate=self.frame_rate, title=self.title)
123
+
124
+ @property
125
+ def imag(self) -> "Spectrogram":
126
+ """Return a new Spectrogram with imaginary part."""
127
+ return self.__class__(S=self.S.imag, f=self.f, frame_rate=self.frame_rate, title=self.title)
128
+
129
+ @property
130
+ def phase(self) -> "Spectrogram":
131
+ """Return a new Spectrogram with normalized phase."""
132
+ phase = self.S / (np.abs(self.S) + 1e-10) # Avoid division by zero
133
+ return self.__class__(S=phase, f=self.f, frame_rate=self.frame_rate, title=self.title)
134
+
135
+
136
+
137
+ #------------------------
138
+ # Useful tools
139
+ #------------------------
140
+
141
+ def print_info(self) -> None:
92
142
  """Print key information about the spectrogram signal."""
93
- time_resolution = self.t[1] - self.t[0]
143
+ time_resolution = 1.0 / self.frame_rate
94
144
  n_freq_bins = self.S.shape[0]
95
-
145
+
96
146
  # Estimate NFFT size
97
147
  nfft = (n_freq_bins - 1) * 2
98
148
 
@@ -100,15 +150,10 @@ class Spectrogram(ModusaSignal):
100
150
  print(f"{'Title':<20}: {self.title}")
101
151
  print(f"{'Kind':<20}: {self._name}")
102
152
  print(f"{'Shape':<20}: {self.S.shape} (freq bins × time frames)")
153
+ print(f"{'Frame Rate':<20}: {self.frame_rate} (frames / sec)")
103
154
  print(f"{'Time resolution':<20}: {time_resolution:.4f} sec ({time_resolution * 1000:.2f} ms)")
104
155
  print(f"{'Freq resolution':<20}: {(self.f[1] - self.f[0]):.2f} Hz")
105
156
  print("-"*50)
106
- #------------------------
107
-
108
-
109
- #------------------------
110
- # Useful tools
111
- #------------------------
112
157
 
113
158
  def __getitem__(self, key: tuple[int | slice, int | slice]) -> "Spectrogram | FrequencyDomainSignal | TimeDomainSignal":
114
159
  """
@@ -139,12 +184,7 @@ class Spectrogram(ModusaSignal):
139
184
  sliced_data = np.asarray(sliced_data).flatten()
140
185
  sliced_f = np.asarray(sliced_f)
141
186
  t0 = float(self.t[t_key])
142
- return FrequencyDomainSignal(
143
- spectrum=sliced_data,
144
- f=sliced_f,
145
- t0=t0,
146
- title=self.title + f" [t = {t0:.2f} sec]"
147
- )
187
+ return FrequencyDomainSignal(spectrum=sliced_data, f=sliced_f, t0=t0, title=self.title + f" [t = {t0:.2f} sec]")
148
188
 
149
189
  # Case 3: time slice at a single frequency (→ TimeDomainSignal)
150
190
  elif isinstance(f_key, int):
@@ -153,21 +193,11 @@ class Spectrogram(ModusaSignal):
153
193
  sr = 1.0 / np.mean(np.diff(self.t)) # assume uniform time axis
154
194
  t0 = float(self.t[0])
155
195
  f_val = float(self.f[f_key])
156
- return TimeDomainSignal(
157
- y=sliced_data,
158
- sr=sr,
159
- t0=t0,
160
- title=self.title + f" [f = {f_val:.2f} Hz]"
161
- )
196
+ return TimeDomainSignal(y=sliced_data, sr=sr, t0=t0, title=self.title + f" [f = {f_val:.2f} Hz]")
162
197
 
163
198
  # Case 4: 2D slice → Spectrogram
164
199
  else:
165
- return self.__class__(
166
- S=sliced_data,
167
- f=sliced_f,
168
- t=sliced_t,
169
- title=self.title
170
- )
200
+ return self.__class__(S=sliced_data, f=sliced_f, frame_rate=self.frame_rate, t0=sliced_t[0], title=self.title)
171
201
 
172
202
  raise TypeError("Expected 2D indexing: signal[f_idx, t_idx]")
173
203
 
@@ -215,12 +245,11 @@ class Spectrogram(ModusaSignal):
215
245
  cropped_f = f[f_mask]
216
246
  cropped_t = t[t_mask]
217
247
 
218
- return self.__class__(S=cropped_S, f=cropped_f, t=cropped_t, title=self.title)
248
+ return self.__class__(S=cropped_S, f=cropped_f, frame_rate=self.frame_rate, t0=cropped_t[0], title=self.title)
219
249
 
220
250
 
221
251
  def plot(
222
252
  self,
223
- log_compression_factor: int | float | None = None,
224
253
  ax: plt.Axes | None = None,
225
254
  cmap: str = "gray_r",
226
255
  title: str | None = None,
@@ -230,7 +259,10 @@ class Spectrogram(ModusaSignal):
230
259
  ylim: tuple[float, float] | None = None,
231
260
  xlim: tuple[float, float] | None = None,
232
261
  highlight: list[tuple[float, float, float, float]] | None = None,
262
+ vlines: list | None = None,
263
+ hlines: list | None = None,
233
264
  origin: str = "lower", # or "lower"
265
+ gamma: int | float | None = None,
234
266
  show_colorbar: bool = True,
235
267
  cax: plt.Axes | None = None,
236
268
  show_grid: bool = True,
@@ -284,7 +316,7 @@ class Spectrogram(ModusaSignal):
284
316
  matplotlib.figure.Figure
285
317
  The figure object containing the plot.
286
318
  """
287
- from modusa.io import Plotter
319
+ from modusa.tools.plotter import Plotter
288
320
 
289
321
  title = title or self.title
290
322
 
@@ -292,7 +324,6 @@ class Spectrogram(ModusaSignal):
292
324
  M=self.S,
293
325
  r=self.f,
294
326
  c=self.t,
295
- log_compression_factor=log_compression_factor,
296
327
  ax=ax,
297
328
  cmap=cmap,
298
329
  title=title,
@@ -302,7 +333,10 @@ class Spectrogram(ModusaSignal):
302
333
  rlim=ylim,
303
334
  clim=xlim,
304
335
  highlight=highlight,
336
+ vlines=vlines,
337
+ hlines=hlines,
305
338
  origin=origin,
339
+ gamma=gamma,
306
340
  show_colorbar=show_colorbar,
307
341
  cax=cax,
308
342
  show_grid=show_grid,
@@ -321,114 +355,179 @@ class Spectrogram(ModusaSignal):
321
355
 
322
356
  def __add__(self, other):
323
357
  other_data = other.S if isinstance(other, self.__class__) else other
324
- result = np.add(self.S, other_data)
325
- return self.__class__(S=result, f=self.f, t=self.t, title=self.title)
358
+ result = MathOps.add(self.S, other_data)
359
+ return self.__class__(S=result, f=self.f, frame_rate=self.frame_rate, t0=self.t0, title=self.title)
326
360
 
327
361
  def __radd__(self, other):
328
- result = np.add(other, self.S)
329
- return self.__class__(S=result, f=self.f, t=self.t, title=self.title)
362
+ result = MathOps.add(other, self.S)
363
+ return self.__class__(S=result, f=self.f, frame_rate=self.frame_rate, t0=self.t0, title=self.title)
330
364
 
331
365
  def __sub__(self, other):
332
366
  other_data = other.S if isinstance(other, self.__class__) else other
333
- result = np.subtract(self.S, other_data)
334
- return self.__class__(S=result, f=self.f, t=self.t, title=self.title)
367
+ result = MathOps.subtract(self.S, other_data)
368
+ return self.__class__(S=result, f=self.f, frame_rate=self.frame_rate, t0=self.t0, title=self.title)
335
369
 
336
370
  def __rsub__(self, other):
337
- result = np.subtract(other, self.S)
338
- return self.__class__(S=result, f=self.f, t=self.t, title=self.title)
371
+ result = MathOps.subtract(other, self.S)
372
+ return self.__class__(S=result, f=self.f, frame_rate=self.frame_rate, t0=self.t0, title=self.title)
339
373
 
340
374
  def __mul__(self, other):
341
375
  other_data = other.S if isinstance(other, self.__class__) else other
342
- result = np.multiply(self.S, other_data)
343
- return self.__class__(S=result, f=self.f, t=self.t, title=self.title)
376
+ result = MathOps.multiply(self.S, other_data)
377
+ return self.__class__(S=result, f=self.f, frame_rate=self.frame_rate, t0=self.t0, title=self.title)
344
378
 
345
379
  def __rmul__(self, other):
346
- result = np.multiply(other, self.S)
347
- return self.__class__(S=result, f=self.f, t=self.t, title=self.title)
380
+ result = MathOps.multiply(other, self.S)
381
+ return self.__class__(S=result, f=self.f, frame_rate=self.frame_rate, t0=self.t0, title=self.title)
348
382
 
349
383
  def __truediv__(self, other):
350
384
  other_data = other.S if isinstance(other, self.__class__) else other
351
- result = np.true_divide(self.S, other_data)
352
- return self.__class__(S=result, f=self.f, t=self.t, title=self.title)
385
+ result = MathOps.true_divide(self.S, other_data)
386
+ return self.__class__(S=result, f=self.f, frame_rate=self.frame_rate, t0=self.t0, title=self.title)
353
387
 
354
388
  def __rtruediv__(self, other):
355
- result = np.true_divide(other, self.S)
356
- return self.__class__(S=result, f=self.f, t=self.t, title=self.title)
389
+ result = MathOps.true_divide(other, self.S)
390
+ return self.__class__(S=result, f=self.f, frame_rate=self.frame_rate, t0=self.t0, title=self.title)
357
391
 
358
392
  def __floordiv__(self, other):
359
393
  other_data = other.S if isinstance(other, self.__class__) else other
360
- result = np.floor_divide(self.S, other_data)
361
- return self.__class__(S=result, f=self.f, t=self.t, title=self.title)
394
+ result = MathOps.floor_divide(self.S, other_data)
395
+ return self.__class__(S=result, f=self.f, frame_rate=self.frame_rate, t0=self.t0, title=self.title)
362
396
 
363
397
  def __rfloordiv__(self, other):
364
- result = np.floor_divide(other, self.S)
365
- return self.__class__(S=result, f=self.f, t=self.t, title=self.title)
398
+ result = MathOps.floor_divide(other, self.S)
399
+ return self.__class__(S=result, f=self.f, frame_rate=self.frame_rate, t0=self.t0, title=self.title)
366
400
 
367
401
  def __pow__(self, other):
368
402
  other_data = other.S if isinstance(other, self.__class__) else other
369
- result = np.power(self.S, other_data)
370
- return self.__class__(S=result, f=self.f, t=self.t, title=self.title)
403
+ result = MathOps.power(self.S, other_data)
404
+ return self.__class__(S=result, f=self.f, frame_rate=self.frame_rate, t0=self.t0, title=self.title)
371
405
 
372
406
  def __rpow__(self, other):
373
- result = np.power(other, self.S)
374
- return self.__class__(S=result, f=self.f, t=self.t, title=self.title)
407
+ result = MathOps.power(other, self.S)
408
+ return self.__class__(S=result, f=self.f, frame_rate=self.frame_rate, t0=self.t0, title=self.title)
375
409
 
376
410
  def __abs__(self):
377
- result = np.abs(self.S)
378
- return self.__class__(S=result, f=self.f, t=self.t, title=self.title)
411
+ result = MathOps.abs(self.S)
412
+ return self.__class__(S=result, f=self.f, frame_rate=self.frame_rate, t0=self.t0, title=self.title)
413
+
414
+ #--------------------------
415
+ # Other signal ops
416
+ #--------------------------
379
417
 
380
418
  def sin(self):
381
419
  """Element-wise sine of the spectrogram."""
382
- return self.__class__(S=np.sin(self.S), f=self.f, t=self.t, title=self.title)
420
+ result = MathOps.sin(self.S)
421
+ return self.__class__(S=result, f=self.f, frame_rate=self.frame_rate, t0=self.t0, title=self.title)
383
422
 
384
423
  def cos(self):
385
424
  """Element-wise cosine of the spectrogram."""
386
- return self.__class__(S=np.cos(self.S), f=self.f, t=self.t, title=self.title)
425
+ result = MathOps.cos(self.S)
426
+ return self.__class__(S=result, f=self.f, frame_rate=self.frame_rate, t0=self.t0, title=self.title)
387
427
 
388
428
  def exp(self):
389
429
  """Element-wise exponential of the spectrogram."""
390
- return self.__class__(S=np.exp(self.S), f=self.f, t=self.t, title=self.title)
430
+ result = MathOps.exp(self.S)
431
+ return self.__class__(S=result, f=self.f, frame_rate=self.frame_rate, t0=self.t0, title=self.title)
391
432
 
392
433
  def tanh(self):
393
434
  """Element-wise hyperbolic tangent of the spectrogram."""
394
- return self.__class__(S=np.tanh(self.S), f=self.f, t=self.t, title=self.title)
435
+ result = MathOps.tanh(self.S)
436
+ return self.__class__(S=result, f=self.f, frame_rate=self.frame_rate, t0=self.t0, title=self.title)
395
437
 
396
438
  def log(self):
397
439
  """Element-wise natural logarithm of the spectrogram."""
398
- return self.__class__(S=np.log(self.S), f=self.f, t=self.t, title=self.title)
440
+ result = MathOps.log(self.S)
441
+ return self.__class__(S=result, f=self.f, frame_rate=self.frame_rate, t0=self.t0, title=self.title)
399
442
 
400
443
  def log1p(self):
401
444
  """Element-wise log(1 + M) of the spectrogram."""
402
- return self.__class__(S=np.log1p(self.S), f=self.f, t=self.t, title=self.title)
445
+ result = MathOps.log1p(self.S)
446
+ return self.__class__(S=result, f=self.f, frame_rate=self.frame_rate, t0=self.t0, title=self.title)
403
447
 
404
448
  def log10(self):
405
449
  """Element-wise base-10 logarithm of the spectrogram."""
406
- return self.__class__(S=np.log10(self.S), f=self.f, t=self.t, title=self.title)
450
+ result = MathOps.log10(self.S)
451
+ return self.__class__(S=result, f=self.f, frame_rate=self.frame_rate, t0=self.t0, title=self.title)
407
452
 
408
453
  def log2(self):
409
454
  """Element-wise base-2 logarithm of the spectrogram."""
410
- return self.__class__(S=np.log2(self.S), f=self.f, t=self.t, title=self.title)
455
+ result = MathOps.log2(self.S)
456
+ return self.__class__(S=result, f=self.f, frame_rate=self.frame_rate, t0=self.t0, title=self.title)
411
457
 
458
+ #--------------------------
459
+ # Aggregation signal ops
460
+ #--------------------------
412
461
 
413
- def mean(self) -> float:
462
+ def mean(self, axis: int | None = None) -> float:
414
463
  """Return the mean of the spectrogram values."""
415
- return float(np.mean(self.S))
416
-
417
- def std(self) -> float:
464
+ from modusa.signals.time_domain_signal import TimeDomainSignal
465
+ from modusa.signals.frequency_domain_signal import FrequencyDomainSignal
466
+ result = MathOps.mean(self.S, axis=axis)
467
+ if axis == 0: # Aggregating across rows
468
+ return TimeDomainSignal(y=result, sr=self.frame_rate, t0=self.t0, title=self.title)
469
+ elif axis in [1, -1]:
470
+ return FrequencyDomainSignal(spectrum=result, f=self.f, t0=self.t0, title=self.title)
471
+ elif axis is None:
472
+ return result
473
+ else:
474
+ raise excp.InputValueError("Can't perform mean operation")
475
+
476
+ def std(self, axis: int | None = None) -> float:
418
477
  """Return the standard deviation of the spectrogram values."""
419
- return float(np.std(self.S))
420
-
421
- def min(self) -> float:
478
+ from modusa.signals.time_domain_signal import TimeDomainSignal
479
+ from modusa.signals.frequency_domain_signal import FrequencyDomainSignal
480
+ result = MathOps.std(self.S, axis=axis)
481
+ if axis == 0: # Aggregating across rows
482
+ return TimeDomainSignal(y=result, sr=self.frame_rate, t0=self.t0, title=self.title)
483
+ elif axis in [1, -1]:
484
+ return FrequencyDomainSignal(spectrum=result, f=self.f, t0=self.t0, title=self.title)
485
+ elif axis is None:
486
+ return result
487
+ else:
488
+ raise excp.InputValueError("Can't perform std operation")
489
+
490
+ def min(self, axis: int | None = None) -> float:
422
491
  """Return the minimum value in the spectrogram."""
423
- return float(np.min(self.S))
424
-
425
- def max(self) -> float:
492
+ from modusa.signals.time_domain_signal import TimeDomainSignal
493
+ from modusa.signals.frequency_domain_signal import FrequencyDomainSignal
494
+ result = MathOps.min(self.S, axis=axis)
495
+ if axis == 0: # Aggregating across rows
496
+ return TimeDomainSignal(y=result, sr=self.frame_rate, t0=self.t0, title=self.title)
497
+ elif axis in [1, -1]:
498
+ return FrequencyDomainSignal(spectrum=result, f=self.f, t0=self.t0, title=self.title)
499
+ elif axis is None:
500
+ return result
501
+ else:
502
+ raise excp.InputValueError("Can't perform min operation")
503
+
504
+ def max(self, axis: int | None = None) -> float:
426
505
  """Return the maximum value in the spectrogram."""
427
- return float(np.max(self.S))
428
-
429
- def sum(self) -> float:
506
+ from modusa.signals.time_domain_signal import TimeDomainSignal
507
+ from modusa.signals.frequency_domain_signal import FrequencyDomainSignal
508
+ result = MathOps.max(self.S, axis=axis)
509
+ if axis == 0: # Aggregating across rows
510
+ return TimeDomainSignal(y=result, sr=self.frame_rate, t0=self.t0, title=self.title)
511
+ elif axis in [1, -1]:
512
+ return FrequencyDomainSignal(spectrum=result, f=self.f, t0=self.t0, title=self.title)
513
+ elif axis is None:
514
+ return result
515
+ else:
516
+ raise excp.InputValueError("Can't perform max operation")
517
+
518
+ def sum(self, axis: int | None = None) -> float:
430
519
  """Return the sum of the spectrogram values."""
431
- return float(np.sum(self.S))
520
+ from modusa.signals.time_domain_signal import TimeDomainSignal
521
+ from modusa.signals.frequency_domain_signal import FrequencyDomainSignal
522
+ result = MathOps.sum(self.S, axis=axis)
523
+ if axis == 0: # Aggregating across rows
524
+ return TimeDomainSignal(y=result, sr=self.frame_rate, t0=self.t0, title=self.title)
525
+ elif axis in [1, -1]:
526
+ return FrequencyDomainSignal(spectrum=result, f=self.f, t0=self.t0, title=self.title)
527
+ elif axis is None:
528
+ return result
529
+ else:
530
+ raise excp.InputValueError("Can't perform sum operation")
432
531
 
433
532
  #-----------------------------------
434
533
  # Repr