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
modusa/signals/base.py CHANGED
@@ -2,293 +2,33 @@
2
2
 
3
3
  from modusa import excp
4
4
  from modusa.decorators import immutable_property, validate_args_type
5
+ from modusa.signals.signal_ops import SignalOps
5
6
  from abc import ABC, abstractmethod
7
+ from dataclasses import dataclass
6
8
  from typing import Self
7
9
  import numpy as np
8
10
  import matplotlib.pyplot as plt
9
11
 
10
12
  class ModusaSignal(ABC):
11
13
  """
12
- Base class prototype for any signal.
14
+ Base class for any signal in the modusa framework.
13
15
 
14
16
  Note
15
17
  ----
16
- - Serves as the foundation for all signal types in the Modusa framework
17
- - Intended to be subclassed
18
- - Subclass shoud implement a **read-only** `data` property (e.g., amplitude or spectral data) and a `plot()` method to visualize the signal
19
-
20
- Warning
21
- -------
22
- - You cannot create a subclass without `data` and `plot()` implemented. It will throw an error on instantiating the subclass.
23
-
24
- Example
25
- -------
26
- .. code-block:: python
27
-
28
- from modusa.signals import ModusaSignal
29
- from modusa.decorators import validate_args_type
30
-
31
- class MySignal(ModusaSignal):
32
-
33
- @validate_args_type()
34
- def __init__(self, y: nd.ndarray):
35
- super().__init__() # Very important for proper initialisation
36
- self._y = y
37
-
38
- @property
39
- def data(self):
40
- return self._y
41
-
42
- @validate_args_type()
43
- def plot(self):
44
- # Your plotting logic here
45
- pass
18
+ - Intended to be subclassed.
46
19
  """
47
20
 
48
21
  #--------Meta Information----------
49
- name = "Modusa Signal"
50
- description = "Base class for any signal types in the Modusa framework."
51
- author_name = "Ankit Anand"
52
- author_email = "ankit0.anand0@gmail.com"
53
- created_at = "2025-06-23"
22
+ _name = "Modusa Signal"
23
+ _description = "Base class for any signal types in the Modusa framework."
24
+ _author_name = "Ankit Anand"
25
+ _author_email = "ankit0.anand0@gmail.com"
26
+ _created_at = "2025-06-23"
54
27
  #----------------------------------
55
28
 
56
29
  @validate_args_type()
57
- def __init__(self, data: np.ndarray, data_idx: np.ndarray):
58
- self._data = data
59
- self._data_idx = data_idx
30
+ def __init__(self):
60
31
  self._plugin_chain = []
