modusa 0.2.23__py3-none-any.whl → 0.3__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 (80) hide show
  1. modusa/.DS_Store +0 -0
  2. modusa/__init__.py +8 -1
  3. modusa/devtools/{generate_doc_source.py → generate_docs_source.py} +5 -5
  4. modusa/devtools/generate_template.py +5 -5
  5. modusa/devtools/main.py +3 -3
  6. modusa/devtools/templates/generator.py +1 -1
  7. modusa/devtools/templates/io.py +1 -1
  8. modusa/devtools/templates/{signal.py → model.py} +18 -11
  9. modusa/devtools/templates/plugin.py +1 -1
  10. modusa/generators/__init__.py +11 -1
  11. modusa/generators/audio.py +188 -0
  12. modusa/generators/audio_waveforms.py +1 -1
  13. modusa/generators/base.py +1 -1
  14. modusa/generators/ftds.py +298 -0
  15. modusa/generators/s1d.py +270 -0
  16. modusa/generators/s2d.py +300 -0
  17. modusa/generators/s_ax.py +102 -0
  18. modusa/generators/t_ax.py +64 -0
  19. modusa/generators/tds.py +267 -0
  20. modusa/models/__init__.py +14 -0
  21. modusa/models/__pycache__/signal1D.cpython-312.pyc.4443461152 +0 -0
  22. modusa/models/audio.py +90 -0
  23. modusa/models/base.py +70 -0
  24. modusa/models/data.py +457 -0
  25. modusa/models/ftds.py +584 -0
  26. modusa/models/s1d.py +578 -0
  27. modusa/models/s2d.py +619 -0
  28. modusa/models/s_ax.py +448 -0
  29. modusa/models/t_ax.py +335 -0
  30. modusa/models/tds.py +465 -0
  31. modusa/plugins/__init__.py +3 -1
  32. modusa/tmp.py +98 -0
  33. modusa/tools/__init__.py +5 -0
  34. modusa/tools/audio_converter.py +56 -67
  35. modusa/tools/audio_loader.py +90 -0
  36. modusa/tools/audio_player.py +42 -67
  37. modusa/tools/math_ops.py +104 -1
  38. modusa/tools/plotter.py +305 -497
  39. modusa/tools/youtube_downloader.py +31 -98
  40. modusa/utils/excp.py +6 -0
  41. modusa/utils/np_func_cat.py +44 -0
  42. modusa/utils/plot.py +142 -0
  43. {modusa-0.2.23.dist-info → modusa-0.3.dist-info}/METADATA +5 -16
  44. modusa-0.3.dist-info/RECORD +60 -0
  45. modusa/devtools/docs/source/generators/audio_waveforms.rst +0 -8
  46. modusa/devtools/docs/source/generators/base.rst +0 -8
  47. modusa/devtools/docs/source/generators/index.rst +0 -8
  48. modusa/devtools/docs/source/io/audio_loader.rst +0 -8
  49. modusa/devtools/docs/source/io/base.rst +0 -8
  50. modusa/devtools/docs/source/io/index.rst +0 -8
  51. modusa/devtools/docs/source/plugins/base.rst +0 -8
  52. modusa/devtools/docs/source/plugins/index.rst +0 -7
  53. modusa/devtools/docs/source/signals/audio_signal.rst +0 -8
  54. modusa/devtools/docs/source/signals/base.rst +0 -8
  55. modusa/devtools/docs/source/signals/frequency_domain_signal.rst +0 -8
  56. modusa/devtools/docs/source/signals/index.rst +0 -11
  57. modusa/devtools/docs/source/signals/spectrogram.rst +0 -8
  58. modusa/devtools/docs/source/signals/time_domain_signal.rst +0 -8
  59. modusa/devtools/docs/source/tools/audio_converter.rst +0 -8
  60. modusa/devtools/docs/source/tools/audio_player.rst +0 -8
  61. modusa/devtools/docs/source/tools/base.rst +0 -8
  62. modusa/devtools/docs/source/tools/fourier_tranform.rst +0 -8
  63. modusa/devtools/docs/source/tools/index.rst +0 -13
  64. modusa/devtools/docs/source/tools/math_ops.rst +0 -8
  65. modusa/devtools/docs/source/tools/plotter.rst +0 -8
  66. modusa/devtools/docs/source/tools/youtube_downloader.rst +0 -8
  67. modusa/io/__init__.py +0 -5
  68. modusa/io/audio_loader.py +0 -184
  69. modusa/io/base.py +0 -43
  70. modusa/signals/__init__.py +0 -3
  71. modusa/signals/audio_signal.py +0 -540
  72. modusa/signals/base.py +0 -27
  73. modusa/signals/frequency_domain_signal.py +0 -376
  74. modusa/signals/spectrogram.py +0 -564
  75. modusa/signals/time_domain_signal.py +0 -412
  76. modusa/tools/fourier_tranform.py +0 -24
  77. modusa-0.2.23.dist-info/RECORD +0 -70
  78. {modusa-0.2.23.dist-info → modusa-0.3.dist-info}/WHEEL +0 -0
  79. {modusa-0.2.23.dist-info → modusa-0.3.dist-info}/entry_points.txt +0 -0
  80. {modusa-0.2.23.dist-info → modusa-0.3.dist-info}/licenses/LICENSE.md +0 -0
