modusa 0.2.22__py3-none-any.whl → 0.3__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 (70) hide show
  1. modusa/.DS_Store +0 -0
  2. modusa/__init__.py +8 -1
  3. modusa/decorators.py +4 -4
  4. modusa/devtools/generate_docs_source.py +96 -0
  5. modusa/devtools/generate_template.py +13 -13
  6. modusa/devtools/main.py +4 -3
  7. modusa/devtools/templates/generator.py +1 -1
  8. modusa/devtools/templates/io.py +1 -1
  9. modusa/devtools/templates/{signal.py → model.py} +18 -11
  10. modusa/devtools/templates/plugin.py +1 -1
  11. modusa/devtools/templates/test.py +2 -3
  12. modusa/devtools/templates/{engine.py → tool.py} +3 -8
  13. modusa/generators/__init__.py +9 -1
  14. modusa/generators/audio.py +188 -0
  15. modusa/generators/audio_waveforms.py +22 -13
  16. modusa/generators/base.py +1 -1
  17. modusa/generators/ftds.py +298 -0
  18. modusa/generators/s1d.py +270 -0
  19. modusa/generators/s2d.py +300 -0
  20. modusa/generators/s_ax.py +102 -0
  21. modusa/generators/t_ax.py +64 -0
  22. modusa/generators/tds.py +267 -0
  23. modusa/main.py +0 -30
  24. modusa/models/__init__.py +14 -0
  25. modusa/models/__pycache__/signal1D.cpython-312.pyc.4443461152 +0 -0
  26. modusa/models/audio.py +90 -0
  27. modusa/models/base.py +70 -0
  28. modusa/models/data.py +457 -0
  29. modusa/models/ftds.py +584 -0
  30. modusa/models/s1d.py +578 -0
  31. modusa/models/s2d.py +619 -0
  32. modusa/models/s_ax.py +448 -0
  33. modusa/models/t_ax.py +335 -0
  34. modusa/models/tds.py +465 -0
  35. modusa/plugins/__init__.py +3 -1
  36. modusa/tmp.py +98 -0
  37. modusa/tools/__init__.py +7 -0
  38. modusa/tools/audio_converter.py +73 -0
  39. modusa/tools/audio_loader.py +90 -0
  40. modusa/tools/audio_player.py +89 -0
  41. modusa/tools/base.py +43 -0
  42. modusa/tools/math_ops.py +335 -0
  43. modusa/tools/plotter.py +351 -0
  44. modusa/tools/youtube_downloader.py +72 -0
  45. modusa/utils/excp.py +15 -42
  46. modusa/utils/np_func_cat.py +44 -0
  47. modusa/utils/plot.py +142 -0
  48. {modusa-0.2.22.dist-info → modusa-0.3.dist-info}/METADATA +5 -16
  49. modusa-0.3.dist-info/RECORD +60 -0
  50. modusa/engines/.DS_Store +0 -0
  51. modusa/engines/__init__.py +0 -3
  52. modusa/engines/base.py +0 -14
  53. modusa/io/__init__.py +0 -9
  54. modusa/io/audio_converter.py +0 -76
  55. modusa/io/audio_loader.py +0 -214
  56. modusa/io/audio_player.py +0 -72
  57. modusa/io/base.py +0 -43
  58. modusa/io/plotter.py +0 -430
  59. modusa/io/youtube_downloader.py +0 -139
  60. modusa/signals/__init__.py +0 -7
  61. modusa/signals/audio_signal.py +0 -483
  62. modusa/signals/base.py +0 -34
  63. modusa/signals/frequency_domain_signal.py +0 -329
  64. modusa/signals/signal_ops.py +0 -158
  65. modusa/signals/spectrogram.py +0 -465
  66. modusa/signals/time_domain_signal.py +0 -309
  67. modusa-0.2.22.dist-info/RECORD +0 -47
  68. {modusa-0.2.22.dist-info → modusa-0.3.dist-info}/WHEEL +0 -0
  69. {modusa-0.2.22.dist-info → modusa-0.3.dist-info}/entry_points.txt +0 -0
  70. {modusa-0.2.22.dist-info → modusa-0.3.dist-info}/licenses/LICENSE.md +0 -0
