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,329 +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 FrequencyDomainSignal(ModusaSignal):
12
- """
13
- Represents a 1D signal in the frequency domain.
14
-
15
- Note
16
- ----
17
- - The class is not intended to be instantiated directly
18
- This class stores the Complex spectrum of a signal
19
- along with its corresponding frequency axis. It optionally tracks the time
20
- origin (`t0`) of the spectral slice, which is useful when working with
21
- time-localized spectral data (e.g., from a spectrogram or short-time Fourier transform).
22
-
23
- Parameters
24
- ----------
25
- spectrum : np.ndarray
26
- The frequency-domain representation of the signal (real or complex-valued).
27
- f : np.ndarray
28
- The frequency axis corresponding to the spectrum values. Must match the shape of `spectrum`.
29
- t0 : float, optional
30
- The time (in seconds) corresponding to the origin of this spectral slice. Defaults to 0.0.
31
- title : str, optional
32
- An optional title for display or plotting purposes.
33
- """
34
- #--------Meta Information----------
35
- _name = ""
36
- _description = ""
37
- _author_name = "Ankit Anand"
38
- _author_email = "ankit0.anand0@gmail.com"
39
- _created_at = "2025-07-09"
40
- #----------------------------------
41
-
42
- @validate_args_type()
43
- def __init__(self, spectrum: np.ndarray, f: np.ndarray, t0: float | int = 0.0, title: str | None = None):
44
- super().__init__() # Instantiating `ModusaSignal` class
45
-
46
- if spectrum.shape != f.shape:
47
- raise excp.InputValueError(f"`spectrum` and `f` shape must match, got {spectrum.shape} and {f.shape}")
48
-
49
-
50
- self._spectrum = spectrum
51
- self._f = f
52
- self._t0 = float(t0)
53
-
54
- self.title = title or self._name # This title will be used as plot title by default
55
-
56
-
57
- #----------------------
58
- # Properties
59
- #----------------------
60
-
61
- @immutable_property("Create a new object instead.")
62
- def spectrum(self) -> np.ndarray:
63
- """Complex valued spectrum data."""
64
- return self._spectrum
65
-
66
- @immutable_property("Create a new object instead.")
67
- def f(self) -> np.ndarray:
68
- """frequency array of the spectrum."""
69
- return self._f
70
-
71
- @immutable_property("Create a new object instead.")
72
- def t0(self) -> np.ndarray:
73
- """Time origin (in seconds) of this spectral slice, e.g., from a spectrogram frame."""
74
- return self._t0
75
-
76
- def __len__(self):
77
- return len(self._y)
78
-
79
-
80
- #----------------------
81
- # Tools
82
- #----------------------
83
-
84
- def __getitem__(self, key: slice) -> Self:
85
- sliced_spectrum = self._spectrum[key]
86
- sliced_f = self._f[key]
87
- return self.__class__(spectrum=sliced_spectrum, f=sliced_f, t0=self.t0, title=self.title)
88
-
89
- @validate_args_type()
90
- def plot(
91
- self,
92
- scale_y: tuple[float, float] | None = None,
93
- ax: plt.Axes | None = None,
94
- color: str = "b",
95
- marker: str | None = None,
96
- linestyle: str | None = None,
97
- stem: bool | None = False,
98
- legend_loc: str | None = None,
99
- title: str | None = None,
100
- ylabel: str | None = "Amplitude",
101
- xlabel: str | None = "Freq (Hz)",
102
- ylim: tuple[float, float] | None = None,
103
- xlim: tuple[float, float] | None = None,
104
- highlight: list[tuple[float, float]] | None = None,
105
- ) -> plt.Figure:
106
- """
107
- Plot the frequency-domain signal as a line or stem plot.
108
-
109
- .. code-block:: python
110
-
111
- spectrum.plot(stem=True, color="r", title="FFT Frame", xlim=(0, 5000))
112
-
113
- Parameters
114
- ----------
115
- scale_y : tuple[float, float], optional
116
- Range to scale the spectrum values before plotting (min, max).
117
- ax : matplotlib.axes.Axes, optional
118
- Axis to plot on. If None, a new figure and axis are created.
119
- color : str, default="b"
120
- Color of the line or stem.
121
- marker : str, optional
122
- Marker style for points (ignored if stem=True).
123
- linestyle : str, optional
124
- Line style for the plot (ignored if stem=True).
125
- stem : bool, default=False
126
- Whether to use a stem plot instead of a line plot.
127
- legend_loc : str, optional
128
- Legend location (e.g., 'upper right'). If None, no legend is shown.
129
- title : str, optional
130
- Title of the plot. Defaults to signal title.
131
- ylabel : str, default="Amplitude"
132
- Label for the y-axis.
133
- xlabel : str, default="Freq (Hz)"
134
- Label for the x-axis.
135
- ylim : tuple[float, float], optional
136
- Limits for the y-axis.
137
- xlim : tuple[float, float], optional
138
- Limits for the x-axis.
139
- highlight : list[tuple[float, float]], optional
140
- Regions to highlight on the frequency axis as shaded spans.
141
-
142
- Returns
143
- -------
144
- matplotlib.figure.Figure
145
- The figure containing the plotted signal.
146
-
147
- Note
148
- ----
149
- - If `ax` is provided, the plot is drawn on it; otherwise, a new figure is created.
150
- - `highlight` can be used to emphasize frequency bands (e.g., formants, harmonics).
151
- - Use `scale_y` to clip or normalize extreme values before plotting.
152
- """
153
-
154
-
155
- from modusa.io import Plotter
156
-
157
- title = title or self.title
158
-
159
- fig: plt.Figure | None = Plotter.plot_signal(y=self.spectrum, x=self.f, 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)
160
-
161
- return fig
162
-
163
- #----------------------------
164
- # Math ops
165
- #----------------------------
166
-
167
- def __array__(self, dtype=None):
168
- return np.asarray(self.spectrum, dtype=dtype)
169
-
170
- def __add__(self, other):
171
- other_data = other.spectrum if isinstance(other, self.__class__) else other
172
- result = np.add(self.spectrum, other_data)
173
- return self.__class__(spectrum=result, f=self.f, t0=self.t0, title=self.title)
174
-
175
- def __radd__(self, other):
176
- result = np.add(other, self.spectrum)
177
- return self.__class__(spectrum=result, f=self.f, t0=self.t0, title=self.title)
178
-
179
- def __sub__(self, other):
180
- other_data = other.spectrum if isinstance(other, self.__class__) else other
181
- result = np.subtract(self.spectrum, other_data)
182
- return self.__class__(spectrum=result, f=self.f, t0=self.t0, title=self.title)
183
-
184
- def __rsub__(self, other):
185
- result = np.subtract(other, self.spectrum)
186
- return self.__class__(spectrum=result, f=self.f, t0=self.t0, title=self.title)
187
-
188
- def __mul__(self, other):
189
- other_data = other.spectrum if isinstance(other, self.__class__) else other
190
- result = np.multiply(self.spectrum, other_data)
191
- return self.__class__(spectrum=result, f=self.f, t0=self.t0, title=self.title)
192
-
193
- def __rmul__(self, other):
194
- result = np.multiply(other, self.spectrum)
195
- return self.__class__(spectrum=result, f=self.f, t0=self.t0, title=self.title)
196
-
197
- def __truediv__(self, other):
198
- other_data = other.spectrum if isinstance(other, self.__class__) else other
199
- result = np.true_divide(self.spectrum, other_data)
200
- return self.__class__(spectrum=result, f=self.f, t0=self.t0, title=self.title)
201
-
202
- def __rtruediv__(self, other):
203
- result = np.true_divide(other, self.spectrum)
204
- return self.__class__(spectrum=result, f=self.f, t0=self.t0, title=self.title)
205
-
206
- def __floordiv__(self, other):
207
- other_data = other.spectrum if isinstance(other, self.__class__) else other
208
- result = np.floor_divide(self.spectrum, other_data)
209
- return self.__class__(spectrum=result, f=self.f, t0=self.t0, title=self.title)
210
-
211
- def __rfloordiv__(self, other):
212
- result = np.floor_divide(other, self.spectrum)
213
- return self.__class__(spectrum=result, f=self.f, t0=self.t0, title=self.title)
214
-
215
- def __pow__(self, other):
216
- other_data = other.spectrum if isinstance(other, self.__class__) else other
217
- result = np.power(self.spectrum, other_data)
218
- return self.__class__(spectrum=result, f=self.f, t0=self.t0, title=self.title)
219
-
220
- def __rpow__(self, other):
221
- result = np.power(other, self.spectrum)
222
- return self.__class__(spectrum=result, f=self.f, t0=self.t0, title=self.title)
223
-
224
- def __abs__(self):
225
- result = np.abs(self.spectrum)
226
- return self.__class__(spectrum=result, f=self.f, t0=self.t0, title=self.title)
227
-
228
-
229
- #--------------------------
230
- # Other signal ops
231
- #--------------------------
232
- def sin(self) -> Self:
233
- """Compute the element-wise sine of the signal data."""
234
- result = np.sin(self.spectrum)
235
- return self.__class__(spectrum=result, f=self.f, t0=self.t0, title=self.title)
236
-
237
- def cos(self) -> Self:
238
- """Compute the element-wise cosine of the signal data."""
239
- result = np.cos(self.spectrum)
240
- return self.__class__(spectrum=result, f=self.f, t0=self.t0, title=self.title)
241
-
242
- def exp(self) -> Self:
243
- """Compute the element-wise exponential of the signal data."""
244
- result = np.exp(self.spectrum)
245
- return self.__class__(spectrum=result, f=self.f, t0=self.t0, title=self.title)
246
-
247
- def tanh(self) -> Self:
248
- """Compute the element-wise hyperbolic tangent of the signal data."""
249
- result = np.tanh(self.spectrum)
250
- return self.__class__(spectrum=result, f=self.f, t0=self.t0, title=self.title)
251
-
252
- def log(self) -> Self:
253
- """Compute the element-wise natural logarithm of the signal data."""
254
- result = np.log(self.spectrum)
255
- return self.__class__(spectrum=result, f=self.f, t0=self.t0, title=self.title)
256
-
257
- def log1p(self) -> Self:
258
- """Compute the element-wise natural logarithm of (1 + signal data)."""
259
- result = np.log1p(self.spectrum)
260
- return self.__class__(spectrum=result, f=self.f, t0=self.t0, title=self.title)
261
-
262
- def log10(self) -> Self:
263
- """Compute the element-wise base-10 logarithm of the signal data."""
264
- result = np.log10(self.spectrum)
265
- return self.__class__(spectrum=result, f=self.f, t0=self.t0, title=self.title)
266
-
267
- def log2(self) -> Self:
268
- """Compute the element-wise base-2 logarithm of the signal data."""
269
- result = np.log2(self.spectrum)
270
- return self.__class__(spectrum=result, f=self.f, t0=self.t0, title=self.title)
271
-
272
-
273
- #--------------------------
274
- # Aggregation signal ops
275
- #--------------------------
276
- def mean(self) -> float:
277
- """Compute the mean of the signal data."""
278
- return float(np.mean(self.spectrum))
279
-
280
- def std(self) -> float:
281
- """Compute the standard deviation of the signal data."""
282
- return float(np.std(self.spectrum))
283
-
284
- def min(self) -> float:
285
- """Compute the minimum value in the signal data."""
286
- return float(np.min(self.spectrum))
287
-
288
- def max(self) -> float:
289
- """Compute the maximum value in the signal data."""
290
- return float(np.max(self.spectrum))
291
-
292
- def sum(self) -> float:
293
- """Compute the sum of the signal data."""
294
- return float(np.sum(self.spectrum))
295
-
296
- #-----------------------------------
297
- # Repr
298
- #-----------------------------------
299
-
300
- def __str__(self):
301
- cls = self.__class__.__name__
302
- data = self.spectrum
303
-
304
- arr_str = np.array2string(
305
- data,
306
- separator=", ",
307
- threshold=50, # limit number of elements shown
308
- edgeitems=3, # show first/last 3 rows and columns
309
- max_line_width=120, # avoid wrapping
310
- formatter={'float_kind': lambda x: f"{x:.4g}"}
311
- )
312
-
313
- return f"Signal({arr_str}, shape={data.shape}, kind={cls})"
314
-
315
- def __repr__(self):
316
- cls = self.__class__.__name__
317
- data = self.spectrum
318
-
319
- arr_str = np.array2string(
320
- data,
321
- separator=", ",
322
- threshold=50, # limit number of elements shown
323
- edgeitems=3, # show first/last 3 rows and columns
324
- max_line_width=120, # avoid wrapping
325
- formatter={'float_kind': lambda x: f"{x:.4g}"}
326
- )
327
-
328
- return f"Signal({arr_str}, shape={data.shape}, kind={cls})"
329
-
@@ -1,158 +0,0 @@
1
- #!/usr/bin/env python3
2
-
3
- from modusa import excp
4
- from typing import Any
5
- import numpy as np
6
-
7
- class SignalOps:
8
- """
9
- Performs arithmetic and NumPy-style ops on ModusaSignal instances.
10
-
11
- Note
12
- ----
13
- - Shape-changing operations like reshape, transpose, etc. are not yet supported. Use only element-wise or aggregation ops for now.
14
- - Index alignment must be handled carefully in future extensions.
15
- """
16
-
17
- def _axes_match(a1: tuple[np.ndarray, ...], a2: tuple[np.ndarray, ...]) -> bool:
18
- """
19
- To check if two axes are same.
20
- """
21
- if len(a1) != len(a2):
22
- return False
23
- return all(np.allclose(x, y, atol=1e-8) for x, y in zip(a1, a2))
24
-
25
-
26
- #----------------------------------
27
- # To handle basic math operations like
28
- # +, -, *, **, / ...
29
- #----------------------------------
30
-
31
- @staticmethod
32
- def add(data: np.ndarray, other: np.ndarray) -> np.ndarray:
33
- return np.add(data, other)
34
-
35
- @staticmethod
36
- def subtract(signal: "ModusaSignal", other: Any) -> "ModusaSignal":
37
- return SignalOps._apply_binary_op(signal, other, np.subtract, "subtract")
38
-
39
- @staticmethod
40
- def multiply(signal: "ModusaSignal", other: Any):
41
- return SignalOps._apply_binary_op(signal, other, np.multiply, "multiply")
42
-
43
- @staticmethod
44
- def divide(signal: "ModusaSignal", other: Any):
45
- return SignalOps._apply_binary_op(signal, other, np.divide, "divide")
46
-
47
- @staticmethod
48
- def power(signal: "ModusaSignal", other: Any):
49
- return SignalOps._apply_binary_op(signal, other, np.power, "power")
50
-
51
- @staticmethod
52
- def floor_divide(signal: "ModusaSignal", other: Any):
53
- return SignalOps._apply_binary_op(signal, other, np.floor_divide, "floor_divide")
54
-
55
-
56
-
57
- #----------------------------------
58
- # To handle numpy aggregator ops
59
- # mean, sum, ...
60
- # TODO: Add dimension select option
61
- #----------------------------------
62
- @staticmethod
63
- def mean(signal: "ModusaSignal") -> float:
64
- """Return the mean of the signal data."""
65
- return float(np.mean(signal._data))
66
-
67
- @staticmethod
68
- def std(signal: "ModusaSignal") -> float:
69
- """Return the standard deviation of the signal data."""
70
- return float(np.std(signal._data))
71
-
72
- @staticmethod
73
- def min(signal: "ModusaSignal") -> float:
74
- """Return the minimum value in the signal data."""
75
- return float(np.min(signal._data))
76
-
77
- @staticmethod
78
- def max(signal: "ModusaSignal") -> float:
79
- """Return the maximum value in the signal data."""
80
- return float(np.max(signal._data))
81
-
82
- @staticmethod
83
- def sum(signal: "ModusaSignal") -> float:
84
- """Return the sum of the signal data."""
85
- return float(np.sum(signal._data))
86
-
87
- #----------------------------------
88
- # To handle numpy ops where the
89
- # shapes are unaltered
90
- # sin, cos, exp, log, ...
91
- #----------------------------------
92
-
93
- @staticmethod
94
- def _apply_unary_op(signal: "ModusaSignal", op_func, op_name: str):
95
- from modusa.signals.base import ModusaSignal # avoid circular import
96
-
97
- if not isinstance(signal, ModusaSignal):
98
- raise excp.InputTypeError(f"`signal` must be a ModusaSignal, got {type(signal)}")
99
-
100
- try:
101
- result = op_func(signal._data)
102
- except Exception as e:
103
- raise excp.SignalOpError(f"{op_name} failed: {e}")
104
-
105
- if not isinstance(result, np.ndarray):
106
- raise excp.SignalOpError(f"{op_name} did not return a NumPy array, got {type(result)}")
107
-
108
- if result.shape != signal._data.shape:
109
- raise excp.SignalOpError(f"{op_name} changed shape: {signal._data.shape} → {result.shape}")
110
-
111
- return signal.replace(data=result)
112
-
113
-
114
- @staticmethod
115
- def sin(signal: "ModusaSignal"):
116
- return SignalOps._apply_unary_op(signal, np.sin, "sin")
117
-
118
- @staticmethod
119
- def cos(signal: "ModusaSignal"):
120
- return SignalOps._apply_unary_op(signal, np.cos, "cos")
121
-
122
- @staticmethod
123
- def exp(signal: "ModusaSignal"):
124
- return SignalOps._apply_unary_op(signal, np.exp, "exp")
125
-
126
- @staticmethod
127
- def tanh(signal: "ModusaSignal"):
128
- return SignalOps._apply_unary_op(signal, np.tanh, "tanh")
129
-
130
- @staticmethod
131
- def log(signal: "ModusaSignal"):
132
- return SignalOps._apply_unary_op(signal, np.log, "log")
133
-
134
- @staticmethod
135
- def log10(signal: "ModusaSignal"):
136
- return SignalOps._apply_unary_op(signal, np.log10, "log10")
137
-
138
- @staticmethod
139
- def log2(signal: "ModusaSignal"):
140
- return SignalOps._apply_unary_op(signal, np.log2, "log2")
141
-
142
- @staticmethod
143
- def log1p(signal: "ModusaSignal"):
144
- return SignalOps._apply_unary_op(signal, np.log1p, "log1p")
145
-
146
-
147
- @staticmethod
148
- def sqrt(signal: "ModusaSignal"):
149
- return SignalOps._apply_unary_op(signal, np.sqrt, "sqrt")
150
-
151
- @staticmethod
152
- def abs(signal: "ModusaSignal"):
153
- return SignalOps._apply_unary_op(signal, np.abs, "abs")
154
-
155
- #------------------------------------
156
- # TODO: Add shape-changing ops like
157
- # reshape, transpose, squeeze later
158
- #------------------------------------