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/s2d.py DELETED
@@ -1,619 +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 .s_ax import SAx
8
- from .data import Data
9
- from modusa.tools.math_ops import MathOps
10
- from typing import Self, Any, Callable
11
- from types import NoneType
12
- import numpy as np
13
- import matplotlib.pyplot as plt
14
- import copy
15
-
16
- class S2D(ModusaSignal):
17
- """
18
- Space to represent 2D signal.
19
-
20
- Note
21
- ----
22
- - Use :class:`~modusa.generators.s2d.S2DGen` API to instantiate this class.
23
- - The signal can have uniform/non-uniform axes.
24
-
25
- Parameters
26
- ----------
27
- M: Data
28
- - Data object holding the main 2D array.
29
- y: SAx
30
- - Y-axis of the signal.
31
- x: SAx
32
- - X-axis of the 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 = "Signal 2D"
41
- _nickname = "signal" # This is to be used in repr/str methods
42
- _description = "Space to represent 2D 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, M, y, x, title = None):
49
- super().__init__() # Instantiating `ModusaSignal` class
50
-
51
- if not (isinstance(M, Data) and isinstance(y, SAx), isinstance(x, SAx)):
52
- raise TypeError(f"`M` must be `Data` instance, `y` and `x` must be `SAx` instances, got {type(M)}, {type(y)} and {type(x)}")
53
-
54
- assert M.ndim == 2
55
- assert M.shape[0] == y.shape[0], f"M and y shape mismatch"
56
- assert M.shape[1] == x.shape[0], f"M and x shape mismatch"
57
-
58
- # All these are private and we do not expose it to users directly.
59
- self._M = M
60
- self._y = y
61
- self._x = x
62
- self._title = title or self._name
63
-
64
- #--------------------------------------
65
- # Properties
66
- #--------------------------------------
67
-
68
- @property
69
- def M(self) -> Data:
70
- return self._M
71
-
72
- @property
73
- def y(self) -> SAx:
74
- return self._y
75
-
76
- @property
77
- def x(self) -> SAx:
78
- return self._x
79
-
80
- @property
81
- def title(self) -> str:
82
- return self._title
83
-
84
- @property
85
- def shape(self) -> tuple:
86
- return self.M.shape
87
-
88
- @property
89
- def ndim(self) -> tuple:
90
- return self.M.ndim # Should be 2
91
-
92
- @property
93
- def size(self) -> int:
94
- return self.M.size
95
-
96
- #===============================
97
-
98
-
99
- #-------------------------------
100
- # NumPy Protocol
101
- #-------------------------------
102
-
103
- def __array__(self, dtype=None) -> np.ndarray:
104
- return np.asarray(self.M.values, dtype=dtype)
105
-
106
- def __array_ufunc__(self, ufunc, method, *inputs, **kwargs):
107
- """
108
- Supports NumPy universal functions on the Signal1D object.
109
- """
110
- from .data import Data # Ensure this is the same Data class you're using
111
- from modusa.utils import np_func_cat as nfc
112
-
113
- raw_inputs = [
114
- np.asarray(obj.M) if isinstance(obj, type(self)) else obj
115
- for obj in inputs
116
- ]
117
-
118
- result = getattr(ufunc, method)(*raw_inputs, **kwargs)
119
-
120
- result = Data(values=result, label=None)
121
- y = self.y.copy()
122
- x = self.x.copy()
123
-
124
- if result.shape[0] != y.shape[0] or result.shape[1] != x.shape[0]:
125
- raise ValueError(f"`{ufunc.__name__}` caused shape mismatch between data and axis, please create a github issue")
126
-
127
- return self.__class__(M=result, y=y, x=x, title=self.title)
128
-
129
- def __array_function__(self, func, types, args, kwargs):
130
- """
131
- Additional numpy function support for modusa signals.
132
- Handles reduction and ufunc-like behavior.
133
- """
134
- from .data import Data
135
- from modusa.utils import np_func_cat as nfc
136
-
137
- if not all(issubclass(t, type(self)) for t in types):
138
- return NotImplemented
139
-
140
- if func in nfc.CONCAT_FUNCS:
141
- raise NotImplementedError(f"`{func.__name__}` is not yet tested on modusa signal, please create a GitHub issue.")
142
-
143
- signal = args[0]
144
- result: Data = func(signal.M, **kwargs)
145
- axis = kwargs.get("axis", None)
146
- keepdims = kwargs.get("keepdims", None)
147
-
148
- if func in nfc.REDUCTION_FUNCS:
149
- if keepdims is None or keepdims is True:
150
- if axis is None: # Both axes collapsed
151
- dummy_y = SAx(0, label=None)
152
- dummy_x = SAx(values=0, label=None)
153
- return self.__class__(M=result, y=dummy_y, x=dummy_x, title=signal.title)
154
-
155
- if isinstance(axis, int): # One of the axis collapsed
156
- if axis == 0:
157
- dummy_y = SAx(0, label=None)
158
- return self.__class__(M=result, y=dummy_y, x=signal.x.copy(), title=signal.title)
159
- elif axis in [1, -1]:
160
- dummy_x = SAx(values=0, label=None)
161
- return self.__class__(M=result, y=signal.y.copy(), x=dummy_x, title=signal.title)
162
- else:
163
- raise ValueError
164
- elif keepdims is False:
165
- if axis is None: # Return Data
166
- return result
167
- if axis == 0: # Return S1D
168
- from .s1d import S1D
169
- return S1D(y=result, x=signal.x, title=signal.title)
170
- elif axis in [1, -1]: # Return S1D
171
- from .s1d import S1D
172
- return S1D(y=result, x=signal.y, title=signal.title)
173
- else:
174
- raise ValueError
175
-
176
- elif func in nfc.X_NEEDS_ADJUSTMENT_FUNCS:
177
- raise NotImplementedError(f"{func.__name__} requires x-axis adjustment logic.")
178
-
179
- else:
180
- raise NotImplementedError(f"`{func.__name__}` is not yet tested on modusa signal, please create a GitHub issue.")
181
-
182
-
183
- #================================
184
-
185
- #-------------------------------
186
- # Indexing
187
- #-------------------------------
188
-
189
- def __getitem__(self, key):
190
- """
191
- Return a sliced or indexed view of the data.
192
-
193
- Parameters
194
- ----------
195
- key : int | slice | S2D
196
- - Index to apply to the values.
197
-
198
- Returns
199
- -------
200
- S2D
201
- - Another sliced S2D object
202
- """
203
-
204
- if not isinstance(key, (int, slice, tuple)):
205
- raise TypeError(f"Invalid key type {type(key)}")
206
-
207
- # We slice the data
208
- sliced_M = self.M[key]
209
-
210
- if isinstance(key, (int, slice)):
211
- sliced_y = self.y[key]
212
- sliced_x = self.x
213
-
214
- if isinstance(key, tuple):
215
- sliced_y = self.y[key[0]]
216
- sliced_x = self.x[key[1]]
217
-
218
- return self.__class__(M=sliced_M, y=sliced_y, x=sliced_x, title=self.title)
219
-
220
-
221
-
222
- def __setitem__(self, key, value):
223
- """
224
- Set values at the specified index.
225
-
226
- Parameters
227
- ----------
228
- key : int | slice | array-like | boolean array | S1D
229
- Index to apply to the values.
230
- value : int | float | array-like
231
- Value(s) to set.
232
- """
233
-
234
- self.M[key] = value # In-place assignment
235
-
236
- #===================================
237
-
238
- #-------------------------------
239
- # Basic arithmetic operations
240
- #-------------------------------
241
- def __add__(self, other):
242
- if isinstance(other, type(self)):
243
- if not self.has_same_axis_as(other):
244
- raise ValueError("Axes are not aligned for the operation.")
245
- return np.add(self, other)
246
-
247
- def __radd__(self, other):
248
- if isinstance(other, type(self)):
249
- if not self.has_same_axis_as(other):
250
- raise ValueError("Axes are not aligned for the operation.")
251
- return np.add(other, self)
252
-
253
- def __sub__(self, other):
254
- if isinstance(other, type(self)):
255
- if not self.has_same_axis_as(other):
256
- raise ValueError("Axes are not aligned for the operation.")
257
- return np.subtract(self, other)
258
-
259
- def __rsub__(self, other):
260
- if isinstance(other, type(self)):
261
- if not self.has_same_axis_as(other):
262
- raise ValueError("Axes are not aligned for the operation.")
263
- return np.subtract(other, self)
264
-
265
- def __mul__(self, other):
266
- if isinstance(other, type(self)):
267
- if not self.has_same_axis_as(other):
268
- raise ValueError("Axes are not aligned for the operation.")
269
- return np.multiply(self, other)
270
-
271
- def __rmul__(self, other):
272
- if isinstance(other, type(self)):
273
- if not self.has_same_axis_as(other):
274
- raise ValueError("Axes are not aligned for the operation.")
275
- return np.multiply(other, self)
276
-
277
- def __truediv__(self, other):
278
- if isinstance(other, type(self)):
279
- if not self.has_same_axis_as(other):
280
- raise ValueError("Axes are not aligned for the operation.")
281
- return np.divide(self, other)
282
-
283
- def __rtruediv__(self, other):
284
- if isinstance(other, type(self)):
285
- if not self.has_same_axis_as(other):
286
- raise ValueError("Axes are not aligned for the operation.")
287
- return np.divide(other, self)
288
-
289
- def __floordiv__(self, other):
290
- if isinstance(other, type(self)):
291
- if not self.has_same_axis_as(other):
292
- raise ValueError("Axes are not aligned for the operation.")
293
- return np.floor_divide(self, other)
294
-
295
- def __rfloordiv__(self, other):
296
- if not self.has_same_axis_as(other):
297
- raise ValueError("Axes are not aligned for the operation.")
298
- return np.floor_divide(other, self)
299
-
300
- def __pow__(self, other):
301
- if isinstance(other, type(self)):
302
- if not self.has_same_axis_as(other):
303
- raise ValueError("Axes are not aligned for the operation.")
304
- return np.power(self, other)
305
-
306
- def __rpow__(self, other):
307
- if isinstance(other, type(self)):
308
- if not self.has_same_axis_as(other):
309
- raise ValueError("Axes are not aligned for the operation.")
310
- return np.power(other, self)
311
-
312
- #===============================
313
-
314
-
315
- #-------------------------------
316
- # Basic comparison operations
317
- #-------------------------------
318
- def __eq__(self, other):
319
- return np.equal(self, other)
320
-
321
- def __ne__(self, other):
322
- return np.not_equal(self, other)
323
-
324
- def __lt__(self, other):
325
- return np.less(self, other)
326
-
327
- def __le__(self, other):
328
- return np.less_equal(self, other)
329
-
330
- def __gt__(self, other):
331
- return np.greater(self, other)
332
-
333
- def __ge__(self, other):
334
- return np.greater_equal(self, other)
335
-
336
- #===============================
337
-
338
-
339
- #-----------------------------------
340
- # Utility Methods
341
- #-----------------------------------
342
-
343
- def unpack(self):
344
- """
345
- Unpacks the object into easy to work
346
- with data structures.
347
-
348
- Returns
349
- -------
350
- (np.ndarray, np.ndarray, np.ndarray)
351
- - M: Signal data array.
352
- - y: Signal Y-axis array.
353
- - x: Signal X-axis array.
354
- """
355
-
356
- M = self.M.values
357
- y = self.y.values
358
- x = self.x.values
359
-
360
- return (M, y, x)
361
-
362
- def copy(self) -> Self:
363
- """
364
- Returns a new copy of the signal.
365
-
366
- Returns
367
- -------
368
- Self
369
- A new copy of the object.
370
- """
371
-
372
- copied_M = self.M.copy()
373
- copied_y = self.y.copy()
374
- copied_x = self.x.copy()
375
- title = self.title # Immutable, hence no need to copy
376
-
377
- return self.__class__(M=copied_M, y=copied_y, x=copied_x, title=title)
378
-
379
- def set_meta_info(self, title = None, M_label = None, y_label = None, x_label = None) -> None:
380
- """
381
- Set meta info about the signals.
382
-
383
- Parameters
384
- ----------
385
- title: str
386
- - Title for the signal
387
- - e.g. "MyTitle"
388
- M_label: str
389
- - Label for the data that matrix is holding.
390
- - e.g. "Intensity (dB)"
391
- y_label: str
392
- - Label for the y-axis.
393
- - e.g. "Frequency (Hz)"
394
- x_label: str
395
- - Label for the x-axis.
396
- - e.g. "Time (sec)"
397
- Returns
398
- -------
399
- S2D
400
- A new instance with updated meta info.
401
- """
402
-
403
- M, y, x = self.M, self.y, self.x
404
-
405
- M_label = str(M_label) if M_label is not None else M.label
406
- y_label = str(y_label) if y_label is not None else y.label
407
- x_label = str(x_label) if x_label is not None else x.label
408
- title = str(title) if title is not None else self.title
409
-
410
- # We create a new copy of the data and axis
411
- new_M = M.set_meta_info(label=M_label)
412
- new_y = y.set_meta_info(label=y_label)
413
- new_x = x.set_meta_info(label=x_label)
414
-
415
- return self.__class__(M=new_M, y=new_y, x=new_x, title=title)
416
-
417
-
418
- def is_same_as(self, other: Self) -> bool:
419
- """
420
- Check if two `S1D` instances are equal.
421
- """
422
-
423
- if not isinstance(other, type(self)):
424
- return False
425
-
426
- if not self.M.is_same_as(other.M):
427
- return False
428
-
429
- if not self.y.is_same_as(other.y):
430
- return False
431
-
432
- if not self.x.is_same_as(other.x):
433
- return False
434
-
435
- return True
436
-
437
- def has_same_axis_as(self, other) -> bool:
438
- """
439
- Check if two 'S1D' instances have same
440
- axis. Many operations need to satify this.
441
- """
442
- return self.y.is_same_as(other.y) and self.x.is_same_as(other.x)
443
-
444
-
445
- def mask(self, condition, set_to=None) -> Self:
446
- """
447
- Mask the signal based on condition and
448
- the values can be set.
449
-
450
- Parameters
451
- ----------
452
- condition: Callable
453
- - Condition function to apply on values of the signal.
454
- - E.g. lambda x: x > 10
455
- set_to: Number
456
- - Number to replace the masked position values.
457
-
458
- Returns
459
- -------
460
- S2D
461
- Masked Signal
462
- """
463
-
464
- mask = condition(self)
465
- new_val = set_to
466
-
467
- if set_to is None: # Return the mask as the same signal but with booleans
468
- return mask
469
-
470
- else:
471
- # We apply the mask and update the signal data
472
- new_data = self.M.mask(condition=condition, set_to=new_val)
473
-
474
- # Since we're just updating the data, there is no change in the axis
475
- return self.__class__(M=new_data, y=self.y.copy(), x=self.x.copy(), title=self.title)
476
-
477
- #===================================
478
-
479
-
480
-
481
-
482
-
483
-
484
- #-----------------------------------
485
- # Visualisation
486
- #-----------------------------------
487
-
488
- def plot(
489
- self,
490
- ax = None,
491
- cmap = "gray_r",
492
- title = None,
493
- M_label = None,
494
- x_label = None,
495
- y_label = None,
496
- y_lim = None,
497
- x_lim = None,
498
- highlight_regions = None,
499
- vlines = None,
500
- hlines = None,
501
- origin = "lower", # or "lower"
502
- gamma = None,
503
- show_colorbar = True,
504
- cax = None,
505
- show_grid = True,
506
- tick_mode = "center", # "center" or "edge"
507
- n_ticks = None,
508
- ) -> "plt.Figure":
509
- """
510
- Plot the S2D instance using Matplotlib.
511
-
512
-
513
- Parameters
514
- ----------
515
- ax : matplotlib.axes.Axes | None
516
- - 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.
517
- - If not passed, we create a new figure, plots the signal on it and then return the figure.
518
- cmap : str, default "gray_r"
519
- Colormap used for the image.
520
- title : str | None
521
- - Title for the plot.
522
- - If not passed, we use the default set during signal instantiation.
523
- y_lim : tuple[float, float] | None
524
- - Limits for the y-axis.
525
- x_lim : tuple[float, float] | None
526
- - Limits for the x-axis.
527
- vlines: list[float]
528
- - List of x values to draw vertical lines.
529
- - e.g. [10, 13.5]
530
- hlines: list[float]
531
- - List of data values to draw horizontal lines.
532
- - e.g. [10, 13.5]
533
- highlight_regions : list[tuple[float, float, str]] | None
534
- - List of time intervals to highlight on the plot.
535
- - [(start, end, 'tag')]
536
- origin : {"lower", "upper"}, default "lower"
537
- Origin position for the image (for flipping vertical axis).
538
- gamma : float or int, optional
539
- If specified, apply log-compression using log(1 + S * factor).
540
- show_colorbar : bool
541
- - Whether to display the colorbar.
542
- - Defaults to True.
543
- cax : matplotlib.axes.Axes | None
544
- - Axis to draw the colorbar on. If None, uses default placement.
545
- - Defaults to None
546
- show_grid : bool
547
- - Whether to show the major gridlines.
548
- - Defaults to None.
549
- tick_mode : {"center", "edge"}
550
- - Whether to place ticks at bin centers or edges.
551
- - Default to "center"
552
- n_ticks : tuple[int]
553
- - Number of ticks (y_ticks, x_ticks) to display on each axis.
554
- - Defaults to None
555
-
556
- Returns
557
- -------
558
- matplotlib.figure.Figure | None
559
- - The figure object containing the plot.
560
- - None if ax is provided.
561
- """
562
- from modusa.tools.plotter import Plotter
563
-
564
- M, y, x = self._M, self._y, self._x
565
- M_val, y_val, x_val = M._values, y._values, x._values
566
-
567
- M_label = M_label or M._label
568
- y_label = y_label or y._label
569
- x_label = x_label or x._label
570
-
571
- title = title or self._title
572
-
573
- fig = Plotter.plot_matrix(M=M_val, r=y_val, c=x_val, ax=ax, cmap=cmap, title=title, M_label=M_label, r_label=y_label, c_label=x_label, r_lim=y_lim, c_lim=x_lim,
574
- highlight_regions=highlight_regions, vlines=vlines, hlines=hlines, origin=origin, gamma=gamma, show_colorbar=show_colorbar, cax=cax, show_grid=show_grid,
575
- tick_mode=tick_mode, n_ticks=n_ticks)
576
-
577
- return fig
578
-
579
- #===================================
580
-
581
- #-----------------------------------
582
- # Information
583
- #-----------------------------------
584
-
585
- def print_info(self) -> None:
586
- """Print key information about the signal."""
587
-
588
- print("-"*50)
589
- print(f"{'Title':<20}: {self._title}")
590
- print("-"*50)
591
- print(f"{'Type':<20}: {self.__class__.__name__}")
592
- print(f"{'Shape':<20}: {self.shape} (freq bins × time frames)")
593
-
594
- # Inheritance chain
595
- cls_chain = " → ".join(cls.__name__ for cls in reversed(self.__class__.__mro__[:-1]))
596
- print(f"{'Inheritance':<20}: {cls_chain}")
597
- print("=" * 50)
598
-
599
- def __str__(self):
600
- arr_str = np.array2string(
601
- np.asarray(self),
602
- separator=", ",
603
- threshold=20, # limit number of elements shown
604
- edgeitems=2, # show first/last 2 rows and columns
605
- max_line_width=120, # avoid wrapping
606
- )
607
- return f"{self._nickname}({arr_str})"
608
-
609
- def __repr__(self):
610
- arr_str = np.array2string(
611
- np.asarray(self),
612
- separator=", ",
613
- threshold=20, # limit number of elements shown
614
- edgeitems=2, # show first/last 2 rows and columns
615
- max_line_width=120, # avoid wrapping
616
- )
617
- return f"{self._nickname}({arr_str})"
618
-
619
- #===================================