modusa 0.1.0__py3-none-any.whl → 0.2.1__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 (48) hide show
  1. modusa/.DS_Store +0 -0
  2. modusa/decorators.py +5 -5
  3. modusa/devtools/generate_template.py +80 -15
  4. modusa/devtools/main.py +6 -4
  5. modusa/devtools/templates/{engines.py → engine.py} +8 -7
  6. modusa/devtools/templates/{generators.py → generator.py} +8 -10
  7. modusa/devtools/templates/io.py +24 -0
  8. modusa/devtools/templates/{plugins.py → plugin.py} +7 -6
  9. modusa/devtools/templates/signal.py +40 -0
  10. modusa/devtools/templates/test.py +11 -0
  11. modusa/engines/.DS_Store +0 -0
  12. modusa/engines/__init__.py +1 -2
  13. modusa/generators/__init__.py +3 -1
  14. modusa/generators/audio_waveforms.py +227 -0
  15. modusa/generators/base.py +14 -25
  16. modusa/io/__init__.py +9 -0
  17. modusa/io/audio_converter.py +76 -0
  18. modusa/io/audio_loader.py +212 -0
  19. modusa/io/audio_player.py +72 -0
  20. modusa/io/base.py +43 -0
  21. modusa/io/plotter.py +430 -0
  22. modusa/io/youtube_downloader.py +139 -0
  23. modusa/main.py +15 -17
  24. modusa/plugins/__init__.py +1 -7
  25. modusa/signals/__init__.py +4 -6
  26. modusa/signals/audio_signal.py +421 -175
  27. modusa/signals/base.py +11 -271
  28. modusa/signals/frequency_domain_signal.py +329 -0
  29. modusa/signals/signal_ops.py +158 -0
  30. modusa/signals/spectrogram.py +465 -0
  31. modusa/signals/time_domain_signal.py +309 -0
  32. modusa/utils/excp.py +5 -0
  33. {modusa-0.1.0.dist-info → modusa-0.2.1.dist-info}/METADATA +16 -11
  34. modusa-0.2.1.dist-info/RECORD +47 -0
  35. modusa/devtools/templates/signals.py +0 -63
  36. modusa/engines/plot_1dsignal.py +0 -130
  37. modusa/engines/plot_2dmatrix.py +0 -159
  38. modusa/generators/basic_waveform.py +0 -185
  39. modusa/plugins/plot_1dsignal.py +0 -59
  40. modusa/plugins/plot_2dmatrix.py +0 -76
  41. modusa/plugins/plot_time_domain_signal.py +0 -59
  42. modusa/signals/signal1d.py +0 -311
  43. modusa/signals/signal2d.py +0 -226
  44. modusa/signals/uniform_time_domain_signal.py +0 -212
  45. modusa-0.1.0.dist-info/RECORD +0 -41
  46. {modusa-0.1.0.dist-info → modusa-0.2.1.dist-info}/WHEEL +0 -0
  47. {modusa-0.1.0.dist-info → modusa-0.2.1.dist-info}/entry_points.txt +0 -0
  48. {modusa-0.1.0.dist-info → modusa-0.2.1.dist-info}/licenses/LICENSE.md +0 -0
