modusa 0.4.29__py3-none-any.whl → 0.4.31__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/__init__.py +12 -8
- modusa/tools/__init__.py +11 -3
- modusa/tools/ann_saver.py +30 -0
- modusa/tools/audio_recorder.py +0 -1
- modusa/tools/audio_stft.py +72 -0
- modusa/tools/youtube_downloader.py +1 -4
- {modusa-0.4.29.dist-info → modusa-0.4.31.dist-info}/METADATA +2 -2
- modusa-0.4.31.dist-info/RECORD +22 -0
- pyproject.toml +2 -2
- modusa/config.py +0 -18
- modusa/decorators.py +0 -176
- modusa/devtools/generate_docs_source.py +0 -92
- modusa/devtools/generate_template.py +0 -144
- modusa/devtools/list_authors.py +0 -2
- modusa/devtools/list_plugins.py +0 -60
- modusa/devtools/main.py +0 -45
- modusa/devtools/templates/generator.py +0 -24
- modusa/devtools/templates/io.py +0 -24
- modusa/devtools/templates/model.py +0 -47
- modusa/devtools/templates/plugin.py +0 -41
- modusa/devtools/templates/test.py +0 -10
- modusa/devtools/templates/tool.py +0 -24
- modusa/generators/__init__.py +0 -13
- modusa/generators/audio.py +0 -188
- modusa/generators/audio_waveforms.py +0 -236
- modusa/generators/base.py +0 -29
- modusa/generators/ftds.py +0 -298
- modusa/generators/s1d.py +0 -270
- modusa/generators/s2d.py +0 -300
- modusa/generators/s_ax.py +0 -102
- modusa/generators/t_ax.py +0 -64
- modusa/generators/tds.py +0 -267
- modusa/models/__init__.py +0 -14
- modusa/models/audio.py +0 -90
- modusa/models/base.py +0 -70
- modusa/models/data.py +0 -457
- modusa/models/ftds.py +0 -584
- modusa/models/s1d.py +0 -578
- modusa/models/s2d.py +0 -619
- modusa/models/s_ax.py +0 -448
- modusa/models/t_ax.py +0 -335
- modusa/models/tds.py +0 -465
- modusa/plugins/__init__.py +0 -3
- modusa/plugins/base.py +0 -100
- modusa/tools/_plotter_old.py +0 -629
- modusa/tools/audio_saver.py +0 -30
- modusa/tools/base.py +0 -43
- modusa/tools/math_ops.py +0 -335
- modusa/utils/__init__.py +0 -1
- modusa/utils/config.py +0 -25
- modusa/utils/excp.py +0 -49
- modusa/utils/logger.py +0 -18
- modusa/utils/np_func_cat.py +0 -44
- modusa/utils/plot.py +0 -142
- modusa-0.4.29.dist-info/RECORD +0 -65
- {modusa-0.4.29.dist-info → modusa-0.4.31.dist-info}/WHEEL +0 -0
- {modusa-0.4.29.dist-info → modusa-0.4.31.dist-info}/entry_points.txt +0 -0
- {modusa-0.4.29.dist-info → modusa-0.4.31.dist-info}/licenses/LICENSE.md +0 -0
modusa/models/ftds.py
DELETED
@@ -1,584 +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 .s2d import S2D
|
7
|
-
from .s_ax import SAx
|
8
|
-
from .t_ax import TAx
|
9
|
-
from .data import Data
|
10
|
-
from modusa.tools.math_ops import MathOps
|
11
|
-
from typing import Self, Any, Callable
|
12
|
-
import numpy as np
|
13
|
-
import matplotlib.pyplot as plt
|
14
|
-
import modusa as ms
|
15
|
-
|
16
|
-
class FTDS(S2D):
|
17
|
-
"""
|
18
|
-
Space to represent feature time domain signal (2D).
|
19
|
-
|
20
|
-
Note
|
21
|
-
----
|
22
|
-
- Use :class:`~modusa.generators.ftds.FTDSGen` API to instantiate this class.
|
23
|
-
- The signal must have uniform time axis thus `TAx`.
|
24
|
-
|
25
|
-
Parameters
|
26
|
-
----------
|
27
|
-
M: Data
|
28
|
-
- Data object holding the main 2D array.
|
29
|
-
f: SAx
|
30
|
-
- Feature-axis of the 2D signal.
|
31
|
-
t: TAx
|
32
|
-
- Time-axis of the 2D signal.
|
33
|
-
title: str
|
34
|
-
- What does the signal represent?
|
35
|
-
- e.g. "MySignal"
|
36
|
-
- This is used as the title while plotting.
|
37
|
-
"""
|
38
|
-
|
39
|
-
#--------Meta Information----------
|
40
|
-
_name = "Feature Time Domain Signal"
|
41
|
-
_nickname = "FTDS" # This is to be used in repr/str methods
|
42
|
-
_description = "Space to represent feature time domain signal (2D)."
|
43
|
-
_author_name = "Ankit Anand"
|
44
|
-
_author_email = "ankit0.anand0@gmail.com"
|
45
|
-
_created_at = "2025-07-21"
|
46
|
-
#----------------------------------
|
47
|
-
|
48
|
-
def __init__(self, M, f, t, title = None):
|
49
|
-
|
50
|
-
if not (isinstance(M, Data) and isinstance(f, SAx), isinstance(t, TAx)):
|
51
|
-
raise TypeError(f"`M` must be `Data` instance, `f` and `x` must be `SAx` and `TAx` instances, got {type(M)}, {type(f)} and {type(t)}")
|
52
|
-
|
53
|
-
super().__init__(M=M, y=f, x=t, title=title) # Instantiating `ModusaSignal` class
|
54
|
-
|
55
|
-
#--------------------------------------
|
56
|
-
# Properties
|
57
|
-
#--------------------------------------
|
58
|
-
|
59
|
-
@property
|
60
|
-
def M(self) -> Data:
|
61
|
-
return self._M
|
62
|
-
|
63
|
-
@property
|
64
|
-
def f(self) -> SAx:
|
65
|
-
return self._y
|
66
|
-
|
67
|
-
@property
|
68
|
-
def t(self) -> SAx:
|
69
|
-
return self._x
|
70
|
-
|
71
|
-
@property
|
72
|
-
def title(self) -> str:
|
73
|
-
return self._title
|
74
|
-
|
75
|
-
@property
|
76
|
-
def shape(self) -> tuple:
|
77
|
-
return self.M.shape
|
78
|
-
|
79
|
-
@property
|
80
|
-
def ndim(self) -> tuple:
|
81
|
-
return self.M.ndim # Should be 2
|
82
|
-
|
83
|
-
@property
|
84
|
-
def size(self) -> int:
|
85
|
-
return self.M.size
|
86
|
-
|
87
|
-
#===================================
|
88
|
-
|
89
|
-
|
90
|
-
#-------------------------------
|
91
|
-
# NumPy Protocol
|
92
|
-
#-------------------------------
|
93
|
-
|
94
|
-
def __array__(self, dtype=None) -> np.ndarray:
|
95
|
-
return np.asarray(self.M.values, dtype=dtype)
|
96
|
-
|
97
|
-
def __array_ufunc__(self, ufunc, method, *inputs, **kwargs):
|
98
|
-
"""
|
99
|
-
Supports NumPy universal functions on the Signal1D object.
|
100
|
-
"""
|
101
|
-
from .data import Data # Ensure this is the same Data class you're using
|
102
|
-
from modusa.utils import np_func_cat as nfc
|
103
|
-
|
104
|
-
raw_inputs = [
|
105
|
-
np.asarray(obj.M) if isinstance(obj, type(self)) else obj
|
106
|
-
for obj in inputs
|
107
|
-
]
|
108
|
-
|
109
|
-
result = getattr(ufunc, method)(*raw_inputs, **kwargs)
|
110
|
-
|
111
|
-
result = Data(values=result, label=None)
|
112
|
-
f = self.f.copy()
|
113
|
-
t = self.t.copy()
|
114
|
-
|
115
|
-
if result.shape[0] != f.shape[0] or result.shape[1] != t.shape[0]:
|
116
|
-
raise ValueError(f"`{ufunc.__name__}` caused shape mismatch between data and axis, please create a github issue")
|
117
|
-
|
118
|
-
return self.__class__(M=result, f=f, t=t, title=self.title)
|
119
|
-
|
120
|
-
def __array_function__(self, func, types, args, kwargs):
|
121
|
-
"""
|
122
|
-
Additional numpy function support for modusa signals.
|
123
|
-
Handles reduction and ufunc-like behavior.
|
124
|
-
"""
|
125
|
-
from .data import Data
|
126
|
-
from modusa.utils import np_func_cat as nfc
|
127
|
-
|
128
|
-
if not all(issubclass(t, type(self)) for t in types):
|
129
|
-
return NotImplemented
|
130
|
-
|
131
|
-
if func in nfc.CONCAT_FUNCS:
|
132
|
-
raise NotImplementedError(f"`{func.__name__}` is not yet tested on modusa signal, please create a GitHub issue.")
|
133
|
-
|
134
|
-
signal = args[0]
|
135
|
-
result: Data = func(signal.M, **kwargs)
|
136
|
-
axis = kwargs.get("axis", None)
|
137
|
-
keepdims = kwargs.get("keepdims", None)
|
138
|
-
|
139
|
-
if func in nfc.REDUCTION_FUNCS:
|
140
|
-
if keepdims is None or keepdims is True:
|
141
|
-
if axis is None: # Both axes collapsed
|
142
|
-
dummy_f = SAx(0, "")
|
143
|
-
dummy_t = TAx(n_points=1, sr=signal.t.sr, t0=0.0, label="")
|
144
|
-
return self.__class__(M=result, f=dummy_f, t=dummy_t, title=signal.title)
|
145
|
-
|
146
|
-
if isinstance(axis, int): # One of the axis collapsed
|
147
|
-
if axis == 0:
|
148
|
-
dummy_f = SAx(0, "")
|
149
|
-
return self.__class__(M=result, f=dummy_f, t=signal.t.copy(), title=signal.title)
|
150
|
-
elif axis in [1, -1]:
|
151
|
-
dummy_t = TAx(n_points=1, sr=signal.t.sr, t0=0.0, label="")
|
152
|
-
return self.__class__(M=result, f=signal.f.copy(), t=dummy_t, title=signal.title)
|
153
|
-
else:
|
154
|
-
raise ValueError
|
155
|
-
elif keepdims is False:
|
156
|
-
if axis is None: # Return Data
|
157
|
-
return result
|
158
|
-
if axis == 0: # Return TDS
|
159
|
-
from .tds import TDS
|
160
|
-
return TDS(y=result, t=signal.t, title=signal.title)
|
161
|
-
elif axis in [1, -1]: # Return S1D
|
162
|
-
from .s1d import S1D
|
163
|
-
return S1D(y=result, x=signal.f, title=signal.title)
|
164
|
-
else:
|
165
|
-
raise ValueError
|
166
|
-
|
167
|
-
|
168
|
-
# Case 3: Reduction keeps both axes (unlikely)
|
169
|
-
else:
|
170
|
-
raise NotImplementedError(f"{func.__name__} result shape={result.shape} not handled for modusa signal")
|
171
|
-
|
172
|
-
elif func in nfc.X_NEEDS_ADJUSTMENT_FUNCS:
|
173
|
-
raise NotImplementedError(f"{func.__name__} requires x-axis adjustment logic.")
|
174
|
-
|
175
|
-
else:
|
176
|
-
raise NotImplementedError(f"`{func.__name__}` is not yet tested on modusa signal, please create a GitHub issue.")
|
177
|
-
|
178
|
-
|
179
|
-
#================================
|
180
|
-
|
181
|
-
#-------------------------------
|
182
|
-
# Indexing
|
183
|
-
#-------------------------------
|
184
|
-
|
185
|
-
def __getitem__(self, key):
|
186
|
-
"""
|
187
|
-
Return a sliced or indexed view of the data.
|
188
|
-
|
189
|
-
Parameters
|
190
|
-
----------
|
191
|
-
key : int | slice | S2D
|
192
|
-
- Index to apply to the values.
|
193
|
-
|
194
|
-
Returns
|
195
|
-
-------
|
196
|
-
S2D | S1D | Data
|
197
|
-
- S2D object if slicing results in 2D array
|
198
|
-
- S1D object if slicing results in 1D array
|
199
|
-
- Data if slicing results in scalar
|
200
|
-
"""
|
201
|
-
from .s1d import S1D
|
202
|
-
from .tds import TDS
|
203
|
-
|
204
|
-
if isinstance(key, S1D):
|
205
|
-
raise TypeError(f"Applying `S1D` mask on `S2D` is not allowed.")
|
206
|
-
|
207
|
-
# We slice the data
|
208
|
-
sliced_M = self.M[key]
|
209
|
-
|
210
|
-
# Case 1: Row indexing only — return a horizontal slice (1D view across columns)
|
211
|
-
if isinstance(key, int):
|
212
|
-
sliced_f = self.f[key]
|
213
|
-
sliced_t = self.t
|
214
|
-
return TDS(y=sliced_M, x=sliced_t, title=self.title)
|
215
|
-
|
216
|
-
# Case 2: Column indexing only — return a vertical slice (1D view across rows)
|
217
|
-
elif isinstance(key, slice):
|
218
|
-
sliced_f = self.f[key]
|
219
|
-
sliced_t = self.t
|
220
|
-
return self.__class__(M=sliced_M, f=sliced_f, t=sliced_t, title=self.title)
|
221
|
-
|
222
|
-
# Case 3: 2D slicing
|
223
|
-
elif isinstance(key, tuple) and len(key) == 2:
|
224
|
-
row_key, col_key = key
|
225
|
-
if isinstance(row_key, int) and isinstance(col_key, int):
|
226
|
-
# Single value extraction → shape = (1, 1)
|
227
|
-
# Will return data object
|
228
|
-
return Data(values=sliced_M, title=self.title)
|
229
|
-
|
230
|
-
elif isinstance(row_key, int) and isinstance(col_key, slice):
|
231
|
-
# Row vector → return S1D
|
232
|
-
sliced_f = self.f[row_key] # Scalar
|
233
|
-
sliced_t = self.t[col_key]
|
234
|
-
|
235
|
-
return TDS(y=sliced_M, t=sliced_t, title=self.title)
|
236
|
-
|
237
|
-
elif isinstance(row_key, slice) and isinstance(col_key, int):
|
238
|
-
# Column vector → return S1D
|
239
|
-
sliced_f = self.f[row_key]
|
240
|
-
sliced_t = self.t[col_key] # Scalar
|
241
|
-
|
242
|
-
return S1D(y=sliced_M, x=sliced_f, title=self.title)
|
243
|
-
|
244
|
-
elif isinstance(row_key, slice) and isinstance(col_key, slice):
|
245
|
-
# 2D slice → return same class
|
246
|
-
sliced_f = self.f[row_key]
|
247
|
-
sliced_t = self.t[col_key]
|
248
|
-
|
249
|
-
return self.__class__(M=sliced_M, f=sliced_f, t=sliced_t, title=self.title)
|
250
|
-
|
251
|
-
# Case 4: Boolean masking signal
|
252
|
-
elif isinstance(key, type(self)):
|
253
|
-
sliced_f = self.f
|
254
|
-
sliced_t = self.t
|
255
|
-
|
256
|
-
return self.__class__(M=sliced_M, f=sliced_f, t=sliced_t, title=self.title)
|
257
|
-
|
258
|
-
else:
|
259
|
-
raise TypeError(f"Unsupported index type: {type(key)}")
|
260
|
-
|
261
|
-
def __setitem__(self, key, value):
|
262
|
-
"""
|
263
|
-
Set values at the specified index.
|
264
|
-
|
265
|
-
Parameters
|
266
|
-
----------
|
267
|
-
key : int | slice | array-like | boolean array | S1D
|
268
|
-
Index to apply to the values.
|
269
|
-
value : int | float | array-like
|
270
|
-
Value(s) to set.
|
271
|
-
"""
|
272
|
-
|
273
|
-
self.M[key] = value # In-place assignment
|
274
|
-
|
275
|
-
#===================================
|
276
|
-
|
277
|
-
#-------------------------------
|
278
|
-
# Basic arithmetic operations
|
279
|
-
#-------------------------------
|
280
|
-
def __add__(self, other):
|
281
|
-
if isinstance(other, type(self)):
|
282
|
-
if not self.has_same_axis_as(other):
|
283
|
-
raise ValueError("Axes are not aligned for the operation.")
|
284
|
-
return np.add(self, other)
|
285
|
-
|
286
|
-
def __radd__(self, other):
|
287
|
-
if isinstance(other, type(self)):
|
288
|
-
if not self.has_same_axis_as(other):
|
289
|
-
raise ValueError("Axes are not aligned for the operation.")
|
290
|
-
return np.add(other, self)
|
291
|
-
|
292
|
-
def __sub__(self, other):
|
293
|
-
if isinstance(other, type(self)):
|
294
|
-
if not self.has_same_axis_as(other):
|
295
|
-
raise ValueError("Axes are not aligned for the operation.")
|
296
|
-
return np.subtract(self, other)
|
297
|
-
|
298
|
-
def __rsub__(self, other):
|
299
|
-
if isinstance(other, type(self)):
|
300
|
-
if not self.has_same_axis_as(other):
|
301
|
-
raise ValueError("Axes are not aligned for the operation.")
|
302
|
-
return np.subtract(other, self)
|
303
|
-
|
304
|
-
def __mul__(self, other):
|
305
|
-
if isinstance(other, type(self)):
|
306
|
-
if not self.has_same_axis_as(other):
|
307
|
-
raise ValueError("Axes are not aligned for the operation.")
|
308
|
-
return np.multiply(self, other)
|
309
|
-
|
310
|
-
def __rmul__(self, other):
|
311
|
-
if isinstance(other, type(self)):
|
312
|
-
if not self.has_same_axis_as(other):
|
313
|
-
raise ValueError("Axes are not aligned for the operation.")
|
314
|
-
return np.multiply(other, self)
|
315
|
-
|
316
|
-
def __truediv__(self, other):
|
317
|
-
if isinstance(other, type(self)):
|
318
|
-
if not self.has_same_axis_as(other):
|
319
|
-
raise ValueError("Axes are not aligned for the operation.")
|
320
|
-
return np.divide(self, other)
|
321
|
-
|
322
|
-
def __rtruediv__(self, other):
|
323
|
-
if isinstance(other, type(self)):
|
324
|
-
if not self.has_same_axis_as(other):
|
325
|
-
raise ValueError("Axes are not aligned for the operation.")
|
326
|
-
return np.divide(other, self)
|
327
|
-
|
328
|
-
def __floordiv__(self, other):
|
329
|
-
if isinstance(other, type(self)):
|
330
|
-
if not self.has_same_axis_as(other):
|
331
|
-
raise ValueError("Axes are not aligned for the operation.")
|
332
|
-
return np.floor_divide(self, other)
|
333
|
-
|
334
|
-
def __rfloordiv__(self, other):
|
335
|
-
if not self.has_same_axis_as(other):
|
336
|
-
raise ValueError("Axes are not aligned for the operation.")
|
337
|
-
return np.floor_divide(other, self)
|
338
|
-
|
339
|
-
def __pow__(self, other):
|
340
|
-
if isinstance(other, type(self)):
|
341
|
-
if not self.has_same_axis_as(other):
|
342
|
-
raise ValueError("Axes are not aligned for the operation.")
|
343
|
-
return np.power(self, other)
|
344
|
-
|
345
|
-
def __rpow__(self, other):
|
346
|
-
if isinstance(other, type(self)):
|
347
|
-
if not self.has_same_axis_as(other):
|
348
|
-
raise ValueError("Axes are not aligned for the operation.")
|
349
|
-
return np.power(other, self)
|
350
|
-
|
351
|
-
#===============================
|
352
|
-
|
353
|
-
|
354
|
-
#-------------------------------
|
355
|
-
# Basic comparison operations
|
356
|
-
#-------------------------------
|
357
|
-
def __eq__(self, other):
|
358
|
-
return np.equal(self, other)
|
359
|
-
|
360
|
-
def __ne__(self, other):
|
361
|
-
return np.not_equal(self, other)
|
362
|
-
|
363
|
-
def __lt__(self, other):
|
364
|
-
return np.less(self, other)
|
365
|
-
|
366
|
-
def __le__(self, other):
|
367
|
-
return np.less_equal(self, other)
|
368
|
-
|
369
|
-
def __gt__(self, other):
|
370
|
-
return np.greater(self, other)
|
371
|
-
|
372
|
-
def __ge__(self, other):
|
373
|
-
return np.greater_equal(self, other)
|
374
|
-
|
375
|
-
#====================================
|
376
|
-
|
377
|
-
|
378
|
-
#-----------------------------------
|
379
|
-
# Utility Methods
|
380
|
-
#-----------------------------------
|
381
|
-
|
382
|
-
def unpack(self):
|
383
|
-
"""
|
384
|
-
Unpacks the object into easy to work
|
385
|
-
with data structures.
|
386
|
-
|
387
|
-
Returns
|
388
|
-
-------
|
389
|
-
(np.ndarray, np.ndarray, np.ndarray)
|
390
|
-
- M: Signal data array.
|
391
|
-
- f: Signal feature-axis array.
|
392
|
-
- t: Signal time-axis array.
|
393
|
-
"""
|
394
|
-
|
395
|
-
M = self.M.values
|
396
|
-
f = self.f.values
|
397
|
-
t = self.t.values
|
398
|
-
|
399
|
-
return (M, f, t)
|
400
|
-
|
401
|
-
def copy(self) -> Self:
|
402
|
-
"""
|
403
|
-
Returns a new copy of the signal.
|
404
|
-
|
405
|
-
Returns
|
406
|
-
-------
|
407
|
-
Self
|
408
|
-
A new copy of the object.
|
409
|
-
"""
|
410
|
-
|
411
|
-
copied_M = self.M.copy()
|
412
|
-
copied_f = self.f.copy()
|
413
|
-
copied_t = self.t.copy()
|
414
|
-
title = self.title # Immutable, hence no need to copy
|
415
|
-
|
416
|
-
return self.__class__(M=copied_M, f=copied_f, t=copied_t, title=title)
|
417
|
-
|
418
|
-
def set_meta_info(self, title = None, M_label = None, f_label = None, t_label = None) -> None:
|
419
|
-
"""
|
420
|
-
Set meta info about the signals.
|
421
|
-
|
422
|
-
Parameters
|
423
|
-
----------
|
424
|
-
title: str
|
425
|
-
- Title for the signal
|
426
|
-
- e.g. "MyTitle"
|
427
|
-
M_label: str
|
428
|
-
- Label for the data that matrix is holding.
|
429
|
-
- e.g. "Intensity (dB)"
|
430
|
-
f_label: str
|
431
|
-
- Label for the feature-axis.
|
432
|
-
- e.g. "Frequency (Hz)"
|
433
|
-
t_label: str
|
434
|
-
- Label for the time-axis.
|
435
|
-
- e.g. "Time (sec)"
|
436
|
-
Returns
|
437
|
-
-------
|
438
|
-
FTDS
|
439
|
-
A new instance with updated meta info.
|
440
|
-
"""
|
441
|
-
|
442
|
-
M, f, t = self.M, self.f, self.t
|
443
|
-
|
444
|
-
M_label = str(M_label) if M_label is not None else M.label
|
445
|
-
f_label = str(f_label) if f_label is not None else f.label
|
446
|
-
t_label = str(t_label) if t_label is not None else t.label
|
447
|
-
title = str(title) if title is not None else self.title
|
448
|
-
|
449
|
-
# We create a new copy of the data and axis
|
450
|
-
new_M = M.set_meta_info(label=M_label)
|
451
|
-
new_f = f.set_meta_info(label=f_label)
|
452
|
-
new_t = t.set_meta_info(label=t_label)
|
453
|
-
|
454
|
-
return self.__class__(M=new_M, f=new_f, t=new_t, title=title)
|
455
|
-
|
456
|
-
def translate_t(self, n_samples):
|
457
|
-
"""
|
458
|
-
Translate the FTDS signal along time axis
|
459
|
-
by `n_samples`.
|
460
|
-
|
461
|
-
Note
|
462
|
-
----
|
463
|
-
- `n_samples` can be both positive and negative.
|
464
|
-
- You might end up getting -ve time values as we are not checking for the values rn.
|
465
|
-
|
466
|
-
Parameters
|
467
|
-
----------
|
468
|
-
n_samples: int
|
469
|
-
- Number of samples to move the signal.
|
470
|
-
- +ve => moving signal forward.
|
471
|
-
- -ve => moving signal backward.
|
472
|
-
|
473
|
-
Returns
|
474
|
-
-------
|
475
|
-
FTDS
|
476
|
-
Translated signal.
|
477
|
-
"""
|
478
|
-
|
479
|
-
# We just need to create a new time axis with shifted t0
|
480
|
-
translated_t = self.t.translate(n_samples=n_samples)
|
481
|
-
|
482
|
-
translated_signal = self.__class__(M=self.M.copy(), f=self.f.copy(), t=translated_t, title=self.title)
|
483
|
-
|
484
|
-
return translated_signal
|
485
|
-
|
486
|
-
|
487
|
-
def mask(self, condition, set_to=None) -> Self:
|
488
|
-
"""
|
489
|
-
Mask the signal based on condition and
|
490
|
-
the values can be set.
|
491
|
-
|
492
|
-
Parameters
|
493
|
-
----------
|
494
|
-
condition: Callable
|
495
|
-
- Condition function to apply on values of the signal.
|
496
|
-
- E.g. lambda x: x > 10
|
497
|
-
set_to: Number
|
498
|
-
- Number to replace the masked position values.
|
499
|
-
|
500
|
-
Returns
|
501
|
-
-------
|
502
|
-
S2D
|
503
|
-
Masked Signal
|
504
|
-
"""
|
505
|
-
|
506
|
-
mask = condition(self)
|
507
|
-
new_val = set_to
|
508
|
-
|
509
|
-
if set_to is None: # Return the mask as the same signal but with booleans
|
510
|
-
return mask
|
511
|
-
|
512
|
-
else:
|
513
|
-
# We apply the mask and update the signal data
|
514
|
-
new_data = self.M.mask(condition=condition, set_to=new_val)
|
515
|
-
|
516
|
-
# Since we're just updating the data, there is no change in the axis
|
517
|
-
return self.__class__(M=new_data, f=self.f.copy(), t=self.t.copy(), title=self.title)
|
518
|
-
|
519
|
-
|
520
|
-
def pad(self, left=None, right=None) -> Self:
|
521
|
-
"""
|
522
|
-
Pad the signal with array like object from the
|
523
|
-
left or right.
|
524
|
-
|
525
|
-
Parameters
|
526
|
-
----------
|
527
|
-
left: arraylike
|
528
|
-
- What to pad to the left of the signal.
|
529
|
-
- E.g. 1 or [1, 0, 1], np.array([1, 2, 3])
|
530
|
-
right: arraylike
|
531
|
-
- What to pad to the right of the signal.
|
532
|
-
- E.g. 1 or [1, 0, 1], np.array([1, 2, 3])
|
533
|
-
|
534
|
-
Returns
|
535
|
-
-------
|
536
|
-
FTDS
|
537
|
-
Padded signal.
|
538
|
-
"""
|
539
|
-
|
540
|
-
if right is None and left is None: # No padding applied
|
541
|
-
return self
|
542
|
-
|
543
|
-
# Pad the data
|
544
|
-
M_padded = self.M.pad(left=left, right=right)
|
545
|
-
|
546
|
-
# Find the new t0
|
547
|
-
if left is not None:
|
548
|
-
if np.ndim(left) == 0: left = np.asarray([left])
|
549
|
-
else: left = np.asarray(left)
|
550
|
-
new_t0 = self.t.t0 - (left.shape[0] / self.t.sr)
|
551
|
-
else:
|
552
|
-
new_t0 = self.t.t0
|
553
|
-
|
554
|
-
t_padded = self.t.__class__(n_points=M_padded.shape[1], sr=self.t.sr, t0=new_t0, label=self.t.label)
|
555
|
-
|
556
|
-
return self.__class__(M=M_padded, f=self.f.copy(), t=t_padded, title=self.title)
|
557
|
-
|
558
|
-
#====================================
|
559
|
-
|
560
|
-
|
561
|
-
|
562
|
-
|
563
|
-
#-----------------------------------
|
564
|
-
# Info
|
565
|
-
#-----------------------------------
|
566
|
-
|
567
|
-
def print_info(self) -> None:
|
568
|
-
"""Print key information about the FTDS signal."""
|
569
|
-
print("-"*50)
|
570
|
-
print(f"{'Title':<20}: {self.title}")
|
571
|
-
print("-"*50)
|
572
|
-
print(f"{'Type':<20}: {self.__class__.__name__}")
|
573
|
-
print(f"{'Shape':<20}: {self.shape} (freq bins × time frames)")
|
574
|
-
print(f"{'Duration':<20}: {self.t.duration}")
|
575
|
-
print(f"{'Frame Rate':<20}: {self.t.sr} (frames / sec)")
|
576
|
-
print(f"{'Frame Duration':<20}: {1 / self.t.sr:.4f} sec ({(1 / self.t.sr) * 1000:.2f} ms)")
|
577
|
-
|
578
|
-
# Inheritance chain
|
579
|
-
cls_chain = " → ".join(cls.__name__ for cls in reversed(self.__class__.__mro__[:-1]))
|
580
|
-
print(f"{'Inheritance':<20}: {cls_chain}")
|
581
|
-
print("=" * 50)
|
582
|
-
|
583
|
-
|
584
|
-
#===================================
|