@@ -1,564 +0,0 @@
1
- #!/usr/bin/env python3
2
-
3
-
4
- from modusa import excp
5
- from modusa.decorators import immutable_property, validate_args_type
6
- from modusa.signals.base import ModusaSignal
7
- from modusa.tools.math_ops import MathOps
8
- from typing import Self, Any
9
- import numpy as np
10
- import matplotlib.pyplot as plt
11
-
12
- class Spectrogram(ModusaSignal):
13
- """
14
- A 2D time–frequency representation of a signal.
15
-
16
- Parameters
17
- ----------
18
- S : np.ndarray
19
- 2D matrix representing the spectrogram (shape: [n_freqs, n_frames]).
20
- f : np.ndarray
21
- Frequency axis corresponding to the rows of `S` (shape: [n_freqs]).
22
- t : np.ndarray
23
- Time axis corresponding to the columns of `S` (shape: [n_frames]).
24
- title : str, optional
25
- Optional title for the spectrogram (e.g., used in plotting).
26
- """
27
-
28
- #--------Meta Information----------
29
- _name = "Spectrogram"
30
- _description = ""
31
- _author_name = "Ankit Anand"
32
- _author_email = "ankit0.anand0@gmail.com"
33
- _created_at = "2025-07-07"
34
- #----------------------------------
35
-
36
- @validate_args_type()
37
- def __init__(self, S: np.ndarray, f: np.ndarray, frame_rate: float, t0: float = 0.0, title: str | None = None):
38
- super().__init__()
39
-
40
- if S.ndim != 2:
41
- raise excp.InputValueError(f"`S` must have 2 dimensions, got {S.ndim}.")
42
- if f.ndim != 1:
43
- raise excp.InputValueError(f"`f` must have 1 dimension, got {f.ndim}.")
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
- )
48
- if S.shape[1] == 0:
49
- raise excp.InputValueError("`S` must have at least one time frame.")
50
-
51
- self._S = S
52
- self._f = f
53
- self._frame_rate = float(frame_rate)
54
- self._t0 = float(t0)
55
- self.title = title or self._name
56
-
57
- #----------------------
58
- # Properties
59
- #----------------------
60
-
61
- @immutable_property("Create a new object instead.")
62
- def S(self) -> np.ndarray:
63
- """Spectrogram matrix (freq × time)."""
64
- return self._S
65
-
66
- @immutable_property("Create a new object instead.")
67
- def f(self) -> np.ndarray:
68
- """Frequency axis."""
69
- return self._f
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
-
85
- @immutable_property("Create a new object instead.")
86
- def t(self) -> np.ndarray:
87
- """Time axis."""
88
- n_frames = self._S.shape[1]
89
- return np.arange(n_frames) / self.frame_rate + self.t0
90
-
91
- @immutable_property("Read only property.")
92
- def shape(self) -> np.ndarray:
93
- """Shape of the spectrogram (freqs, frames)."""
94
- return self.S.shape
95
-
96
- @immutable_property("Read only property.")
97
- def ndim(self) -> np.ndarray:
98
- """Number of dimensions (always 2)."""
99
- return self.S.ndim
100
-
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:
142
- """Print key information about the spectrogram signal."""
143
- time_resolution = 1.0 / self.frame_rate
144
- n_freq_bins = self.S.shape[0]
145
-
146
- # Estimate NFFT size
147
- nfft = (n_freq_bins - 1) * 2
148
-
149
- print("-"*50)
150
- print(f"{'Title':<20}: {self.title}")
151
- print(f"{'Kind':<20}: {self._name}")
152
- print(f"{'Shape':<20}: {self.S.shape} (freq bins × time frames)")
153
- print(f"{'Frame Rate':<20}: {self.frame_rate} (frames / sec)")
154
- print(f"{'Time resolution':<20}: {time_resolution:.4f} sec ({time_resolution * 1000:.2f} ms)")
155
- print(f"{'Freq resolution':<20}: {(self.f[1] - self.f[0]):.2f} Hz")
156
- print("-"*50)
157
-
158
- def __getitem__(self, key: tuple[int | slice, int | slice]) -> "Spectrogram | FrequencyDomainSignal | TimeDomainSignal":
159
- """
160
- Enable 2D indexing: signal[f_idx, t_idx]
161
-
162
- Returns:
163
- - Spectrogram when both f and t are slices
164
- - FrequencyDomainSignal when t is int (i.e., fixed time)
165
- - TimeDomainSignal when f is int (i.e., fixed frequency)
166
- """
167
- from modusa.signals.time_domain_signal import TimeDomainSignal
168
- from modusa.signals.frequency_domain_signal import FrequencyDomainSignal
169
-
170
- if isinstance(key, tuple) and len(key) == 2:
171
- f_key, t_key = key
172
-
173
- sliced_data = self.S[f_key, t_key]
174
- sliced_f = self.f[f_key]
175
- sliced_t = self.t[t_key]
176
-
177
- # Case 1: Scalar value → return plain numpy scalar
178
- if np.isscalar(sliced_data):
179
- return np.array(sliced_data)
180
-
181
- # Case 2: frequency slice at a single time (→ FrequencyDomainSignal)
182
- elif isinstance(t_key, int):
183
- if not isinstance(f_key, int): # already handled scalar case
184
- sliced_data = np.asarray(sliced_data).flatten()
185
- sliced_f = np.asarray(sliced_f)
186
- t0 = float(self.t[t_key])
187
- return FrequencyDomainSignal(spectrum=sliced_data, f=sliced_f, t0=t0, title=self.title + f" [t = {t0:.2f} sec]")
188
-
189
- # Case 3: time slice at a single frequency (→ TimeDomainSignal)
190
- elif isinstance(f_key, int):
191
- sliced_data = np.asarray(sliced_data).flatten()
192
- sliced_t = np.asarray(sliced_t)
193
- sr = 1.0 / np.mean(np.diff(self.t)) # assume uniform time axis
194
- t0 = float(self.t[0])
195
- f_val = float(self.f[f_key])
196
- return TimeDomainSignal(y=sliced_data, sr=sr, t0=t0, title=self.title + f" [f = {f_val:.2f} Hz]")
197
-
198
- # Case 4: 2D slice → Spectrogram
199
- else:
200
- return self.__class__(S=sliced_data, f=sliced_f, frame_rate=self.frame_rate, t0=sliced_t[0], title=self.title)
201
-
202
- raise TypeError("Expected 2D indexing: signal[f_idx, t_idx]")
203
-
204
- def crop(
205
- self,
206
- f_min: float | None = None,
207
- f_max: float | None = None,
208
- t_min: float | None = None,
209
- t_max: float | None = None
210
- ) -> "Spectrogram":
211
- """
212
- Crop the spectrogram to a rectangular region in frequency-time space.
213
-
214
- .. code-block:: python
215
-
216
- cropped = spec.crop(f_min=100, f_max=1000, t_min=5.0, t_max=10.0)
217
-
218
- Parameters
219
- ----------
220
- f_min : float or None
221
- Inclusive lower frequency bound. If None, no lower bound.
222
- f_max : float or None
223
- Exclusive upper frequency bound. If None, no upper bound.
224
- t_min : float or None
225
- Inclusive lower time bound. If None, no lower bound.
226
- t_max : float or None
227
- Exclusive upper time bound. If None, no upper bound.
228
-
229
- Returns
230
- -------
231
- Spectrogram
232
- Cropped spectrogram.
233
- """
234
- S = self.S
235
- f = self.f
236
- t = self.t
237
-
238
- f_mask = (f >= f_min) if f_min is not None else np.ones_like(f, dtype=bool)
239
- f_mask &= (f < f_max) if f_max is not None else f_mask
240
-
241
- t_mask = (t >= t_min) if t_min is not None else np.ones_like(t, dtype=bool)
242
- t_mask &= (t < t_max) if t_max is not None else t_mask
243
-
244
- cropped_S = S[np.ix_(f_mask, t_mask)]
245
- cropped_f = f[f_mask]
246
- cropped_t = t[t_mask]
247
-
248
- return self.__class__(S=cropped_S, f=cropped_f, frame_rate=self.frame_rate, t0=cropped_t[0], title=self.title)
249
-
250
-
251
- def plot(
252
- self,
253
- ax: plt.Axes | None = None,
254
- cmap: str = "gray_r",
255
- title: str | None = None,
256
- Mlabel: str | None = None,
257
- ylabel: str | None = "Frequency (hz)",
258
- xlabel: str | None = "Time (sec)",
259
- ylim: tuple[float, float] | None = None,
260
- xlim: tuple[float, float] | None = None,
261
- highlight: list[tuple[float, float, float, float]] | None = None,
262
- vlines: list | None = None,
263
- hlines: list | None = None,
264
- origin: str = "lower", # or "lower"
265
- gamma: int | float | None = None,
266
- show_colorbar: bool = True,
267
- cax: plt.Axes | None = None,
268
- show_grid: bool = True,
269
- tick_mode: str = "center", # "center" or "edge"
270
- n_ticks: tuple[int, int] | None = None,
271
- ) -> plt.Figure:
272
- """
273
- Plot the spectrogram using Matplotlib.
274
-
275
- .. code-block:: python
276
-
277
- fig = spec.plot(log_compression_factor=10, title="Log-scaled Spectrogram")
278
-
279
- Parameters
280
- ----------
281
- log_compression_factor : float or int, optional
282
- If specified, apply log-compression using log(1 + S * factor).
283
- ax : matplotlib.axes.Axes, optional
284
- Axes to draw on. If None, a new figure and axes are created.
285
- cmap : str, default "gray_r"
286
- Colormap used for the image.
287
- title : str, optional
288
- Title to use for the plot. Defaults to the signal's title.
289
- Mlabel : str, optional
290
- Label for the colorbar (e.g., "Magnitude", "dB").
291
- ylabel : str, optional
292
- Label for the y-axis. Default is "Frequency (hz)".
293
- xlabel : str, optional
294
- Label for the x-axis. Default is "Time (sec)".
295
- ylim : tuple of float, optional
296
- Limits for the y-axis (frequency).
297
- xlim : tuple of float, optional
298
- Limits for the x-axis (time).
299
- highlight : list of (x, y, w, h), optional
300
- Rectangular regions to highlight, specified in data coordinates.
301
- origin : {"lower", "upper"}, default "lower"
302
- Origin position for the image (for flipping vertical axis).
303
- show_colorbar : bool, default True
304
- Whether to display the colorbar.
305
- cax : matplotlib.axes.Axes, optional
306
- Axis to draw the colorbar on. If None, uses default placement.
307
- show_grid : bool, default True
308
- Whether to show the major gridlines.
309
- tick_mode : {"center", "edge"}, default "center"
310
- Whether to place ticks at bin centers or edges.
311
- n_ticks : tuple of int, optional
312
- Number of ticks (y_ticks, x_ticks) to display on each axis.
313
-
314
- Returns
315
- -------
316
- matplotlib.figure.Figure
317
- The figure object containing the plot.
318
- """
319
- from modusa.tools.plotter import Plotter
320
-
321
- title = title or self.title
322
-
323
- fig = Plotter.plot_matrix(
324
- M=self.S,
325
- r=self.f,
326
- c=self.t,
327
- ax=ax,
328
- cmap=cmap,
329
- title=title,
330
- Mlabel=Mlabel,
331
- rlabel=ylabel,
332
- clabel=xlabel,
333
- rlim=ylim,
334
- clim=xlim,
335
- highlight=highlight,
336
- vlines=vlines,
337
- hlines=hlines,
338
- origin=origin,
339
- gamma=gamma,
340
- show_colorbar=show_colorbar,
341
- cax=cax,
342
- show_grid=show_grid,
343
- tick_mode=tick_mode,
344
- n_ticks=n_ticks
345
- )
346
-
347
- return fig
348
-
349
- #----------------------------
350
- # Math ops
351
- #----------------------------
352
-
353
- def __array__(self, dtype=None):
354
- return np.asarray(self._S, dtype=dtype)
355
-
356
- def __add__(self, other):
357
- other_data = other.S if isinstance(other, self.__class__) else other
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)
360
-
361
- def __radd__(self, other):
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)
364
-
365
- def __sub__(self, other):
366
- other_data = other.S if isinstance(other, self.__class__) else other
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)
369
-
370
- def __rsub__(self, other):
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)
373
-
374
- def __mul__(self, other):
375
- other_data = other.S if isinstance(other, self.__class__) else other
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)
378
-
379
- def __rmul__(self, other):
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)
382
-
383
- def __truediv__(self, other):
384
- other_data = other.S if isinstance(other, self.__class__) else other
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)
387
-
388
- def __rtruediv__(self, other):
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)
391
-
392
- def __floordiv__(self, other):
393
- other_data = other.S if isinstance(other, self.__class__) else other
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)
396
-
397
- def __rfloordiv__(self, other):
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)
400
-
401
- def __pow__(self, other):
402
- other_data = other.S if isinstance(other, self.__class__) else other
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)
405
-
406
- def __rpow__(self, other):
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)
409
-
410
- def __abs__(self):
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
- #--------------------------
417
-
418
- def sin(self):
419
- """Element-wise sine of the spectrogram."""
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)
422
-
423
- def cos(self):
424
- """Element-wise cosine of the spectrogram."""
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)
427
-
428
- def exp(self):
429
- """Element-wise exponential of the spectrogram."""
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)
432
-
433
- def tanh(self):
434
- """Element-wise hyperbolic tangent of the spectrogram."""
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)
437
-
438
- def log(self):
439
- """Element-wise natural logarithm of the spectrogram."""
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)
442
-
443
- def log1p(self):
444
- """Element-wise log(1 + M) of the spectrogram."""
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)
447
-
448
- def log10(self):
449
- """Element-wise base-10 logarithm of the spectrogram."""
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)
452
-
453
- def log2(self):
454
- """Element-wise base-2 logarithm of the spectrogram."""
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)
457
-
458
- #--------------------------
459
- # Aggregation signal ops
460
- #--------------------------
461
-
462
- def mean(self, axis: int | None = None) -> float:
463
- """Return the mean of the spectrogram values."""
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:
477
- """Return the standard deviation of the spectrogram values."""
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:
491
- """Return the minimum value in the spectrogram."""
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:
505
- """Return the maximum value in the spectrogram."""
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:
519
- """Return the sum of the spectrogram values."""
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")
531
-
532
- #-----------------------------------
533
- # Repr
534
- #-----------------------------------
535
-
536
- def __str__(self):
537
- cls = self.__class__.__name__
538
- data = self.S
539
-
540
- arr_str = np.array2string(
541
- data,
542
- separator=", ",
543
- threshold=50, # limit number of elements shown
544
- edgeitems=3, # show first/last 3 rows and columns
545
- max_line_width=120, # avoid wrapping
546
- formatter={'float_kind': lambda x: f"{x:.4g}"}
547
- )
548
-
549
- return f"Signal({arr_str}, shape={data.shape}, kind={cls})"
550
-
551
- def __repr__(self):
552
- cls = self.__class__.__name__
553
- data = self.S
554
-
555
- arr_str = np.array2string(
556
- data,
557
- separator=", ",
558
- threshold=50, # limit number of elements shown
559
- edgeitems=3, # show first/last 3 rows and columns
560
- max_line_width=120, # avoid wrapping
561
- formatter={'float_kind': lambda x: f"{x:.4g}"}
562
- )
563
-
564
- return f"Signal({arr_str}, shape={data.shape}, kind={cls})"