@@ -0,0 +1,158 @@
1
+ #!/usr/bin/env python3
2
+
3
+ from modusa import excp
4
+ from typing import Any
5
+ import numpy as np
6
+
7
+ class SignalOps:
8
+ """
9
+ Performs arithmetic and NumPy-style ops on ModusaSignal instances.
10
+
11
+ Note
12
+ ----
13
+ - Shape-changing operations like reshape, transpose, etc. are not yet supported. Use only element-wise or aggregation ops for now.
14
+ - Index alignment must be handled carefully in future extensions.
15
+ """
16
+
17
+ def _axes_match(a1: tuple[np.ndarray, ...], a2: tuple[np.ndarray, ...]) -> bool:
18
+ """
19
+ To check if two axes are same.
20
+ """
21
+ if len(a1) != len(a2):
22
+ return False
23
+ return all(np.allclose(x, y, atol=1e-8) for x, y in zip(a1, a2))
24
+
25
+
26
+ #----------------------------------
27
+ # To handle basic math operations like
28
+ # +, -, *, **, / ...
29
+ #----------------------------------
30
+
31
+ @staticmethod
32
+ def add(data: np.ndarray, other: np.ndarray) -> np.ndarray:
33
+ return np.add(data, other)
34
+
35
+ @staticmethod
36
+ def subtract(signal: "ModusaSignal", other: Any) -> "ModusaSignal":
37
+ return SignalOps._apply_binary_op(signal, other, np.subtract, "subtract")
38
+
39
+ @staticmethod
40
+ def multiply(signal: "ModusaSignal", other: Any):
41
+ return SignalOps._apply_binary_op(signal, other, np.multiply, "multiply")
42
+
43
+ @staticmethod
44
+ def divide(signal: "ModusaSignal", other: Any):
45
+ return SignalOps._apply_binary_op(signal, other, np.divide, "divide")
46
+
47
+ @staticmethod
48
+ def power(signal: "ModusaSignal", other: Any):
49
+ return SignalOps._apply_binary_op(signal, other, np.power, "power")
50
+
51
+ @staticmethod
52
+ def floor_divide(signal: "ModusaSignal", other: Any):
53
+ return SignalOps._apply_binary_op(signal, other, np.floor_divide, "floor_divide")
54
+
55
+
56
+
57
+ #----------------------------------
58
+ # To handle numpy aggregator ops
59
+ # mean, sum, ...
60
+ # TODO: Add dimension select option
61
+ #----------------------------------
62
+ @staticmethod
63
+ def mean(signal: "ModusaSignal") -> float:
64
+ """Return the mean of the signal data."""
65
+ return float(np.mean(signal._data))
66
+
67
+ @staticmethod
68
+ def std(signal: "ModusaSignal") -> float:
69
+ """Return the standard deviation of the signal data."""
70
+ return float(np.std(signal._data))
71
+
72
+ @staticmethod
73
+ def min(signal: "ModusaSignal") -> float:
74
+ """Return the minimum value in the signal data."""
75
+ return float(np.min(signal._data))
76
+
77
+ @staticmethod
78
+ def max(signal: "ModusaSignal") -> float:
79
+ """Return the maximum value in the signal data."""
80
+ return float(np.max(signal._data))
81
+
82
+ @staticmethod
83
+ def sum(signal: "ModusaSignal") -> float:
84
+ """Return the sum of the signal data."""
85
+ return float(np.sum(signal._data))
86
+
87
+ #----------------------------------
88
+ # To handle numpy ops where the
89
+ # shapes are unaltered
90
+ # sin, cos, exp, log, ...
91
+ #----------------------------------
92
+
93
+ @staticmethod
94
+ def _apply_unary_op(signal: "ModusaSignal", op_func, op_name: str):
95
+ from modusa.signals.base import ModusaSignal # avoid circular import
96
+
97
+ if not isinstance(signal, ModusaSignal):
98
+ raise excp.InputTypeError(f"`signal` must be a ModusaSignal, got {type(signal)}")
99
+
100
+ try:
101
+ result = op_func(signal._data)
102
+ except Exception as e:
103
+ raise excp.SignalOpError(f"{op_name} failed: {e}")
104
+
105
+ if not isinstance(result, np.ndarray):
106
+ raise excp.SignalOpError(f"{op_name} did not return a NumPy array, got {type(result)}")
107
+
108
+ if result.shape != signal._data.shape:
109
+ raise excp.SignalOpError(f"{op_name} changed shape: {signal._data.shape} → {result.shape}")
110
+
111
+ return signal.replace(data=result)
112
+
113
+
114
+ @staticmethod
115
+ def sin(signal: "ModusaSignal"):
116
+ return SignalOps._apply_unary_op(signal, np.sin, "sin")
117
+
118
+ @staticmethod
119
+ def cos(signal: "ModusaSignal"):
120
+ return SignalOps._apply_unary_op(signal, np.cos, "cos")
121
+
122
+ @staticmethod
123
+ def exp(signal: "ModusaSignal"):
124
+ return SignalOps._apply_unary_op(signal, np.exp, "exp")
125
+
126
+ @staticmethod
127
+ def tanh(signal: "ModusaSignal"):
128
+ return SignalOps._apply_unary_op(signal, np.tanh, "tanh")
129
+
130
+ @staticmethod
131
+ def log(signal: "ModusaSignal"):
132
+ return SignalOps._apply_unary_op(signal, np.log, "log")
133
+
134
+ @staticmethod
135
+ def log10(signal: "ModusaSignal"):
136
+ return SignalOps._apply_unary_op(signal, np.log10, "log10")
137
+
138
+ @staticmethod
139
+ def log2(signal: "ModusaSignal"):
140
+ return SignalOps._apply_unary_op(signal, np.log2, "log2")
141
+
142
+ @staticmethod
143
+ def log1p(signal: "ModusaSignal"):
144
+ return SignalOps._apply_unary_op(signal, np.log1p, "log1p")
145
+
146
+
147
+ @staticmethod
148
+ def sqrt(signal: "ModusaSignal"):
149
+ return SignalOps._apply_unary_op(signal, np.sqrt, "sqrt")
150
+
151
+ @staticmethod
152
+ def abs(signal: "ModusaSignal"):
153
+ return SignalOps._apply_unary_op(signal, np.abs, "abs")
154
+
155
+ #------------------------------------
156
+ # TODO: Add shape-changing ops like
157
+ # reshape, transpose, squeeze later
158
+ #------------------------------------
@@ -0,0 +1,465 @@
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 typing import Self, Any
8
+ import numpy as np
9
+ import matplotlib.pyplot as plt
10
+
11
+ class Spectrogram(ModusaSignal):
12
+ """
13
+ A 2D time–frequency representation of a signal.
14
+
15
+ Parameters
16
+ ----------
17
+ S : np.ndarray
18
+ 2D matrix representing the spectrogram (shape: [n_freqs, n_frames]).
19
+ f : np.ndarray
20
+ Frequency axis corresponding to the rows of `S` (shape: [n_freqs]).
21
+ t : np.ndarray
22
+ Time axis corresponding to the columns of `S` (shape: [n_frames]).
23
+ title : str, optional
24
+ Optional title for the spectrogram (e.g., used in plotting).
25
+ """
26
+
27
+ #--------Meta Information----------
28
+ _name = "Spectrogram"
29
+ _description = ""
30
+ _author_name = "Ankit Anand"
31
+ _author_email = "ankit0.anand0@gmail.com"
32
+ _created_at = "2025-07-07"
33
+ #----------------------------------
34
+
35
+ @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
38
+
39
+ if S.ndim != 2:
40
+ raise excp.InputValueError(f"`S` must have 2 dimension, got {S.ndim}.")
41
+ if f.ndim != 1:
42
+ 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
+
49
+ if S.shape[1] == 0:
50
+ raise excp.InputValueError("`S` must have at least one time frame")
51
+
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
+ self._S = S
58
+ self._f = f
59
+ self._t = t
60
+ self.title = title or self._name
61
+
62
+ #----------------------
63
+ # Properties
64
+ #----------------------
65
+ @immutable_property("Create a new object instead.")
66
+ def S(self) -> np.ndarray:
67
+ """Spectrogram matrix (freq × time)."""
68
+ return self._S
69
+
70
+ @immutable_property("Create a new object instead.")
71
+ def f(self) -> np.ndarray:
72
+ """Frequency axis."""
73
+ return self._f
74
+
75
+ @immutable_property("Create a new object instead.")
76
+ def t(self) -> np.ndarray:
77
+ """Time axis."""
78
+ return self._t
79
+
80
+ @immutable_property("Read only property.")
81
+ def shape(self) -> np.ndarray:
82
+ """Shape of the spectrogram (freqs, frames)."""
83
+ return self.S.shape
84
+
85
+ @immutable_property("Read only property.")
86
+ def ndim(self) -> np.ndarray:
87
+ """Number of dimensions (always 2)."""
88
+ return self.S.ndim
89
+
90
+ @immutable_property("Mutation not allowed.")
91
+ def info(self) -> None:
92
+ """Print key information about the spectrogram signal."""
93
+ time_resolution = self.t[1] - self.t[0]
94
+ n_freq_bins = self.S.shape[0]
95
+
96
+ # Estimate NFFT size
97
+ nfft = (n_freq_bins - 1) * 2
98
+
99
+ print("-"*50)
100
+ print(f"{'Title':<20}: {self.title}")
101
+ print(f"{'Kind':<20}: {self._name}")
102
+ print(f"{'Shape':<20}: {self.S.shape} (freq bins × time frames)")
103
+ print(f"{'Time resolution':<20}: {time_resolution:.4f} sec ({time_resolution * 1000:.2f} ms)")
104
+ print(f"{'Freq resolution':<20}: {(self.f[1] - self.f[0]):.2f} Hz")
105
+ print("-"*50)
106
+ #------------------------
107
+
108
+
109
+ #------------------------
110
+ # Useful tools
111
+ #------------------------
112
+
113
+ def __getitem__(self, key: tuple[int | slice, int | slice]) -> "Spectrogram | FrequencyDomainSignal | TimeDomainSignal":
114
+ """
115
+ Enable 2D indexing: signal[f_idx, t_idx]
116
+
117
+ Returns:
118
+ - Spectrogram when both f and t are slices
119
+ - FrequencyDomainSignal when t is int (i.e., fixed time)
120
+ - TimeDomainSignal when f is int (i.e., fixed frequency)
121
+ """
122
+ from modusa.signals.time_domain_signal import TimeDomainSignal
123
+ from modusa.signals.frequency_domain_signal import FrequencyDomainSignal
124
+
125
+ if isinstance(key, tuple) and len(key) == 2:
126
+ f_key, t_key = key
127
+
128
+ sliced_data = self.S[f_key, t_key]
129
+ sliced_f = self.f[f_key]
130
+ sliced_t = self.t[t_key]
131
+
132
+ # Case 1: Scalar value → return plain numpy scalar
133
+ if np.isscalar(sliced_data):
134
+ return np.array(sliced_data)
135
+
136
+ # Case 2: frequency slice at a single time (→ FrequencyDomainSignal)
137
+ elif isinstance(t_key, int):
138
+ if not isinstance(f_key, int): # already handled scalar case
139
+ sliced_data = np.asarray(sliced_data).flatten()
140
+ sliced_f = np.asarray(sliced_f)
141
+ 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
+ )
148
+
149
+ # Case 3: time slice at a single frequency (→ TimeDomainSignal)
150
+ elif isinstance(f_key, int):
151
+ sliced_data = np.asarray(sliced_data).flatten()
152
+ sliced_t = np.asarray(sliced_t)
153
+ sr = 1.0 / np.mean(np.diff(self.t)) # assume uniform time axis
154
+ t0 = float(self.t[0])
155
+ 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
+ )
162
+
163
+ # Case 4: 2D slice → Spectrogram
164
+ else:
165
+ return self.__class__(
166
+ S=sliced_data,
167
+ f=sliced_f,
168
+ t=sliced_t,
169
+ title=self.title
170
+ )
171
+
172
+ raise TypeError("Expected 2D indexing: signal[f_idx, t_idx]")
173
+
174
+ def crop(
175
+ self,
176
+ f_min: float | None = None,
177
+ f_max: float | None = None,
178
+ t_min: float | None = None,
179
+ t_max: float | None = None
180
+ ) -> "Spectrogram":
181
+ """
182
+ Crop the spectrogram to a rectangular region in frequency-time space.
183
+
184
+ .. code-block:: python
185
+
186
+ cropped = spec.crop(f_min=100, f_max=1000, t_min=5.0, t_max=10.0)
187
+
188
+ Parameters
189
+ ----------
190
+ f_min : float or None
191
+ Inclusive lower frequency bound. If None, no lower bound.
192
+ f_max : float or None
193
+ Exclusive upper frequency bound. If None, no upper bound.
194
+ t_min : float or None
195
+ Inclusive lower time bound. If None, no lower bound.
196
+ t_max : float or None
197
+ Exclusive upper time bound. If None, no upper bound.
198
+
199
+ Returns
200
+ -------
201
+ Spectrogram
202
+ Cropped spectrogram.
203
+ """
204
+ S = self.S
205
+ f = self.f
206
+ t = self.t
207
+
208
+ f_mask = (f >= f_min) if f_min is not None else np.ones_like(f, dtype=bool)
209
+ f_mask &= (f < f_max) if f_max is not None else f_mask
210
+
211
+ t_mask = (t >= t_min) if t_min is not None else np.ones_like(t, dtype=bool)
212
+ t_mask &= (t < t_max) if t_max is not None else t_mask
213
+
214
+ cropped_S = S[np.ix_(f_mask, t_mask)]
215
+ cropped_f = f[f_mask]
216
+ cropped_t = t[t_mask]
217
+
218
+ return self.__class__(S=cropped_S, f=cropped_f, t=cropped_t, title=self.title)
219
+
220
+
221
+ def plot(
222
+ self,
223
+ log_compression_factor: int | float | None = None,
224
+ ax: plt.Axes | None = None,
225
+ cmap: str = "gray_r",
226
+ title: str | None = None,
227
+ Mlabel: str | None = None,
228
+ ylabel: str | None = "Frequency (hz)",
229
+ xlabel: str | None = "Time (sec)",
230
+ ylim: tuple[float, float] | None = None,
231
+ xlim: tuple[float, float] | None = None,
232
+ highlight: list[tuple[float, float, float, float]] | None = None,
233
+ origin: str = "lower", # or "lower"
234
+ show_colorbar: bool = True,
235
+ cax: plt.Axes | None = None,
236
+ show_grid: bool = True,
237
+ tick_mode: str = "center", # "center" or "edge"
238
+ n_ticks: tuple[int, int] | None = None,
239
+ ) -> plt.Figure:
240
+ """
241
+ Plot the spectrogram using Matplotlib.
242
+
243
+ .. code-block:: python
244
+
245
+ fig = spec.plot(log_compression_factor=10, title="Log-scaled Spectrogram")
246
+
247
+ Parameters
248
+ ----------
249
+ log_compression_factor : float or int, optional
250
+ If specified, apply log-compression using log(1 + S * factor).
251
+ ax : matplotlib.axes.Axes, optional
252
+ Axes to draw on. If None, a new figure and axes are created.
253
+ cmap : str, default "gray_r"
254
+ Colormap used for the image.
255
+ title : str, optional
256
+ Title to use for the plot. Defaults to the signal's title.
257
+ Mlabel : str, optional
258
+ Label for the colorbar (e.g., "Magnitude", "dB").
259
+ ylabel : str, optional
260
+ Label for the y-axis. Default is "Frequency (hz)".
261
+ xlabel : str, optional
262
+ Label for the x-axis. Default is "Time (sec)".
263
+ ylim : tuple of float, optional
264
+ Limits for the y-axis (frequency).
265
+ xlim : tuple of float, optional
266
+ Limits for the x-axis (time).
267
+ highlight : list of (x, y, w, h), optional
268
+ Rectangular regions to highlight, specified in data coordinates.
269
+ origin : {"lower", "upper"}, default "lower"
270
+ Origin position for the image (for flipping vertical axis).
271
+ show_colorbar : bool, default True
272
+ Whether to display the colorbar.
273
+ cax : matplotlib.axes.Axes, optional
274
+ Axis to draw the colorbar on. If None, uses default placement.
275
+ show_grid : bool, default True
276
+ Whether to show the major gridlines.
277
+ tick_mode : {"center", "edge"}, default "center"
278
+ Whether to place ticks at bin centers or edges.
279
+ n_ticks : tuple of int, optional
280
+ Number of ticks (y_ticks, x_ticks) to display on each axis.
281
+
282
+ Returns
283
+ -------
284
+ matplotlib.figure.Figure
285
+ The figure object containing the plot.
286
+ """
287
+ from modusa.io import Plotter
288
+
289
+ title = title or self.title
290
+
291
+ fig = Plotter.plot_matrix(
292
+ M=self.S,
293
+ r=self.f,
294
+ c=self.t,
295
+ log_compression_factor=log_compression_factor,
296
+ ax=ax,
297
+ cmap=cmap,
298
+ title=title,
299
+ Mlabel=Mlabel,
300
+ rlabel=ylabel,
301
+ clabel=xlabel,
302
+ rlim=ylim,
303
+ clim=xlim,
304
+ highlight=highlight,
305
+ origin=origin,
306
+ show_colorbar=show_colorbar,
307
+ cax=cax,
308
+ show_grid=show_grid,
309
+ tick_mode=tick_mode,
310
+ n_ticks=n_ticks
311
+ )
312
+
313
+ return fig
314
+
315
+ #----------------------------
316
+ # Math ops
317
+ #----------------------------
318
+
319
+ def __array__(self, dtype=None):
320
+ return np.asarray(self._S, dtype=dtype)
321
+
322
+ def __add__(self, other):
323
+ 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)
326
+
327
+ 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)
330
+
331
+ def __sub__(self, other):
332
+ 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)
335
+
336
+ 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)
339
+
340
+ def __mul__(self, other):
341
+ 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)
344
+
345
+ 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)
348
+
349
+ def __truediv__(self, other):
350
+ 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)
353
+
354
+ 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)
357
+
358
+ def __floordiv__(self, other):
359
+ 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)
362
+
363
+ 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)
366
+
367
+ def __pow__(self, other):
368
+ 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)
371
+
372
+ 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)
375
+
376
+ def __abs__(self):
377
+ result = np.abs(self.S)
378
+ return self.__class__(S=result, f=self.f, t=self.t, title=self.title)
379
+
380
+ def sin(self):
381
+ """Element-wise sine of the spectrogram."""
382
+ return self.__class__(S=np.sin(self.S), f=self.f, t=self.t, title=self.title)
383
+
384
+ def cos(self):
385
+ """Element-wise cosine of the spectrogram."""
386
+ return self.__class__(S=np.cos(self.S), f=self.f, t=self.t, title=self.title)
387
+
388
+ def exp(self):
389
+ """Element-wise exponential of the spectrogram."""
390
+ return self.__class__(S=np.exp(self.S), f=self.f, t=self.t, title=self.title)
391
+
392
+ def tanh(self):
393
+ """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)
395
+
396
+ def log(self):
397
+ """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)
399
+
400
+ def log1p(self):
401
+ """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)
403
+
404
+ def log10(self):
405
+ """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)
407
+
408
+ def log2(self):
409
+ """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)
411
+
412
+
413
+ def mean(self) -> float:
414
+ """Return the mean of the spectrogram values."""
415
+ return float(np.mean(self.S))
416
+
417
+ def std(self) -> float:
418
+ """Return the standard deviation of the spectrogram values."""
419
+ return float(np.std(self.S))
420
+
421
+ def min(self) -> float:
422
+ """Return the minimum value in the spectrogram."""
423
+ return float(np.min(self.S))
424
+
425
+ def max(self) -> float:
426
+ """Return the maximum value in the spectrogram."""
427
+ return float(np.max(self.S))
428
+
429
+ def sum(self) -> float:
430
+ """Return the sum of the spectrogram values."""
431
+ return float(np.sum(self.S))
432
+
433
+ #-----------------------------------
434
+ # Repr
435
+ #-----------------------------------
436
+
437
+ def __str__(self):
438
+ cls = self.__class__.__name__
439
+ data = self.S
440
+
441
+ arr_str = np.array2string(
442
+ data,
443
+ separator=", ",
444
+ threshold=50, # limit number of elements shown
445
+ edgeitems=3, # show first/last 3 rows and columns
446
+ max_line_width=120, # avoid wrapping
447
+ formatter={'float_kind': lambda x: f"{x:.4g}"}
448
+ )
449
+
450
+ return f"Signal({arr_str}, shape={data.shape}, kind={cls})"
451
+
452
+ def __repr__(self):
453
+ cls = self.__class__.__name__
454
+ data = self.S
455
+
456
+ arr_str = np.array2string(
457
+ data,
458
+ separator=", ",
459
+ threshold=50, # limit number of elements shown
460
+ edgeitems=3, # show first/last 3 rows and columns
461
+ max_line_width=120, # avoid wrapping
462
+ formatter={'float_kind': lambda x: f"{x:.4g}"}
463
+ )
464
+
465
+ return f"Signal({arr_str}, shape={data.shape}, kind={cls})"