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.
Files changed (58) hide show
  1. modusa/__init__.py +12 -8
  2. modusa/tools/__init__.py +11 -3
  3. modusa/tools/ann_saver.py +30 -0
  4. modusa/tools/audio_recorder.py +0 -1
  5. modusa/tools/audio_stft.py +72 -0
  6. modusa/tools/youtube_downloader.py +1 -4
  7. {modusa-0.4.29.dist-info → modusa-0.4.31.dist-info}/METADATA +2 -2
  8. modusa-0.4.31.dist-info/RECORD +22 -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.29.dist-info/RECORD +0 -65
  56. {modusa-0.4.29.dist-info → modusa-0.4.31.dist-info}/WHEEL +0 -0
  57. {modusa-0.4.29.dist-info → modusa-0.4.31.dist-info}/entry_points.txt +0 -0
  58. {modusa-0.4.29.dist-info → modusa-0.4.31.dist-info}/licenses/LICENSE.md +0 -0
modusa/models/s1d.py DELETED
@@ -1,578 +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 .base import ModusaSignal
7
- from .data import Data
8
- from .s_ax import SAx
9
- from .t_ax import TAx
10
- from modusa.tools.math_ops import MathOps
11
- from typing import Self, Any, Callable
12
- from types import NoneType
13
- import numpy as np
14
- import matplotlib.pyplot as plt
15
- import copy
16
-
17
- class S1D(ModusaSignal):
18
- """
19
- Space to represent any 1D Signal.
20
-
21
- Note
22
- ----
23
- - Use :class:`~modusa.generators.s1d.S1DGen` API to instantiate this class.
24
- - The signal can have uniform/non-uniform axis.
25
-
26
-
27
- Parameters
28
- ----------
29
- data: Data
30
- - Data object holding the main array.
31
- sax: SAx
32
- - Axis for the signal.
33
- title: str
34
- - Title for the signal.
35
- - Default: None => ''
36
- - This is used as the title while plotting.
37
- """
38
-
39
- #--------Meta Information----------
40
- _name = "Signal 1D"
41
- _nickname = "signal" # This is to be used in repr/str methods
42
- _description = "Space to represent any 1D Signal."
43
- _author_name = "Ankit Anand"
44
- _author_email = "ankit0.anand0@gmail.com"
45
- _created_at = "2025-07-20"
46
- #----------------------------------
47
-
48
- def __init__(self, y, x, title = None):
49
- super().__init__() # Instantiating `ModusaSignal` class
50
-
51
- if not (isinstance(y, Data) and isinstance(x, SAx)):
52
- raise TypeError(f"`y` must be `Data` instance and `x` must be `SAx` instance, got {type(y)} and {type(x)}")
53
-
54
- assert y.ndim == 1
55
- assert y.shape == x.shape, f"y and x shape must match"
56
-
57
- # All these are private and we do not expose it to users directly.
58
- self._y = y
59
- self._x = x
60
- self._title = title or self._name
61
-
62
-
63
- #-----------------------------------
64
- # Properties (User Facing)
65
- #-----------------------------------
66
-
67
- @property
68
- def y(self) -> Data:
69
- return self._y
70
-
71
- @property
72
- def x(self) -> SAx:
73
- return self._x
74
-
75
- @property
76
- def title(self) -> str:
77
- return self._title
78
-
79
- @property
80
- def shape(self) -> tuple:
81
- return self.y.shape
82
-
83
- @property
84
- def ndim(self) -> int:
85
- return self.y.ndim # Should be 1
86
-
87
- @property
88
- def size(self) -> int:
89
- return self.y.size
90
-
91
- #===================================
92
-
93
- #-------------------------------
94
- # NumPy Protocol
95
- #-------------------------------
96
-
97
- def __array__(self, dtype=None) -> np.ndarray:
98
- return np.asarray(self.y.values, dtype=dtype)
99
-
100
- def __array_ufunc__(self, ufunc, method, *inputs, **kwargs):
101
- """
102
- Supports NumPy universal functions on the Signal1D object.
103
- """
104
- from .data import Data # Ensure this is the same Data class you're using
105
- from modusa.utils import np_func_cat as nfc
106
-
107
- raw_inputs = [
108
- np.asarray(obj.y) if isinstance(obj, type(self)) else obj
109
- for obj in inputs
110
- ]
111
-
112
- result = getattr(ufunc, method)(*raw_inputs, **kwargs)
113
-
114
- y = Data(values=result, label=None) # label=None or you could copy from self.y.label
115
- x = self.x.copy()
116
-
117
- if y.shape != x.shape:
118
- raise ValueError(f"`{ufunc.__name__}` caused shape mismatch between data and axis, please create a github issue")
119
-
120
- return self.__class__(y=y, x=x, title=self.title)
121
-
122
- def __array_function__(self, func, types, args, kwargs):
123
- """
124
- Additional numpy function support.
125
- """
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
- # Not supporting concatenate like operations as axis any random axis can't be concatenated
132
- if func in nfc.CONCAT_FUNCS:
133
- raise NotImplementedError(f"`{func.__name__}` is not yet tested on modusa signal, please create a GitHub issue.")
134
-
135
- # Single signal input expected
136
- signal = args[0]
137
- result: Data = func(signal.y, **kwargs)
138
- axis = kwargs.get("axis", None)
139
- keepdims = kwargs.get("keepdims", None)
140
-
141
- if func in nfc.REDUCTION_FUNCS:
142
- if keepdims is None or keepdims is True: # Default state is True => return 1D signal by wrapping the scalar
143
- from .s_ax import SAx
144
- dummy_x = SAx(0, "")
145
- return self.__class__(y=result, x=dummy_x, title=signal.title)
146
- elif keepdims is False: # Return Data
147
- from .data import Data
148
- return Data(values=result, label=None)
149
-
150
- elif func in nfc.X_NEEDS_ADJUSTMENT_FUNCS:
151
- # You must define logic for adjusting x
152
- raise NotImplementedError(f"{func.__name__} requires x-axis adjustment logic.")
153
-
154
- else:
155
- raise NotImplementedError(f"`{func.__name__}` is not yet tested on modusa signal, please create a GitHub issue.")
156
-
157
- #================================
158
-
159
- #-------------------------------
160
- # Indexing
161
- #-------------------------------
162
-
163
- def __getitem__(self, key):
164
- """
165
- Return a sliced or indexed view of the data.
166
-
167
- Parameters
168
- ----------
169
- key : int | slice | S1D
170
- - Index to apply to the values.
171
-
172
- Returns
173
- -------
174
- S1D
175
- A new S1D object with sliced values and same meta data.
176
- """
177
-
178
- if not isinstance(key, (int, slice)):
179
- raise TypeError(f"Invalid key type: {type(key)}")
180
-
181
- sliced_y = self.y[key]
182
- sliced_x = self.x[key]
183
-
184
- return self.__class__(y=sliced_y, x=sliced_x, title=self.title)
185
-
186
- def __setitem__(self, key, value):
187
- """
188
- Set values at the specified index.
189
-
190
- Parameters
191
- ----------
192
- key : int | slice | array-like | boolean array | S1D
193
- Index to apply to the values.
194
- value : int | float | array-like
195
- Value(s) to set.
196
- """
197
-
198
- if not isinstance(key, (int, slice)):
199
- raise TypeError(f"Invalid key type: {type(key)}")
200
-
201
- self.y[key] = value # In-place assignment
202
-
203
- #===================================
204
-
205
- #-------------------------------
206
- # Basic arithmetic operations
207
- #-------------------------------
208
- def __add__(self, other):
209
- if isinstance(other, type(self)):
210
- if not self.has_same_axis_as(other):
211
- raise ValueError("Axes are not aligned for the operation.")
212
- return np.add(self, other)
213
-
214
- def __radd__(self, other):
215
- if isinstance(other, type(self)):
216
- if not self.has_same_axis_as(other):
217
- raise ValueError("Axes are not aligned for the operation.")
218
- return np.add(other, self)
219
-
220
- def __sub__(self, other):
221
- if isinstance(other, type(self)):
222
- if not self.has_same_axis_as(other):
223
- raise ValueError("Axes are not aligned for the operation.")
224
- return np.subtract(self, other)
225
-
226
- def __rsub__(self, other):
227
- if isinstance(other, type(self)):
228
- if not self.has_same_axis_as(other):
229
- raise ValueError("Axes are not aligned for the operation.")
230
- return np.subtract(other, self)
231
-
232
- def __mul__(self, other):
233
- if isinstance(other, type(self)):
234
- if not self.has_same_axis_as(other):
235
- raise ValueError("Axes are not aligned for the operation.")
236
- return np.multiply(self, other)
237
-
238
- def __rmul__(self, other):
239
- if isinstance(other, type(self)):
240
- if not self.has_same_axis_as(other):
241
- raise ValueError("Axes are not aligned for the operation.")
242
- return np.multiply(other, self)
243
-
244
- def __truediv__(self, other):
245
- if isinstance(other, type(self)):
246
- if not self.has_same_axis_as(other):
247
- raise ValueError("Axes are not aligned for the operation.")
248
- return np.divide(self, other)
249
-
250
- def __rtruediv__(self, other):
251
- if isinstance(other, type(self)):
252
- if not self.has_same_axis_as(other):
253
- raise ValueError("Axes are not aligned for the operation.")
254
- return np.divide(other, self)
255
-
256
- def __floordiv__(self, other):
257
- if isinstance(other, type(self)):
258
- if not self.has_same_axis_as(other):
259
- raise ValueError("Axes are not aligned for the operation.")
260
- return np.floor_divide(self, other)
261
-
262
- def __rfloordiv__(self, other):
263
- if not self.has_same_axis_as(other):
264
- raise ValueError("Axes are not aligned for the operation.")
265
- return np.floor_divide(other, self)
266
-
267
- def __pow__(self, other):
268
- if isinstance(other, type(self)):
269
- if not self.has_same_axis_as(other):
270
- raise ValueError("Axes are not aligned for the operation.")
271
- return np.power(self, other)
272
-
273
- def __rpow__(self, other):
274
- if isinstance(other, type(self)):
275
- if not self.has_same_axis_as(other):
276
- raise ValueError("Axes are not aligned for the operation.")
277
- return np.power(other, self)
278
-
279
- #===============================
280
-
281
-
282
- #-------------------------------
283
- # Basic comparison operations
284
- #-------------------------------
285
- def __eq__(self, other):
286
- return np.equal(self, other)
287
-
288
- def __ne__(self, other):
289
- return np.not_equal(self, other)
290
-
291
- def __lt__(self, other):
292
- return np.less(self, other)
293
-
294
- def __le__(self, other):
295
- return np.less_equal(self, other)
296
-
297
- def __gt__(self, other):
298
- return np.greater(self, other)
299
-
300
- def __ge__(self, other):
301
- return np.greater_equal(self, other)
302
-
303
- #===============================
304
-
305
-
306
- #-----------------------------------
307
- # Utility Methods
308
- #-----------------------------------
309
-
310
- def unpack(self):
311
- """
312
- Unpacks the object into easy to work
313
- with data structures.
314
-
315
- Returns
316
- -------
317
- (np.ndarray, np.ndarray)
318
- - y: Signal data array.
319
- - x: Signal axis array.
320
- """
321
-
322
- y = self.y.values
323
- x = self.x.values
324
-
325
- return (y, x)
326
-
327
- def copy(self) -> Self:
328
- """
329
- Returns a new copy of the signal.
330
-
331
- Returns
332
- -------
333
- Self
334
- A new copy of the object.
335
- """
336
- copied_y = self.y.copy()
337
- copied_x = self.x.copy()
338
- title = self.title # Immutable, hence no need to copy
339
-
340
- return self.__class__(y=copied_y, x=copied_x, title=title)
341
-
342
- def set_meta_info(self, title=None, y_label=None, x_label=None) -> None:
343
- """
344
- Set meta info about the signal.
345
-
346
- Parameters
347
- ----------
348
- title: str
349
- - Title for the signal
350
- - e.g. "Speedometer"
351
- y_label: str
352
- - Label for the y-axis.
353
- - e.g. "Speeed (m/s)"
354
- x_label: str
355
- - Label for the x-axis.
356
- - e.g. "Distance (m)"
357
- """
358
-
359
- y, x = self.y, self.x
360
-
361
- new_title = str(title) if title is not None else self.title
362
- new_y_label = str(y_label) if y_label is not None else y.label
363
- new_x_label = str(x_label) if x_label is not None else x.label
364
-
365
- # We create a new copy of the data and axis
366
- new_y = y.__class__(values=y.values().copy(), label=y_label)
367
- new_x = x.__class__(values=x.values().copy(), label=x_label)
368
-
369
- return self.__class__(y=new_y, x=new_x, title=title)
370
-
371
-
372
- def is_same_as(self, other: Self) -> bool:
373
- """
374
- Check if two `S1D` instances are equal.
375
- """
376
-
377
- if not isinstance(other, type(self)):
378
- return False
379
-
380
- if not self.y.is_same_as(other.y):
381
- return False
382
-
383
- if not self.x.is_same_as(other.x):
384
- return False
385
-
386
- return True
387
-
388
- def has_same_axis_as(self, other) -> bool:
389
- """
390
- Check if two 'S1D' instances have same
391
- axis. Many operations need to satify this.
392
- """
393
- return self.x.is_same_as(other.x)
394
-
395
- def mask(self, condition, set_to=None) -> Self:
396
- """
397
- Mask the signal based on condition and
398
- the values can be set.
399
-
400
- Parameters
401
- ----------
402
- condition: Callable
403
- - Condition function to apply on values of the signal.
404
- - E.g. lambda x: x > 10
405
- set_to: Number
406
- - Number to replace the masked position values.
407
-
408
- Returns
409
- -------
410
- S1D
411
- Masked Signal
412
- """
413
-
414
- mask = condition(self)
415
- new_val = set_to
416
-
417
- if set_to is None: # Return the mask as the same signal but with booleans
418
- return mask
419
-
420
- else:
421
- # We apply the mask and update the signal data
422
- new_data = self.y.mask(condition=condition, set_to=new_val)
423
-
424
- # Since we're just updating the data, there is no change in the axis
425
- return self.__class__(y=new_data, x=self.x.copy(), title=self.title)
426
-
427
- #===================================
428
-
429
- #-----------------------------------
430
- # Tools
431
- #-----------------------------------
432
-
433
- def plot(
434
- self,
435
- ax = None,
436
- fmt: str = "k-",
437
- title = None,
438
- y_label = None,
439
- x_label = None,
440
- y_lim = None,
441
- x_lim = None,
442
- highlight_regions = None,
443
- vlines = None,
444
- hlines = None,
445
- legend = None,
446
- show_grid = True,
447
- show_stem = False,
448
- ) -> plt.Figure | None:
449
- """
450
- Plot the signal.
451
-
452
- .. code-block:: python
453
-
454
- import modusa as ms
455
- import numpy as np
456
- signal = ms.S1D(data=np.random.random(100), data_label="My data (unit)", title="My Random Signal")
457
- display(signal.plot())
458
-
459
- Parameters
460
- ----------
461
- ax : matplotlib.axes.Axes | None
462
- - If you want to plot the signal on a given matplotlib axes, you can pass the ax here. We do not return any figure in this case.
463
- - If not passed, we create a new figure, plots the signal on it and then return the figure.
464
- fmt : str
465
- - Format of the plot as per matplotlib standards (Eg. "k-" or "blue--o)
466
- - Default is "k-"
467
- title : str | None
468
- - Title for the plot.
469
- - If not passed, we use the default set during signal instantiation.
470
- y_label : str | None
471
- - Label for the y-axis.
472
- - If not passed, we use the default set during signal instantiation.
473
- x_label : str | None
474
- - Label for the x-axis.
475
- - If not passed, we use the default set during signal instantiation.
476
- y_lim : tuple[float, float] | None
477
- - Limits for the y-axis.
478
- x_lim : tuple[float, float] | None
479
- - Limits for the x-axis.
480
- highlight_regions : list[tuple[float, float, str]] | None
481
- - List of time intervals to highlight on the plot.
482
- - [(start, end, 'tag')]
483
- vlines: list[float]
484
- - List of x values to draw vertical lines.
485
- - e.g. [10, 13.5]
486
- hlines: list[float]
487
- - List of data values to draw horizontal lines.
488
- - e.g. [10, 13.5]
489
- show_grid: bool
490
- - If true, shows grid.
491
- - Default: True
492
- show_stem : bool
493
- - If True, use a stem plot instead of a continuous line.
494
- - Autorejects if signal is too large.
495
- legend : str | tuple[str, str] | None
496
- - If provided, adds a legend at the specified location.
497
- - e.g., "signal" -> gets converted into ("signal", "best")
498
- - e.g. ("signal", "upper right")
499
-
500
- Returns
501
- -------
502
- matplotlib.figure.Figure | None
503
- - The figure object containing the plot.
504
- - None in case an axis is provided.
505
-
506
- See Also
507
- --------
508
- modusa.tools.plotter.Plotter
509
- """
510
-
511
- from modusa.tools.plotter import Plotter
512
-
513
- y: Data = self._y
514
- x: SAx = self._x
515
- y_val = y._values
516
- x_val = x._values
517
-
518
- if y_label is None: y_label = self._y._label
519
- if x_label is None: x_label = self._x._label
520
- if title is None: title = self.title
521
-
522
- fig: plt.Figure | None = Plotter.plot_signal(y=y_val, x=x_val, ax=ax, fmt=fmt, title=title, y_label=y_label, x_label=x_label, y_lim=y_lim, x_lim=x_lim, highlight_regions=highlight_regions, vlines=vlines, hlines=hlines, show_grid=show_grid, show_stem=show_stem, legend=legend)
523
-
524
- return fig
525
-
526
- #===================================
527
-
528
-
529
-
530
- #----------------------------------
531
- # Information
532
- #----------------------------------
533
-
534
- def print_info(self) -> None:
535
- """Prints info about the audio."""
536
- print("-" * 50)
537
- print(f"{'Title'}: {self._title}")
538
- print("-" * 50)
539
- print(f"{'Type':<20}: {self.__class__.__name__}")
540
- print(f"{'Shape':<20}: {self.shape}")
541
-
542
- # Inheritance chain
543
- cls_chain = " → ".join(cls.__name__ for cls in reversed(self.__class__.__mro__[:-1]))
544
- print(f"{'Inheritance':<20}: {cls_chain}")
545
- print("=" * 50)
546
-
547
- def __str__(self):
548
- y, x = self._y, self._x
549
- y_label = y._label
550
- y_val = y._values
551
- shape = self.shape
552
-
553
-
554
- arr_str = np.array2string(
555
- y_val,
556
- separator=", ",
557
- threshold=30, # limit number of elements shown
558
- edgeitems=2, # show first/last 3 rows and columns
559
- max_line_width=120, # avoid wrapping
560
- )
561
-
562
- return f"{self._nickname}({arr_str})"
563
-
564
- def __repr__(self):
565
- y, x = self._y, self._x
566
- y_label = y._label
567
- y_val = y._values
568
- shape = self.shape
569
-
570
- arr_str = np.array2string(
571
- y_val,
572
- separator=", ",
573
- threshold=30, # limit number of elements shown
574
- edgeitems=2, # show first/last 3 rows and columns
575
- max_line_width=120, # avoid wrapping
576
- )
577
- return f"{self._nickname}({arr_str})"
578
- #===================================