@@ -1,483 +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 modusa.signals.base import ModusaSignal
7
- from modusa.signals.signal_ops import SignalOps
8
- from typing import Self, Any
9
- import numpy as np
10
- import matplotlib.pyplot as plt
11
- from pathlib import Path
12
-
13
- class AudioSignal(ModusaSignal):
14
- """
15
- Represents a 1D audio signal within modusa framework.
16
-
17
- Note
18
- ----
19
- - It is highly recommended to use :class:`~modusa.io.AudioLoader` to instantiate an object of this class.
20
- - This class assumes audio is mono (1D numpy array).
21
- - Either `sr` (sampling rate) or `t` (time axis) must be provided.
22
- - If both `t` and `sr` are given, `t` takes precedence for timing and `sr` is computed from that.
23
- - If `t` is provided but `sr` is missing, `sr` is estimated from the `t`.
24
- - If `t` is provided, the starting time `t0` will be overridden by `t[0]`.
25
-
26
- Parameters
27
- ----------
28
- y : np.ndarray
29
- 1D numpy array representing the audio signal.
30
- sr : int | None
31
- Sampling rate in Hz. Required if `t` is not provided.
32
- t : np.ndarray | None
33
- Optional time axis corresponding to `y`. Must be the same length as `y`.
34
- t0 : float, optional
35
- Starting time in seconds. Defaults to 0.0. Set to `t[0]` if `t` is provided.
36
- title : str | None, optional
37
- Optional title for the signal. Defaults to `"Audio Signal"`.
38
- """
39
-
40
- #--------Meta Information----------
41
- _name = "Audio Signal"
42
- _description = ""
43
- _author_name = "Ankit Anand"
44
- _author_email = "ankit0.anand0@gmail.com"
45
- _created_at = "2025-07-04"
46
- #----------------------------------
47
-
48
- @validate_args_type()
49
- def __init__(self, y: np.ndarray, sr: int | None = None, t: np.ndarray | None = None, t0: float = 0.0, title: str | None = None):
50
-
51
- if y.ndim != 1:
52
- raise excp.InputValueError(f"`y` must have 1 dimension, not {y.ndim}.")
53
-
54
- if t is not None:
55
- if len(t) != len(y):
56
- raise excp.InputValueError("Length of `t` must match `y`.")
57
- if sr is None:
58
- # Estimate sr from t if not provided
59
- dt = t[1] - t[0]
60
- sr = round(1.0 / dt) # Round to avoid floating-point drift
61
- t0 = float(t[0]) # Override t0 from first timestamp
62
-
63
- elif sr is None:
64
- raise excp.InputValueError("Either `sr` or `t` must be provided.")
65
-
66
- self._y = y
67
- self._sr = sr
68
- self._t0 = t0
69
- self.title = title or self._name
70
-
71
- #----------------------
72
- # Properties
73
- #----------------------
74
- @immutable_property("Create a new object instead.")
75
- def y(self) -> np.ndarray:
76
- """Audio data."""
77
- return self._y
78
-
79
- @immutable_property("Create a new object instead.")
80
- def sr(self) -> np.ndarray:
81
- """Sampling rate of the audio."""
82
- return self._sr
83
-
84
- @immutable_property("Create a new object instead.")
85
- def t0(self) -> np.ndarray:
86
- """Start timestamp of the audio."""
87
- return self._t0
88
-
89
- @immutable_property("Create a new object instead.")
90
- def t(self) -> np.ndarray:
91
- """Timestamp array of the audio."""
92
- return self.t0 + np.arange(len(self.y)) / self.sr
93
-
94
- @immutable_property("Mutation not allowed.")
95
- def Ts(self) -> int:
96
- """Sampling Period of the audio."""
97
- return 1.0 / self.sr
98
-
99
- @immutable_property("Mutation not allowed.")
100
- def duration(self) -> int:
101
- """Duration of the audio."""
102
- return len(self.y) / self.sr
103
-
104
- @immutable_property("Mutation not allowed.")
105
- def info(self) -> None:
106
- """Prints info about the audio."""
107
- print("-" * 50)
108
- print(f"{'Title':<20}: {self.title}")
109
- print(f"{'Kind':<20}: {self._name}")
110
- print(f"{'Duration':<20}: {self.duration:.2f} sec")
111
- print(f"{'Sampling Rate':<20}: {self.sr} Hz")
112
- print(f"{'Sampling Period':<20}: {(self.Ts*1000) :.4f} ms")
113
- print("-" * 50)
114
-
115
- #----------------------
116
- # Methods
117
- #----------------------
118
- def __getitem__(self, key):
119
- if isinstance(key, (int, slice)):
120
- # Basic slicing of 1D signals
121
- sliced_data = self._data[key]
122
- sliced_axis = self._axes[0][key] # assumes only 1 axis
123
-
124
- return self.replace(data=sliced_data, axes=(sliced_axis, ))
125
- else:
126
- raise TypeError(
127
- f"Indexing with type {type(key)} is not supported. Use int or slice."
128
- )
129
-
130
- @validate_args_type()
131
- def crop(self, t_min: int | float | None = None, t_max: int | float | None = None) -> "AudioSignal":
132
- """
133
- Crop the audio signal to a time range [t_min, t_max].
134
-
135
- .. code-block:: python
136
-
137
- from modusa.generators import AudioSignalGenerator
138
- audio_example = AudioSignalGenerator.generate_example()
139
- cropped_audio = audio_example.crop(1.5, 2)
140
-
141
- Parameters
142
- ----------
143
- t_min : float or None
144
- Inclusive lower time bound. If None, no lower bound.
145
- t_max : float or None
146
- Exclusive upper time bound. If None, no upper bound.
147
-
148
- Returns
149
- -------
150
- AudioSignal
151
- Cropped audio signal.
152
- """
153
- y = self.y
154
- t = self.t
155
-
156
- mask = np.ones_like(t, dtype=bool)
157
- if t_min is not None:
158
- mask &= (t >= t_min)
159
- if t_max is not None:
160
- mask &= (t < t_max)
161
-
162
- cropped_y = y[mask]
163
- new_t0 = t[mask][0] if np.any(mask) else self.t0 # fallback to original t0 if mask is empty
164
-
165
- return self.__class__(y=cropped_y, sr=self.sr, t0=new_t0, title=self.title)
166
-
167
-
168
- @validate_args_type()
169
- def plot(
170
- self,
171
- scale_y: tuple[float, float] | None = None,
172
- ax: plt.Axes | None = None,
173
- color: str = "b",
174
- marker: str | None = None,
175
- linestyle: str | None = None,
176
- stem: bool | None = False,
177
- legend_loc: str | None = None,
178
- title: str | None = None,
179
- ylabel: str | None = "Amplitude",
180
- xlabel: str | None = "Time (sec)",
181
- ylim: tuple[float, float] | None = None,
182
- xlim: tuple[float, float] | None = None,
183
- highlight: list[tuple[float, float]] | None = None,
184
- ) -> plt.Figure:
185
- """
186
- Plot the audio waveform using matplotlib.
187
-
188
- .. code-block:: python
189
-
190
- from modusa.generators import AudioSignalGenerator
191
- audio_example = AudioSignalGenerator.generate_example()
192
- audio_example.plot(color="orange", title="Example Audio")
193
-
194
- Parameters
195
- ----------
196
- scale_y : tuple of float, optional
197
- Range to scale the y-axis data before plotting. Useful for normalization.
198
- ax : matplotlib.axes.Axes, optional
199
- Pre-existing axes to plot into. If None, a new figure and axes are created.
200
- color : str, optional
201
- Color of the waveform line. Default is `"b"` (blue).
202
- marker : str or None, optional
203
- Marker style for each point. Follows matplotlib marker syntax.
204
- linestyle : str or None, optional
205
- Line style for the waveform. Follows matplotlib linestyle syntax.
206
- stem : bool, optional
207
- If True, use a stem plot instead of a continuous line.
208
- legend_loc : str or None, optional
209
- If provided, adds a legend at the specified location (e.g., "upper right").
210
- title : str or None, optional
211
- Plot title. Defaults to the signal’s title.
212
- ylabel : str or None, optional
213
- Label for the y-axis. Defaults to `"Amplitude"`.
214
- xlabel : str or None, optional
215
- Label for the x-axis. Defaults to `"Time (sec)"`.
216
- ylim : tuple of float or None, optional
217
- Limits for the y-axis.
218
- xlim : tuple of float or None, optional
219
- Limits for the x-axis.
220
- highlight : list of tuple of float or None, optional
221
- List of time intervals to highlight on the plot, each as (start, end).
222
-
223
- Returns
224
- -------
225
- matplotlib.figure.Figure
226
- The figure object containing the plot.
227
- """
228
-
229
- from modusa.io import Plotter
230
-
231
- title = title or self.title
232
-
233
- fig: plt.Figure | None = Plotter.plot_signal(y=self.y, x=self.t, 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)
234
-
235
- return fig
236
-
237
- def play(self, regions: list[tuple[float, float], ...] | None = None, title: str | None = None):
238
- """
239
- Play the audio signal inside a Jupyter Notebook.
240
-
241
- .. code-block:: python
242
-
243
- from modusa.generators import AudioSignalGenerator
244
- audio = AudioSignalGenerator.generate_example()
245
- audio.play(regions=[(0.0, 1.0), (2.0, 3.0)])
246
-
247
- Parameters
248
- ----------
249
- regions : list of tuple of float, optional
250
- List of (start_time, end_time) pairs in seconds specifying the regions to play.
251
- If None, the entire signal is played.
252
- title : str or None, optional
253
- Optional title for the player interface. Defaults to the signal’s internal title.
254
-
255
- Returns
256
- -------
257
- IPython.display.Audio
258
- An interactive audio player widget for Jupyter environments.
259
-
260
- Note
261
- ----
262
- - This method uses :class:`~modusa.io.AudioPlayer` to render an interactive audio player.
263
- - Optionally, specific regions of the signal can be played back, each defined by a (start, end) time pair.
264
- """
265
-
266
- from modusa.io import AudioPlayer
267
- audio_player = AudioPlayer.play(y=self.y, sr=self.sr, regions=regions, title=self.title)
268
-
269
- return audio_player
270
-
271
- def to_spectrogram(
272
- self,
273
- n_fft: int = 2048,
274
- hop_length: int = 512,
275
- win_length: int | None = None,
276
- window: str = "hann"
277
- ) -> "Spectrogram":
278
- """
279
- Compute the Short-Time Fourier Transform (STFT) and return a Spectrogram object.
280
-
281
- Parameters
282
- ----------
283
- n_fft : int
284
- FFT size.
285
- win_length : int or None
286
- Window length. Defaults to `n_fft` if None.
287
- hop_length : int
288
- Hop length between frames.
289
- window : str
290
- Type of window function to use (e.g., 'hann', 'hamming').
291
-
292
- Returns
293
- -------
294
- Spectrogram
295
- Spectrogram object containing S (complex STFT), t (time bins), and f (frequency bins).
296
- """
297
- from modusa.signals.spectrogram import Spectrogram
298
- import librosa
299
-
300
- S = librosa.stft(self.y, n_fft=n_fft, win_length=win_length, hop_length=hop_length, window=window)
301
- f = librosa.fft_frequencies(sr=self.sr, n_fft=n_fft)
302
- t = librosa.frames_to_time(np.arange(S.shape[1]), sr=self.sr, hop_length=hop_length)
303
- t += self.t0
304
- spec = Spectrogram(S=S, f=f, t=t)
305
- if self.title != self._name: # Means title of the audio was reset so we pass that info to spec
306
- spec.title = self.title
307
-
308
- return spec
309
-
310
-
311
- #----------------------------
312
- # Math ops
313
- #----------------------------
314
-
315
- def __array__(self, dtype=None):
316
- return np.asarray(self.y, dtype=dtype)
317
-
318
- def __array_ufunc__(self, ufunc, method, *inputs, **kwargs):
319
- if ufunc == np.abs and method == "__call__":
320
- # Extract the actual array from self or others
321
- result = ufunc(self.y, **kwargs)
322
- return self.__class__(y=result, sr=self.sr, title=f"{self.title} (abs)")
323
- return NotImplemented
324
-
325
- def __add__(self, other):
326
- other_data = other.y if isinstance(other, self.__class__) else other
327
- result = np.add(self.y, other_data)
328
- return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
329
-
330
- def __radd__(self, other):
331
- result = np.add(other, self.y)
332
- return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
333
-
334
- def __sub__(self, other):
335
- other_data = other.y if isinstance(other, self.__class__) else other
336
- result = np.subtract(self.y, other_data)
337
- return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
338
-
339
- def __rsub__(self, other):
340
- result = np.subtract(other, self.y)
341
- return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
342
-
343
- def __mul__(self, other):
344
- other_data = other.y if isinstance(other, self.__class__) else other
345
- result = np.multiply(self.y, other_data)
346
- return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
347
-
348
- def __rmul__(self, other):
349
- result = np.multiply(other, self.y)
350
- return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
351
-
352
- def __truediv__(self, other):
353
- other_data = other.y if isinstance(other, self.__class__) else other
354
- result = np.true_divide(self.y, other_data)
355
- return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
356
-
357
- def __rtruediv__(self, other):
358
- result = np.true_divide(other, self.y)
359
- return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
360
-
361
- def __floordiv__(self, other):
362
- other_data = other._y if isinstance(other, self.__class__) else other
363
- result = np.floor_divide(self.y, other_data)
364
- return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
365
-
366
- def __rfloordiv__(self, other):
367
- result = np.floor_divide(other, self.y)
368
- return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
369
-
370
- def __pow__(self, other):
371
- other_data = other.y if isinstance(other, self.__class__) else other
372
- result = np.power(self.y, other_data)
373
- return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
374
-
375
- def __rpow__(self, other):
376
- result = np.power(other, self.y)
377
- return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
378
-
379
- def __abs__(self):
380
- result = np.abs(self.y)
381
- return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
382
-
383
-
384
- #--------------------------
385
- # Other signal ops
386
- #--------------------------
387
- def sin(self) -> Self:
388
- """Compute the element-wise sine of the signal data."""
389
- result = np.sin(self.y)
390
- return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
391
-
392
- def cos(self) -> Self:
393
- """Compute the element-wise cosine of the signal data."""
394
- result = np.cos(self.y)
395
- return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
396
-
397
- def exp(self) -> Self:
398
- """Compute the element-wise exponential of the signal data."""
399
- result = np.exp(self.y)
400
- return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
401
-
402
- def tanh(self) -> Self:
403
- """Compute the element-wise hyperbolic tangent of the signal data."""
404
- result = np.tanh(self.y)
405
- return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
406
-
407
- def log(self) -> Self:
408
- """Compute the element-wise natural logarithm of the signal data."""
409
- result = np.log(self.y)
410
- return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
411
-
412
- def log1p(self) -> Self:
413
- """Compute the element-wise natural logarithm of (1 + signal data)."""
414
- result = np.log1p(self.y)
415
- return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
416
-
417
- def log10(self) -> Self:
418
- """Compute the element-wise base-10 logarithm of the signal data."""
419
- result = np.log10(self.y)
420
- return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
421
-
422
- def log2(self) -> Self:
423
- """Compute the element-wise base-2 logarithm of the signal data."""
424
- result = np.log2(self.y)
425
- return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
426
-
427
-
428
- #--------------------------
429
- # Aggregation signal ops
430
- #--------------------------
431
- def mean(self) -> float:
432
- """Compute the mean of the signal data."""
433
- return float(np.mean(self.y))
434
-
435
- def std(self) -> float:
436
- """Compute the standard deviation of the signal data."""
437
- return float(np.std(self.y))
438
-
439
- def min(self) -> float:
440
- """Compute the minimum value in the signal data."""
441
- return float(np.min(self.y))
442
-
443
- def max(self) -> float:
444
- """Compute the maximum value in the signal data."""
445
- return float(np.max(self.y))
446
-
447
- def sum(self) -> float:
448
- """Compute the sum of the signal data."""
449
- return float(np.sum(self.y))
450
-
451
- #-----------------------------------
452
- # Repr
453
- #-----------------------------------
454
-
455
- def __str__(self):
456
- cls = self.__class__.__name__
457
- data = self.y
458
-
459
- arr_str = np.array2string(
460
- data,
461
- separator=", ",
462
- threshold=50, # limit number of elements shown
463
- edgeitems=3, # show first/last 3 rows and columns
464
- max_line_width=120, # avoid wrapping
465
- formatter={'float_kind': lambda x: f"{x:.4g}"}
466
- )
467
-
468
- return f"Signal({arr_str}, shape={data.shape}, kind={cls})"
469
-
470
- def __repr__(self):
471
- cls = self.__class__.__name__
472
- data = self.y
473
-
474
- arr_str = np.array2string(
475
- data,
476
- separator=", ",
477
- threshold=50, # limit number of elements shown
478
- edgeitems=3, # show first/last 3 rows and columns
479
- max_line_width=120, # avoid wrapping
480
- formatter={'float_kind': lambda x: f"{x:.4g}"}
481
- )
482
-
483
- return f"Signal({arr_str}, shape={data.shape}, kind={cls})"
modusa/signals/base.py DELETED
@@ -1,34 +0,0 @@
1
- #!/usr/bin/env python3
2
-
3
- from modusa import excp
4
- from modusa.decorators import immutable_property, validate_args_type
5
- from modusa.signals.signal_ops import SignalOps
6
- from abc import ABC, abstractmethod
7
- from dataclasses import dataclass
8
- from typing import Self
9
- import numpy as np
10
- import matplotlib.pyplot as plt
11
-
12
- class ModusaSignal(ABC):
13
- """
14
- Base class for any signal in the modusa framework.
15
-
16
- Note
17
- ----
18
- - Intended to be subclassed.
19
- """
20
-
21
- #--------Meta Information----------
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"
27
- #----------------------------------
28
-
29
- @validate_args_type()
30
- def __init__(self):
31
- self._plugin_chain = []
32
-
33
-
34
-