61
-
62
- #----------------------------
63
- # Setters
64
- #----------------------------
65
- @validate_args_type()
66
- def set_name(self, name: str) -> Self:
67
- self.name = name
68
-
69
- return name
70
-
71
-
72
- #----------------------------
73
- # Properties
74
- #----------------------------
75
-
76
- @property
77
- def data(self) -> np.ndarray:
78
- """
79
- The core signal data as a NumPy array.
80
-
81
- Note
82
- ----
83
- - Different signals might need to have different variable names to store data, e.g. y(t) -> y, M(x, y) -> M, x(t) -> x
84
- - This `data` property must return the correct data for any given subclass (y for y(t), M for M(x, y)), see the example.
85
- - Must return `np.ndarray`
86
- """
87
- return self._data
88
-
89
- @immutable_property("Create a new object instead.")
90
- def data_idx(self) -> np.ndarray:
91
- """
92
- The coordinate values associated with each element of the signal `data`.
93
-
94
- Note
95
- ----
96
- - This is often a 1D array of the same length as the first axis of `data`.
97
- - For time-domain signals, this typically represents timestamps.
98
- - For spectrograms or other 2D signals, you may use a dict mapping axes to coordinate arrays.
99
- - This property is read-only; to modify it, create a new signal object.
100
-
101
- Returns
102
- -------
103
- np.ndarray
104
- An array of coordinates or indices corresponding to the signal data.
105
- """
106
- return self._data_idx
107
-
108
- @immutable_property("Read-only property.")
109
- def shape(self) -> tuple[int]:
110
- """Shape of the signal."""
111
- return self.data.shape
112
-
113
-
114
- @immutable_property("plugin_chain is read-only. It is automatically updated.")
115
- def plugin_chain(self) -> list[str]:
116
- """
117
- List of plugin names applied to the signal.
118
-
119
- Note
120
- ----
121
- - Reflects the signal’s processing history
122
- - Ordered by application sequence
123
- - Managed automatically (read-only)
124
- - Use `info` for a formatted summary
125
- """
126
- return self._plugin_chain.copy()
127
-
128
- @immutable_property("Mutation not allowed, generated automatically.")
129
- def info(self) -> None:
130
- """
131
- Prints a quick summary of the signal.
132
-
133
- Note
134
- ----
135
- - Output is printed to the console.
136
- - Returns nothing.
137
-
138
- """
139
-
140
- print("\n".join([
141
- f"{self.__class__.__name__}(",
142
- f" Signal Name: '{self.name}',",
143
- f" Inheritance: {' → '.join(cls.__name__ for cls in self.__class__.mro()[:-1])}",
144
- f" Plugin Chain: {' → '.join(self._plugin_chain) or '(none)'}",
145
- f")"
146
- ]))
147
-
148
- #-------------------------------
149
- # Basic functionalities
150
- #-------------------------------
151
-
152
- @abstractmethod
153
- def plot(self) -> plt.Figure:
154
- """
155
- Plot a visual representation of the signal.
156
-
157
- Note
158
- ----
159
- - For any signal, one must implement `plot` method with useful features to make users happy
160
- - Try to return `matplotlib.figure.Figure` for customization/saving but other plotting libraries are also welcome
161
- """
162
- pass
163
-
164
- @abstractmethod
165
- def _with_data(self, new_data: np.ndarray, new_data_idx: np.ndarray) -> Self:
166
- """Subclasses must override this to return a copy with new data."""
167
- pass
168
-
169
-
170
- def trim(self, start: float | None = None, stop: float | None = None):
171
- """
172
- Returns a new signal trimmed between two data_idx values.
173
-
174
- Parameters
175
- ----------
176
- start : float or None
177
- The starting data_idx value (inclusive). If None, starts from the beginning.
178
- stop : float or None
179
- The stopping data_idx value (exclusive). If None, goes till the end.
180
-
181
- Returns
182
- -------
183
- ModusaSignal
184
- A new signal with trimmed data and data_idx.
185
- """
186
- # Define bounds
187
- start_v = -np.inf if start is None else start
188
- stop_v = np.inf if stop is None else stop
189
-
190
- # Build a mask over data_idx values
191
- mask = (self.data_idx >= start_v) & (self.data_idx < stop_v)
192
- idx = np.where(mask)[0]
193
-
194
- return self._with_data(new_data=self.data[idx], new_data_idx=self.data_idx[idx])
195
-
196
- #----------------------------
197
- # Dunder method
198
- #----------------------------
199
-
200
-
201
- #----------------------------
202
- # Slicing
203
- #----------------------------
204
-
205
- def __getitem__(self, key):
206
- if isinstance(key, (int, slice)):
207
- # Normal Python-style slicing by index
208
- sliced_data = self.data[key]
209
- sliced_idx = self.data_idx[key]
210
- return self._with_data(new_data=sliced_data, new_data_idx=sliced_idx)
211
-
212
- else:
213
- raise TypeError(f"Indexing with type {type(key)} is not supported. Use int or slice.")
214
-
215
32
 
216
33
 
