modusa 0.1.0__py3-none-any.whl → 0.2.0__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.
- modusa/.DS_Store +0 -0
- modusa/decorators.py +5 -5
- modusa/devtools/generate_template.py +80 -15
- modusa/devtools/main.py +6 -4
- modusa/devtools/templates/{engines.py → engine.py} +8 -7
- modusa/devtools/templates/{generators.py → generator.py} +8 -10
- modusa/devtools/templates/io.py +24 -0
- modusa/devtools/templates/{plugins.py → plugin.py} +7 -6
- modusa/devtools/templates/signal.py +40 -0
- modusa/devtools/templates/test.py +11 -0
- modusa/engines/.DS_Store +0 -0
- modusa/engines/__init__.py +1 -2
- modusa/generators/__init__.py +3 -1
- modusa/generators/audio_waveforms.py +227 -0
- modusa/generators/base.py +14 -25
- modusa/io/__init__.py +9 -0
- modusa/io/audio_converter.py +76 -0
- modusa/io/audio_loader.py +212 -0
- modusa/io/audio_player.py +72 -0
- modusa/io/base.py +43 -0
- modusa/io/plotter.py +430 -0
- modusa/io/youtube_downloader.py +139 -0
- modusa/main.py +15 -17
- modusa/plugins/__init__.py +1 -7
- modusa/signals/__init__.py +4 -6
- modusa/signals/audio_signal.py +421 -175
- modusa/signals/base.py +11 -271
- modusa/signals/frequency_domain_signal.py +329 -0
- modusa/signals/signal_ops.py +158 -0
- modusa/signals/spectrogram.py +465 -0
- modusa/signals/time_domain_signal.py +309 -0
- modusa/utils/excp.py +5 -0
- {modusa-0.1.0.dist-info → modusa-0.2.0.dist-info}/METADATA +15 -10
- modusa-0.2.0.dist-info/RECORD +47 -0
- modusa/devtools/templates/signals.py +0 -63
- modusa/engines/plot_1dsignal.py +0 -130
- modusa/engines/plot_2dmatrix.py +0 -159
- modusa/generators/basic_waveform.py +0 -185
- modusa/plugins/plot_1dsignal.py +0 -59
- modusa/plugins/plot_2dmatrix.py +0 -76
- modusa/plugins/plot_time_domain_signal.py +0 -59
- modusa/signals/signal1d.py +0 -311
- modusa/signals/signal2d.py +0 -226
- modusa/signals/uniform_time_domain_signal.py +0 -212
- modusa-0.1.0.dist-info/RECORD +0 -41
- {modusa-0.1.0.dist-info → modusa-0.2.0.dist-info}/WHEEL +0 -0
- {modusa-0.1.0.dist-info → modusa-0.2.0.dist-info}/entry_points.txt +0 -0
- {modusa-0.1.0.dist-info → modusa-0.2.0.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
|
|
14
|
+
Base class for any signal in the modusa framework.
|
|
13
15
|
|
|
14
16
|
Note
|
|
15
17
|
----
|
|
16
|
-
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
|
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
|
-
|
|
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
|
+
|