modusa 0.4.28__py3-none-any.whl → 0.4.30__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 (58) hide show
  1. modusa/__init__.py +9 -8
  2. modusa/tools/__init__.py +7 -2
  3. modusa/tools/ann_saver.py +30 -0
  4. modusa/tools/audio_recorder.py +0 -1
  5. modusa/tools/synth.py +2 -0
  6. modusa/tools/youtube_downloader.py +1 -4
  7. {modusa-0.4.28.dist-info → modusa-0.4.30.dist-info}/METADATA +2 -2
  8. modusa-0.4.30.dist-info/RECORD +21 -0
  9. pyproject.toml +2 -2
  10. modusa/config.py +0 -18
  11. modusa/decorators.py +0 -176
  12. modusa/devtools/generate_docs_source.py +0 -92
  13. modusa/devtools/generate_template.py +0 -144
  14. modusa/devtools/list_authors.py +0 -2
  15. modusa/devtools/list_plugins.py +0 -60
  16. modusa/devtools/main.py +0 -45
  17. modusa/devtools/templates/generator.py +0 -24
  18. modusa/devtools/templates/io.py +0 -24
  19. modusa/devtools/templates/model.py +0 -47
  20. modusa/devtools/templates/plugin.py +0 -41
  21. modusa/devtools/templates/test.py +0 -10
  22. modusa/devtools/templates/tool.py +0 -24
  23. modusa/generators/__init__.py +0 -13
  24. modusa/generators/audio.py +0 -188
  25. modusa/generators/audio_waveforms.py +0 -236
  26. modusa/generators/base.py +0 -29
  27. modusa/generators/ftds.py +0 -298
  28. modusa/generators/s1d.py +0 -270
  29. modusa/generators/s2d.py +0 -300
  30. modusa/generators/s_ax.py +0 -102
  31. modusa/generators/t_ax.py +0 -64
  32. modusa/generators/tds.py +0 -267
  33. modusa/models/__init__.py +0 -14
  34. modusa/models/audio.py +0 -90
  35. modusa/models/base.py +0 -70
  36. modusa/models/data.py +0 -457
  37. modusa/models/ftds.py +0 -584
  38. modusa/models/s1d.py +0 -578
  39. modusa/models/s2d.py +0 -619
  40. modusa/models/s_ax.py +0 -448
  41. modusa/models/t_ax.py +0 -335
  42. modusa/models/tds.py +0 -465
  43. modusa/plugins/__init__.py +0 -3
  44. modusa/plugins/base.py +0 -100
  45. modusa/tools/_plotter_old.py +0 -629
  46. modusa/tools/audio_saver.py +0 -30
  47. modusa/tools/base.py +0 -43
  48. modusa/tools/math_ops.py +0 -335
  49. modusa/utils/__init__.py +0 -1
  50. modusa/utils/config.py +0 -25
  51. modusa/utils/excp.py +0 -49
  52. modusa/utils/logger.py +0 -18
  53. modusa/utils/np_func_cat.py +0 -44
  54. modusa/utils/plot.py +0 -142
  55. modusa-0.4.28.dist-info/RECORD +0 -65
  56. {modusa-0.4.28.dist-info → modusa-0.4.30.dist-info}/WHEEL +0 -0
  57. {modusa-0.4.28.dist-info → modusa-0.4.30.dist-info}/entry_points.txt +0 -0
  58. {modusa-0.4.28.dist-info → modusa-0.4.30.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
- #===================================