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,465 +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 typing import Self, Any
8
- import numpy as np
9
- import matplotlib.pyplot as plt
10
-
11
- class Spectrogram(ModusaSignal):
12
- """
13
- A 2D time–frequency representation of a signal.
14
-
15
- Parameters
16
- ----------
17
- S : np.ndarray
18
- 2D matrix representing the spectrogram (shape: [n_freqs, n_frames]).
19
- f : np.ndarray
20
- Frequency axis corresponding to the rows of `S` (shape: [n_freqs]).
21
- t : np.ndarray
22
- Time axis corresponding to the columns of `S` (shape: [n_frames]).
23
- title : str, optional
24
- Optional title for the spectrogram (e.g., used in plotting).
25
- """
26
-
27
- #--------Meta Information----------
28
- _name = "Spectrogram"
29
- _description = ""
30
- _author_name = "Ankit Anand"
31
- _author_email = "ankit0.anand0@gmail.com"
32
- _created_at = "2025-07-07"
33
- #----------------------------------
34
-
35
- @validate_args_type()
36
- def __init__(self, S: np.ndarray, f: np.ndarray, t: np.ndarray, title: str | None = None):
37
- super().__init__() # Instantiating `ModusaSignal` class
38
-
39
- if S.ndim != 2:
40
- raise excp.InputValueError(f"`S` must have 2 dimension, got {S.ndim}.")
41
- if f.ndim != 1:
42
- raise excp.InputValueError(f"`f` must have 1 dimension, got {f.ndim}.")
43
- if t.ndim != 1:
44
- raise excp.InputValueError(f"`t` must have 1 dimension, got {t.ndim}.")
45
-
46
- if t.shape[0] != S.shape[1] or f.shape[0] != S.shape[0]:
47
- raise excp.InputValueError(f"`f` and `t` shape do not match with `S` {S.shape}, got {(f.shape[0], t.shape[0])}")
48
-
49
- if S.shape[1] == 0:
50
- raise excp.InputValueError("`S` must have at least one time frame")
51
-
52
- if t.shape[0] >= 2:
53
- dts = np.diff(t)
54
- if not np.allclose(dts, dts[0]):
55
- raise excp.InputValueError("`t` must be equally spaced")
56
-
57
- self._S = S
58
- self._f = f
59
- self._t = t
60
- self.title = title or self._name
61
-
62
- #----------------------
63
- # Properties
64
- #----------------------
65
- @immutable_property("Create a new object instead.")
66
- def S(self) -> np.ndarray:
67
- """Spectrogram matrix (freq × time)."""
68
- return self._S
69
-
70
- @immutable_property("Create a new object instead.")
71
- def f(self) -> np.ndarray:
72
- """Frequency axis."""
73
- return self._f
74
-
75
- @immutable_property("Create a new object instead.")
76
- def t(self) -> np.ndarray:
77
- """Time axis."""
78
- return self._t
79
-
80
- @immutable_property("Read only property.")
81
- def shape(self) -> np.ndarray:
82
- """Shape of the spectrogram (freqs, frames)."""
83
- return self.S.shape
84
-
85
- @immutable_property("Read only property.")
86
- def ndim(self) -> np.ndarray:
87
- """Number of dimensions (always 2)."""
88
- return self.S.ndim
89
-
90
- @immutable_property("Mutation not allowed.")
91
- def info(self) -> None:
92
- """Print key information about the spectrogram signal."""
93
- time_resolution = self.t[1] - self.t[0]
94
- n_freq_bins = self.S.shape[0]
95
-
96
- # Estimate NFFT size
97
- nfft = (n_freq_bins - 1) * 2
98
-
99
- print("-"*50)
100
- print(f"{'Title':<20}: {self.title}")
101
- print(f"{'Kind':<20}: {self._name}")
102
- print(f"{'Shape':<20}: {self.S.shape} (freq bins × time frames)")
103
- print(f"{'Time resolution':<20}: {time_resolution:.4f} sec ({time_resolution * 1000:.2f} ms)")
104
- print(f"{'Freq resolution':<20}: {(self.f[1] - self.f[0]):.2f} Hz")
105
- print("-"*50)
106
- #------------------------
107
-
108
-
109
- #------------------------
110
- # Useful tools
111
- #------------------------
112
-
113
- def __getitem__(self, key: tuple[int | slice, int | slice]) -> "Spectrogram | FrequencyDomainSignal | TimeDomainSignal":
114
- """
115
- Enable 2D indexing: signal[f_idx, t_idx]
116
-
117
- Returns:
118
- - Spectrogram when both f and t are slices
119
- - FrequencyDomainSignal when t is int (i.e., fixed time)
120
- - TimeDomainSignal when f is int (i.e., fixed frequency)
121
- """
122
- from modusa.signals.time_domain_signal import TimeDomainSignal
123
- from modusa.signals.frequency_domain_signal import FrequencyDomainSignal
124
-
125
- if isinstance(key, tuple) and len(key) == 2:
126
- f_key, t_key = key
127
-
128
- sliced_data = self.S[f_key, t_key]
129
- sliced_f = self.f[f_key]
130
- sliced_t = self.t[t_key]
131
-
132
- # Case 1: Scalar value → return plain numpy scalar
133
- if np.isscalar(sliced_data):
134
- return np.array(sliced_data)
135
-
136
- # Case 2: frequency slice at a single time (→ FrequencyDomainSignal)
137
- elif isinstance(t_key, int):
138
- if not isinstance(f_key, int): # already handled scalar case
139
- sliced_data = np.asarray(sliced_data).flatten()
140
- sliced_f = np.asarray(sliced_f)
141
- t0 = float(self.t[t_key])
142
- return FrequencyDomainSignal(
143
- spectrum=sliced_data,
144
- f=sliced_f,
145
- t0=t0,
146
- title=self.title + f" [t = {t0:.2f} sec]"
147
- )
148
-
149
- # Case 3: time slice at a single frequency (→ TimeDomainSignal)
150
- elif isinstance(f_key, int):
151
- sliced_data = np.asarray(sliced_data).flatten()
152
- sliced_t = np.asarray(sliced_t)
153
- sr = 1.0 / np.mean(np.diff(self.t)) # assume uniform time axis
154
- t0 = float(self.t[0])
155
- f_val = float(self.f[f_key])
156
- return TimeDomainSignal(
157
- y=sliced_data,
158
- sr=sr,
159
- t0=t0,
160
- title=self.title + f" [f = {f_val:.2f} Hz]"
161
- )
162
-
163
- # Case 4: 2D slice → Spectrogram
164
- else:
165
- return self.__class__(
166
- S=sliced_data,
167
- f=sliced_f,
168
- t=sliced_t,
169
- title=self.title
170
- )
171
-
172
- raise TypeError("Expected 2D indexing: signal[f_idx, t_idx]")
173
-
174
- def crop(
175
- self,
176
- f_min: float | None = None,
177
- f_max: float | None = None,
178
- t_min: float | None = None,
179
- t_max: float | None = None
180
- ) -> "Spectrogram":
181
- """
182
- Crop the spectrogram to a rectangular region in frequency-time space.
183
-
184
- .. code-block:: python
185
-
186
- cropped = spec.crop(f_min=100, f_max=1000, t_min=5.0, t_max=10.0)
187
-
188
- Parameters
189
- ----------
190
- f_min : float or None
191
- Inclusive lower frequency bound. If None, no lower bound.
192
- f_max : float or None
193
- Exclusive upper frequency bound. If None, no upper bound.
194
- t_min : float or None
195
- Inclusive lower time bound. If None, no lower bound.
196
- t_max : float or None
197
- Exclusive upper time bound. If None, no upper bound.
198
-
199
- Returns
200
- -------
201
- Spectrogram
202
- Cropped spectrogram.
203
- """
204
- S = self.S
205
- f = self.f
206
- t = self.t
207
-
208
- f_mask = (f >= f_min) if f_min is not None else np.ones_like(f, dtype=bool)
209
- f_mask &= (f < f_max) if f_max is not None else f_mask
210
-
211
- t_mask = (t >= t_min) if t_min is not None else np.ones_like(t, dtype=bool)
212
- t_mask &= (t < t_max) if t_max is not None else t_mask
213
-
214
- cropped_S = S[np.ix_(f_mask, t_mask)]
215
- cropped_f = f[f_mask]
216
- cropped_t = t[t_mask]
217
-
218
- return self.__class__(S=cropped_S, f=cropped_f, t=cropped_t, title=self.title)
219
-
220
-
221
- def plot(
222
- self,
223
- log_compression_factor: int | float | None = None,
224
- ax: plt.Axes | None = None,
225
- cmap: str = "gray_r",
226
- title: str | None = None,
227
- Mlabel: str | None = None,
228
- ylabel: str | None = "Frequency (hz)",
229
- xlabel: str | None = "Time (sec)",
230
- ylim: tuple[float, float] | None = None,
231
- xlim: tuple[float, float] | None = None,
232
- highlight: list[tuple[float, float, float, float]] | None = None,
233
- origin: str = "lower", # or "lower"
234
- show_colorbar: bool = True,
235
- cax: plt.Axes | None = None,
236
- show_grid: bool = True,
237
- tick_mode: str = "center", # "center" or "edge"
238
- n_ticks: tuple[int, int] | None = None,
239
- ) -> plt.Figure:
240
- """
241
- Plot the spectrogram using Matplotlib.
242
-
243
- .. code-block:: python
244
-
245
- fig = spec.plot(log_compression_factor=10, title="Log-scaled Spectrogram")
246
-
247
- Parameters
248
- ----------
249
- log_compression_factor : float or int, optional
250
- If specified, apply log-compression using log(1 + S * factor).
251
- ax : matplotlib.axes.Axes, optional
252
- Axes to draw on. If None, a new figure and axes are created.
253
- cmap : str, default "gray_r"
254
- Colormap used for the image.
255
- title : str, optional
256
- Title to use for the plot. Defaults to the signal's title.
257
- Mlabel : str, optional
258
- Label for the colorbar (e.g., "Magnitude", "dB").
259
- ylabel : str, optional
260
- Label for the y-axis. Default is "Frequency (hz)".
261
- xlabel : str, optional
262
- Label for the x-axis. Default is "Time (sec)".
263
- ylim : tuple of float, optional
264
- Limits for the y-axis (frequency).
265
- xlim : tuple of float, optional
266
- Limits for the x-axis (time).
267
- highlight : list of (x, y, w, h), optional
268
- Rectangular regions to highlight, specified in data coordinates.
269
- origin : {"lower", "upper"}, default "lower"
270
- Origin position for the image (for flipping vertical axis).
271
- show_colorbar : bool, default True
272
- Whether to display the colorbar.
273
- cax : matplotlib.axes.Axes, optional
274
- Axis to draw the colorbar on. If None, uses default placement.
275
- show_grid : bool, default True
276
- Whether to show the major gridlines.
277
- tick_mode : {"center", "edge"}, default "center"
278
- Whether to place ticks at bin centers or edges.
279
- n_ticks : tuple of int, optional
280
- Number of ticks (y_ticks, x_ticks) to display on each axis.
281
-
282
- Returns
283
- -------
284
- matplotlib.figure.Figure
285
- The figure object containing the plot.
286
- """
287
- from modusa.io import Plotter
288
-
289
- title = title or self.title
290
-
291
- fig = Plotter.plot_matrix(
292
- M=self.S,
293
- r=self.f,
294
- c=self.t,
295
- log_compression_factor=log_compression_factor,
296
- ax=ax,
297
- cmap=cmap,
298
- title=title,
299
- Mlabel=Mlabel,
300
- rlabel=ylabel,
301
- clabel=xlabel,
302
- rlim=ylim,
303
- clim=xlim,
304
- highlight=highlight,
305
- origin=origin,
306
- show_colorbar=show_colorbar,
307
- cax=cax,
308
- show_grid=show_grid,
309
- tick_mode=tick_mode,
310
- n_ticks=n_ticks
311
- )
312
-
313
- return fig
314
-
315
- #----------------------------
316
- # Math ops
317
- #----------------------------
318
-
319
- def __array__(self, dtype=None):
320
- return np.asarray(self._S, dtype=dtype)
321
-
322
- def __add__(self, other):
323
- other_data = other.S if isinstance(other, self.__class__) else other
324
- result = np.add(self.S, other_data)
325
- return self.__class__(S=result, f=self.f, t=self.t, title=self.title)
326
-
327
- def __radd__(self, other):
328
- result = np.add(other, self.S)
329
- return self.__class__(S=result, f=self.f, t=self.t, title=self.title)
330
-
331
- def __sub__(self, other):
332
- other_data = other.S if isinstance(other, self.__class__) else other
333
- result = np.subtract(self.S, other_data)
334
- return self.__class__(S=result, f=self.f, t=self.t, title=self.title)
335
-
336
- def __rsub__(self, other):
337
- result = np.subtract(other, self.S)
338
- return self.__class__(S=result, f=self.f, t=self.t, title=self.title)
339
-
340
- def __mul__(self, other):
341
- other_data = other.S if isinstance(other, self.__class__) else other
342
- result = np.multiply(self.S, other_data)
343
- return self.__class__(S=result, f=self.f, t=self.t, title=self.title)
344
-
345
- def __rmul__(self, other):
346
- result = np.multiply(other, self.S)
347
- return self.__class__(S=result, f=self.f, t=self.t, title=self.title)
348
-
349
- def __truediv__(self, other):
350
- other_data = other.S if isinstance(other, self.__class__) else other
351
- result = np.true_divide(self.S, other_data)
352
- return self.__class__(S=result, f=self.f, t=self.t, title=self.title)
353
-
354
- def __rtruediv__(self, other):
355
- result = np.true_divide(other, self.S)
356
- return self.__class__(S=result, f=self.f, t=self.t, title=self.title)
357
-
358
- def __floordiv__(self, other):
359
- other_data = other.S if isinstance(other, self.__class__) else other
360
- result = np.floor_divide(self.S, other_data)
361
- return self.__class__(S=result, f=self.f, t=self.t, title=self.title)
362
-
363
- def __rfloordiv__(self, other):
364
- result = np.floor_divide(other, self.S)
365
- return self.__class__(S=result, f=self.f, t=self.t, title=self.title)
366
-
367
- def __pow__(self, other):
368
- other_data = other.S if isinstance(other, self.__class__) else other
369
- result = np.power(self.S, other_data)
370
- return self.__class__(S=result, f=self.f, t=self.t, title=self.title)
371
-
372
- def __rpow__(self, other):
373
- result = np.power(other, self.S)
374
- return self.__class__(S=result, f=self.f, t=self.t, title=self.title)
375
-
376
- def __abs__(self):
377
- result = np.abs(self.S)
378
- return self.__class__(S=result, f=self.f, t=self.t, title=self.title)
379
-
380
- def sin(self):
381
- """Element-wise sine of the spectrogram."""
382
- return self.__class__(S=np.sin(self.S), f=self.f, t=self.t, title=self.title)
383
-
384
- def cos(self):
385
- """Element-wise cosine of the spectrogram."""
386
- return self.__class__(S=np.cos(self.S), f=self.f, t=self.t, title=self.title)
387
-
388
- def exp(self):
389
- """Element-wise exponential of the spectrogram."""
390
- return self.__class__(S=np.exp(self.S), f=self.f, t=self.t, title=self.title)
391
-
392
- def tanh(self):
393
- """Element-wise hyperbolic tangent of the spectrogram."""
394
- return self.__class__(S=np.tanh(self.S), f=self.f, t=self.t, title=self.title)
395
-
396
- def log(self):
397
- """Element-wise natural logarithm of the spectrogram."""
398
- return self.__class__(S=np.log(self.S), f=self.f, t=self.t, title=self.title)
399
-
400
- def log1p(self):
401
- """Element-wise log(1 + M) of the spectrogram."""
402
- return self.__class__(S=np.log1p(self.S), f=self.f, t=self.t, title=self.title)
403
-
404
- def log10(self):
405
- """Element-wise base-10 logarithm of the spectrogram."""
406
- return self.__class__(S=np.log10(self.S), f=self.f, t=self.t, title=self.title)
407
-
408
- def log2(self):
409
- """Element-wise base-2 logarithm of the spectrogram."""
410
- return self.__class__(S=np.log2(self.S), f=self.f, t=self.t, title=self.title)
411
-
412
-
413
- def mean(self) -> float:
414
- """Return the mean of the spectrogram values."""
415
- return float(np.mean(self.S))
416
-
417
- def std(self) -> float:
418
- """Return the standard deviation of the spectrogram values."""
419
- return float(np.std(self.S))
420
-
421
- def min(self) -> float:
422
- """Return the minimum value in the spectrogram."""
423
- return float(np.min(self.S))
424
-
425
- def max(self) -> float:
426
- """Return the maximum value in the spectrogram."""
427
- return float(np.max(self.S))
428
-
429
- def sum(self) -> float:
430
- """Return the sum of the spectrogram values."""
431
- return float(np.sum(self.S))
432
-
433
- #-----------------------------------
434
- # Repr
435
- #-----------------------------------
436
-
437
- def __str__(self):
438
- cls = self.__class__.__name__
439
- data = self.S
440
-
441
- arr_str = np.array2string(
442
- data,
443
- separator=", ",
444
- threshold=50, # limit number of elements shown
445
- edgeitems=3, # show first/last 3 rows and columns
446
- max_line_width=120, # avoid wrapping
447
- formatter={'float_kind': lambda x: f"{x:.4g}"}
448
- )
449
-
450
- return f"Signal({arr_str}, shape={data.shape}, kind={cls})"
451
-
452
- def __repr__(self):
453
- cls = self.__class__.__name__
454
- data = self.S
455
-
456
- arr_str = np.array2string(
457
- data,
458
- separator=", ",
459
- threshold=50, # limit number of elements shown
460
- edgeitems=3, # show first/last 3 rows and columns
461
- max_line_width=120, # avoid wrapping
462
- formatter={'float_kind': lambda x: f"{x:.4g}"}
463
- )
464
-
465
- return f"Signal({arr_str}, shape={data.shape}, kind={cls})"