217
- def __str__(self):
218
- cls = self.__class__.__name__
219
- data = self.data
220
-
221
- arr_str = np.array2string(
222
- data,
223
- separator=", ",
224
- threshold=50, # limit number of elements shown
225
- edgeitems=3, # show first/last 3 rows and columns
226
- max_line_width=120, # avoid wrapping
227
- formatter={'float_kind': lambda x: f"{x:.4g}"}
228
- )
229
-
230
- return f"{cls}({arr_str}, shape={data.shape})"
231
-
232
- def __repr__(self):
233
- cls = self.__class__.__name__
234
- data = self.data
235
-
236
- arr_str = np.array2string(
237
- data,
238
- separator=", ",
239
- threshold=50, # limit number of elements shown
240
- edgeitems=3, # show first/last 3 rows and columns
241
- max_line_width=120, # avoid wrapping
242
- formatter={'float_kind': lambda x: f"{x:.4g}"}
243
- )
244
-
245
- return f"{cls}({arr_str}, shape={data.shape})"
246
-
247
-
248
- #----------------------------
249
- # Math ops
250
- #----------------------------
251
-
252
- def __array__(self, dtype=None):
253
- return self.data if dtype is None else self.data.astype(dtype)
254
-
255
- def __array_ufunc__(self, ufunc, method, *inputs, **kwargs):
256
- if method != '__call__':
257
- return NotImplemented
258
-
259
- # Replace ModusaSignal instances with their data
260
- new_inputs = [i.data if isinstance(i, ModusaSignal) else i for i in inputs]
261
-
262
- try:
263
- result = ufunc(*new_inputs, **kwargs)
264
- except Exception as e:
265
- raise TypeError(f"Ufunc {ufunc.__name__} failed: {e}")
266
-
267
- return self._with_data(result)
268
-
269
- def _apply_op(self, other, op, label):
270
- if isinstance(other, ModusaSignal):
271
- other = other.data # extract data
272
-
273
- try:
274
- result = op(self.data, other)
275
- except Exception as e:
276
- raise TypeError(f"Operation {label} failed: {e}")
277
-
278
- return self._with_data(result)
279
-
280
- def __add__(self, other): return self._apply_op(other, np.add, "+")
281
- def __sub__(self, other): return self._apply_op(other, np.subtract, "-")
282
- def __mul__(self, other): return self._apply_op(other, np.multiply, "*")
283
- def __truediv__(self, other): return self._apply_op(other, np.divide, "/")
284
- def __pow__(self, other): return self._apply_op(other, np.power, "**")
285
-
286
- def __radd__(self, other): return self.__add__(other)
287
- def __rsub__(self, other): return self._apply_op(other, lambda a, b: b - a, "r-")
288
- def __rmul__(self, other): return self.__mul__(other)
289
- def __rtruediv__(self, other): return self._apply_op(other, lambda a, b: b / a, "r/")
290
- def __rpow__(self, other): return self._apply_op(other, lambda a, b: b ** a, "r**")
291
-
292
- def __neg__(self): return self._with_data(-self.data)
293
- def __abs__(self): return self._with_data(np.abs(self.data))
294
-
34
+
@@ -0,0 +1,329 @@
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 FrequencyDomainSignal(ModusaSignal):
12
+ """
13
+ Represents a 1D signal in the frequency domain.
14
+
15
+ Note
16
+ ----
17
+ - The class is not intended to be instantiated directly
18
+ This class stores the Complex spectrum of a signal
19
+ along with its corresponding frequency axis. It optionally tracks the time
20
+ origin (`t0`) of the spectral slice, which is useful when working with
21
+ time-localized spectral data (e.g., from a spectrogram or short-time Fourier transform).
22
+
23
+ Parameters
24
+ ----------
25
+ spectrum : np.ndarray
26
+ The frequency-domain representation of the signal (real or complex-valued).
27
+ f : np.ndarray
28
+ The frequency axis corresponding to the spectrum values. Must match the shape of `spectrum`.
29
+ t0 : float, optional
30
+ The time (in seconds) corresponding to the origin of this spectral slice. Defaults to 0.0.
31
+ title : str, optional
32
+ An optional title for display or plotting purposes.
33
+ """
34
+ #--------Meta Information----------
35
+ _name = ""
36
+ _description = ""
37
+ _author_name = "Ankit Anand"
38
+ _author_email = "ankit0.anand0@gmail.com"
39
+ _created_at = "2025-07-09"
40
+ #----------------------------------
41
+
42
+ @validate_args_type()
43
+ def __init__(self, spectrum: np.ndarray, f: np.ndarray, t0: float | int = 0.0, title: str | None = None):
44
+ super().__init__() # Instantiating `ModusaSignal` class
45
+
46
+ if spectrum.shape != f.shape:
47
+ raise excp.InputValueError(f"`spectrum` and `f` shape must match, got {spectrum.shape} and {f.shape}")
48
+
49
+
50
+ self._spectrum = spectrum
51
+ self._f = f
52
+ self._t0 = float(t0)
53
+
54
+ self.title = title or self._name # This title will be used as plot title by default
55
+
56
+
57
+ #----------------------
58
+ # Properties
59
+ #----------------------
60
+
61
+ @immutable_property("Create a new object instead.")
62
+ def spectrum(self) -> np.ndarray:
63
+ """Complex valued spectrum data."""
64
+ return self._spectrum
65
+
66
+ @immutable_property("Create a new object instead.")
67
+ def f(self) -> np.ndarray:
68
+ """frequency array of the spectrum."""
69
+ return self._f
70
+
71
+ @immutable_property("Create a new object instead.")
72
+ def t0(self) -> np.ndarray:
73
+ """Time origin (in seconds) of this spectral slice, e.g., from a spectrogram frame."""
74
+ return self._t0
75
+
76
+ def __len__(self):
77
+ return len(self._y)
78
+
79
+
80
+ #----------------------
81
+ # Tools
82
+ #----------------------
83
+
84
+ def __getitem__(self, key: slice) -> Self:
85
+ sliced_spectrum = self._spectrum[key]
86
+ sliced_f = self._f[key]
87
+ return self.__class__(spectrum=sliced_spectrum, f=sliced_f, t0=self.t0, title=self.title)
88
+
89
+ @validate_args_type()
90
+ def plot(
91
+ self,
92
+ scale_y: tuple[float, float] | None = None,
93
+ ax: plt.Axes | None = None,
94
+ color: str = "b",
95
+ marker: str | None = None,
96
+ linestyle: str | None = None,
97
+ stem: bool | None = False,
98
+ legend_loc: str | None = None,
99
+ title: str | None = None,
100
+ ylabel: str | None = "Amplitude",
101
+ xlabel: str | None = "Freq (Hz)",
102
+ ylim: tuple[float, float] | None = None,
103
+ xlim: tuple[float, float] | None = None,
104
+ highlight: list[tuple[float, float]] | None = None,
105
+ ) -> plt.Figure:
106
+ """
107
+ Plot the frequency-domain signal as a line or stem plot.
108
+
109
+ .. code-block:: python
110
+
111
+ spectrum.plot(stem=True, color="r", title="FFT Frame", xlim=(0, 5000))
112
+
113
+ Parameters
114
+ ----------
115
+ scale_y : tuple[float, float], optional
116
+ Range to scale the spectrum values before plotting (min, max).
117
+ ax : matplotlib.axes.Axes, optional
118
+ Axis to plot on. If None, a new figure and axis are created.
119
+ color : str, default="b"
120
+ Color of the line or stem.
121
+ marker : str, optional
122
+ Marker style for points (ignored if stem=True).
123
+ linestyle : str, optional
124
+ Line style for the plot (ignored if stem=True).
125
+ stem : bool, default=False
126
+ Whether to use a stem plot instead of a line plot.
127
+ legend_loc : str, optional
128
+ Legend location (e.g., 'upper right'). If None, no legend is shown.
129
+ title : str, optional
130
+ Title of the plot. Defaults to signal title.
131
+ ylabel : str, default="Amplitude"
132
+ Label for the y-axis.
133
+ xlabel : str, default="Freq (Hz)"
134
+ Label for the x-axis.
135
+ ylim : tuple[float, float], optional
136
+ Limits for the y-axis.
137
+ xlim : tuple[float, float], optional
138
+ Limits for the x-axis.
139
+ highlight : list[tuple[float, float]], optional
140
+ Regions to highlight on the frequency axis as shaded spans.
141
+
142
+ Returns
143
+ -------
144
+ matplotlib.figure.Figure
145
+ The figure containing the plotted signal.
146
+
147
+ Note
148
+ ----
149
+ - If `ax` is provided, the plot is drawn on it; otherwise, a new figure is created.
150
+ - `highlight` can be used to emphasize frequency bands (e.g., formants, harmonics).
151
+ - Use `scale_y` to clip or normalize extreme values before plotting.
152
+ """
153
+
154
+
155
+ from modusa.io import Plotter
156
+
157
+ title = title or self.title
158
+
159
+ fig: plt.Figure | None = Plotter.plot_signal(y=self.spectrum, x=self.f, scale_y=scale_y, ax=ax, color=color, marker=marker, linestyle=linestyle, stem=stem, legend_loc=legend_loc, title=title, ylabel=ylabel, xlabel=xlabel, ylim=ylim, xlim=xlim, highlight=highlight)
160
+
161
+ return fig
162
+
163
+ #----------------------------
164
+ # Math ops
165
+ #----------------------------
166
+
167
+ def __array__(self, dtype=None):
168
+ return np.asarray(self.spectrum, dtype=dtype)
169
+
170
+ def __add__(self, other):
171
+ other_data = other.spectrum if isinstance(other, self.__class__) else other
172
+ result = np.add(self.spectrum, other_data)
173
+ return self.__class__(spectrum=result, f=self.f, t0=self.t0, title=self.title)
174
+
175
+ def __radd__(self, other):
176
+ result = np.add(other, self.spectrum)
177
+ return self.__class__(spectrum=result, f=self.f, t0=self.t0, title=self.title)
178
+
179
+ def __sub__(self, other):
180
+ other_data = other.spectrum if isinstance(other, self.__class__) else other
181
+ result = np.subtract(self.spectrum, other_data)
182
+ return self.__class__(spectrum=result, f=self.f, t0=self.t0, title=self.title)
183
+
184
+ def __rsub__(self, other):
185
+ result = np.subtract(other, self.spectrum)
186
+ return self.__class__(spectrum=result, f=self.f, t0=self.t0, title=self.title)
187
+
188
+ def __mul__(self, other):
189
+ other_data = other.spectrum if isinstance(other, self.__class__) else other
190
+ result = np.multiply(self.spectrum, other_data)
191
+ return self.__class__(spectrum=result, f=self.f, t0=self.t0, title=self.title)
192
+
193
+ def __rmul__(self, other):
194
+ result = np.multiply(other, self.spectrum)
195
+ return self.__class__(spectrum=result, f=self.f, t0=self.t0, title=self.title)
196
+
197
+ def __truediv__(self, other):
198
+ other_data = other.spectrum if isinstance(other, self.__class__) else other
199
+ result = np.true_divide(self.spectrum, other_data)
200
+ return self.__class__(spectrum=result, f=self.f, t0=self.t0, title=self.title)
201
+
202
+ def __rtruediv__(self, other):
203
+ result = np.true_divide(other, self.spectrum)
204
+ return self.__class__(spectrum=result, f=self.f, t0=self.t0, title=self.title)
205
+
206
+ def __floordiv__(self, other):
207
+ other_data = other.spectrum if isinstance(other, self.__class__) else other
208
+ result = np.floor_divide(self.spectrum, other_data)
209
+ return self.__class__(spectrum=result, f=self.f, t0=self.t0, title=self.title)
210
+
211
+ def __rfloordiv__(self, other):
212
+ result = np.floor_divide(other, self.spectrum)
213
+ return self.__class__(spectrum=result, f=self.f, t0=self.t0, title=self.title)
214
+
215
+ def __pow__(self, other):
216
+ other_data = other.spectrum if isinstance(other, self.__class__) else other
217
+ result = np.power(self.spectrum, other_data)
218
+ return self.__class__(spectrum=result, f=self.f, t0=self.t0, title=self.title)
219
+
220
+ def __rpow__(self, other):
221
+ result = np.power(other, self.spectrum)
222
+ return self.__class__(spectrum=result, f=self.f, t0=self.t0, title=self.title)
223
+
224
+ def __abs__(self):
225
+ result = np.abs(self.spectrum)
226
+ return self.__class__(spectrum=result, f=self.f, t0=self.t0, title=self.title)
227
+
228
+
229
+ #--------------------------
230
+ # Other signal ops
231
+ #--------------------------
232
+ def sin(self) -> Self:
233
+ """Compute the element-wise sine of the signal data."""
234
+ result = np.sin(self.spectrum)
235
+ return self.__class__(spectrum=result, f=self.f, t0=self.t0, title=self.title)
236
+
237
+ def cos(self) -> Self:
238
+ """Compute the element-wise cosine of the signal data."""
239
+ result = np.cos(self.spectrum)
240
+ return self.__class__(spectrum=result, f=self.f, t0=self.t0, title=self.title)
241
+
242
+ def exp(self) -> Self:
243
+ """Compute the element-wise exponential of the signal data."""
244
+ result = np.exp(self.spectrum)
245
+ return self.__class__(spectrum=result, f=self.f, t0=self.t0, title=self.title)
246
+
247
+ def tanh(self) -> Self:
248
+ """Compute the element-wise hyperbolic tangent of the signal data."""
249
+ result = np.tanh(self.spectrum)
250
+ return self.__class__(spectrum=result, f=self.f, t0=self.t0, title=self.title)
251
+
252
+ def log(self) -> Self:
253
+ """Compute the element-wise natural logarithm of the signal data."""
254
+ result = np.log(self.spectrum)
255
+ return self.__class__(spectrum=result, f=self.f, t0=self.t0, title=self.title)
256
+
257
+ def log1p(self) -> Self:
258
+ """Compute the element-wise natural logarithm of (1 + signal data)."""
259
+ result = np.log1p(self.spectrum)
260
+ return self.__class__(spectrum=result, f=self.f, t0=self.t0, title=self.title)
261
+
262
+ def log10(self) -> Self:
263
+ """Compute the element-wise base-10 logarithm of the signal data."""
264
+ result = np.log10(self.spectrum)
265
+ return self.__class__(spectrum=result, f=self.f, t0=self.t0, title=self.title)
266
+
267
+ def log2(self) -> Self:
268
+ """Compute the element-wise base-2 logarithm of the signal data."""
269
+ result = np.log2(self.spectrum)
270
+ return self.__class__(spectrum=result, f=self.f, t0=self.t0, title=self.title)
271
+
272
+
273
+ #--------------------------
274
+ # Aggregation signal ops
275
+ #--------------------------
276
+ def mean(self) -> float:
277
+ """Compute the mean of the signal data."""
278
+ return float(np.mean(self.spectrum))
279
+
280
+ def std(self) -> float:
281
+ """Compute the standard deviation of the signal data."""
282
+ return float(np.std(self.spectrum))
283
+
284
+ def min(self) -> float:
285
+ """Compute the minimum value in the signal data."""
286
+ return float(np.min(self.spectrum))
287
+
288
+ def max(self) -> float:
289
+ """Compute the maximum value in the signal data."""
290
+ return float(np.max(self.spectrum))
291
+
292
+ def sum(self) -> float:
293
+ """Compute the sum of the signal data."""
294
+ return float(np.sum(self.spectrum))
295
+
296
+ #-----------------------------------
297
+ # Repr
298
+ #-----------------------------------
299
+
300
+ def __str__(self):
301
+ cls = self.__class__.__name__
302
+ data = self.spectrum
303
+
304
+ arr_str = np.array2string(
305
+ data,
306
+ separator=", ",
307
+ threshold=50, # limit number of elements shown
308
+ edgeitems=3, # show first/last 3 rows and columns
309
+ max_line_width=120, # avoid wrapping
310
+ formatter={'float_kind': lambda x: f"{x:.4g}"}
311
+ )
312
+
313
+ return f"Signal({arr_str}, shape={data.shape}, kind={cls})"
314
+
315
+ def __repr__(self):
316
+ cls = self.__class__.__name__
317
+ data = self.spectrum
318
+
319
+ arr_str = np.array2string(
320
+ data,
321
+ separator=", ",
322
+ threshold=50, # limit number of elements shown
323
+ edgeitems=3, # show first/last 3 rows and columns
324
+ max_line_width=120, # avoid wrapping
325
+ formatter={'float_kind': lambda x: f"{x:.4g}"}
326
+ )
327
+
328
+ return f"Signal({arr_str}, shape={data.shape}, kind={cls})"
329
+