modusa 0.1.0__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 (41) hide show
  1. modusa/.DS_Store +0 -0
  2. modusa/__init__.py +1 -0
  3. modusa/config.py +18 -0
  4. modusa/decorators.py +176 -0
  5. modusa/devtools/generate_template.py +79 -0
  6. modusa/devtools/list_authors.py +2 -0
  7. modusa/devtools/list_plugins.py +60 -0
  8. modusa/devtools/main.py +42 -0
  9. modusa/devtools/templates/engines.py +28 -0
  10. modusa/devtools/templates/generators.py +26 -0
  11. modusa/devtools/templates/plugins.py +40 -0
  12. modusa/devtools/templates/signals.py +63 -0
  13. modusa/engines/__init__.py +4 -0
  14. modusa/engines/base.py +14 -0
  15. modusa/engines/plot_1dsignal.py +130 -0
  16. modusa/engines/plot_2dmatrix.py +159 -0
  17. modusa/generators/__init__.py +3 -0
  18. modusa/generators/base.py +40 -0
  19. modusa/generators/basic_waveform.py +185 -0
  20. modusa/main.py +35 -0
  21. modusa/plugins/__init__.py +7 -0
  22. modusa/plugins/base.py +100 -0
  23. modusa/plugins/plot_1dsignal.py +59 -0
  24. modusa/plugins/plot_2dmatrix.py +76 -0
  25. modusa/plugins/plot_time_domain_signal.py +59 -0
  26. modusa/signals/__init__.py +9 -0
  27. modusa/signals/audio_signal.py +230 -0
  28. modusa/signals/base.py +294 -0
  29. modusa/signals/signal1d.py +311 -0
  30. modusa/signals/signal2d.py +226 -0
  31. modusa/signals/uniform_time_domain_signal.py +212 -0
  32. modusa/utils/.DS_Store +0 -0
  33. modusa/utils/__init__.py +1 -0
  34. modusa/utils/config.py +25 -0
  35. modusa/utils/excp.py +71 -0
  36. modusa/utils/logger.py +18 -0
  37. modusa-0.1.0.dist-info/METADATA +86 -0
  38. modusa-0.1.0.dist-info/RECORD +41 -0
  39. modusa-0.1.0.dist-info/WHEEL +4 -0
  40. modusa-0.1.0.dist-info/entry_points.txt +5 -0
  41. modusa-0.1.0.dist-info/licenses/LICENSE.md +9 -0
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env python3
2
+
3
+
4
+ from modusa.plugins.base import ModusaPlugin
5
+ from modusa.decorators import immutable_property, validate_args_type, plugin_safety_check
6
+ import matplotlib.pyplot as plt
7
+
8
+ class PlotTimeDomainSignalPlugin(ModusaPlugin):
9
+ """
10
+
11
+ """
12
+
13
+ #--------Meta Information----------
14
+ name = ""
15
+ description = ""
16
+ author_name = "Ankit Anand"
17
+ author_email = "ankit0.anand0@gmail.com"
18
+ created_at = "2025-07-03"
19
+ #----------------------------------
20
+
21
+ def __init__(self):
22
+ super().__init__()
23
+
24
+ @immutable_property(error_msg="Mutation not allowed.")
25
+ def allowed_input_signal_types(self) -> tuple[type, ...]:
26
+ from modusa.signals import UniformTimeDomainSignal, AudioSignal
27
+ return (UniformTimeDomainSignal, AudioSignal, )
28
+
29
+
30
+ @immutable_property(error_msg="Mutation not allowed.")
31
+ def allowed_output_signal_types(self) -> tuple[type, ...]:
32
+ return (plt.Figure, type(None))
33
+
34
+
35
+ @plugin_safety_check()
36
+ @validate_args_type()
37
+ def apply(
38
+ self,
39
+ signal: "UniformTimeDomainSignal",
40
+ scale_y: tuple[float, float] | None = None,
41
+ scale_t: tuple[float, float] | None = None,
42
+ ax: plt.Axes | None = None,
43
+ color: str | None = "b",
44
+ marker: str | None = None,
45
+ linestyle: str | None = None,
46
+ stem: bool = False,
47
+ labels: tuple[str, str, str] | None = None,
48
+ legend_loc: str | None = None,
49
+ zoom: tuple[float, float] | None = None,
50
+ highlight: list[tuple[float, float], ...] | None = None,
51
+ show_grid: bool | None = False,
52
+ ) -> plt.Figure:
53
+
54
+ # Run the engine here
55
+ from modusa.engines import Plot1DSignalEngine
56
+
57
+ fig: plt.Figure | None = Plot1DSignalEngine().run(y=signal.data, x=signal.t, scale_y=scale_y, scale_x=scale_t, ax=ax, color=color, marker=marker, linestyle=linestyle, stem=stem, labels=labels, legend_loc=legend_loc, zoom=zoom, highlight=highlight)
58
+
59
+ return fig
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env python3
2
+
3
+ from .base import ModusaSignal
4
+
5
+ from .signal1d import Signal1D
6
+ from .signal2d import Signal2D
7
+ from .uniform_time_domain_signal import UniformTimeDomainSignal
8
+
9
+ from .audio_signal import AudioSignal
@@ -0,0 +1,230 @@
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
+ from pathlib import Path
11
+
12
+ class AudioSignal(ModusaSignal):
13
+ """
14
+
15
+ """
16
+
17
+ #--------Meta Information----------
18
+ name = "Audio Signal"
19
+ description = ""
20
+ author_name = "Ankit Anand"
21
+ author_email = "ankit0.anand0@gmail.com"
22
+ created_at = "2025-07-04"
23
+ #----------------------------------
24
+
25
+ @validate_args_type()
26
+ def __init__(self, y: np.ndarray, t: np.ndarray | None = None):
27
+
28
+ if y.ndim != 1: # Mono
29
+ raise excp.InputValueError(f"`y` must have 1 dimension not {y.ndim}.")
30
+ if t.ndim != 1:
31
+ raise excp.InputValueError(f"`t` must have 1 dimension not {t.ndim}.")
32
+
33
+ if t is None:
34
+ t = np.arange(y.shape[0])
35
+ else:
36
+ if t.shape[0] != y.shape[0]:
37
+ raise excp.InputValueError(f"`y` and `t` must have same shape.")
38
+ dts = np.diff(t)
39
+ if not np.allclose(dts, dts[0]):
40
+ raise excp.InputValueError("`t` must be equally spaced")
41
+
42
+ super().__init__(data=y, data_idx=t) # Instantiating `ModusaSignal` class
43
+
44
+ self._y_unit = ""
45
+ self._t_unit = "sec"
46
+
47
+ self._title = "Audio Signal"
48
+ self._y_label = "Amplitude"
49
+ self._t_label = "Time"
50
+
51
+ def _with_data(self, new_data: np.ndarray, new_data_idx: np.ndarray) -> Self:
52
+ """Subclasses must override this to return a copy with new data."""
53
+ new_signal = self.__class__(y=new_data, t=new_data_idx)
54
+ new_signal.set_units(y_unit=self.y_unit, t_unit=self.t_unit)
55
+ new_signal.set_plot_labels(title=self.title, y_label=self.y_label, t_label=self.t_label)
56
+
57
+ return new_signal
58
+
59
+ #----------------------
60
+ # From methods
61
+ #----------------------
62
+ @classmethod
63
+ def from_array(cls, y: np.ndarray, t: np.ndarray | None = None) -> Self:
64
+
65
+ signal = cls(y=y, t=t)
66
+
67
+ return signal
68
+
69
+ @classmethod
70
+ def from_array_with_sr(cls, y: np.ndarray, sr: int) -> Self:
71
+ t = np.arange(y.shape[0]) * (1.0 / sr)
72
+
73
+ signal = cls(y=y, t=t)
74
+
75
+ return signal
76
+
77
+ @classmethod
78
+ def from_list(cls, y: list, t: list) -> Self:
79
+
80
+ y = np.array(y)
81
+ t = np.array(t)
82
+ signal = cls(y=y, t=t)
83
+
84
+ return signal
85
+
86
+ @classmethod
87
+ def from_file(cls, fp: str | Path, sr: int | None = None) -> Self:
88
+
89
+ import librosa
90
+
91
+ fp = Path(fp)
92
+ y, sr = librosa.load(fp, sr=sr)
93
+ t = np.arange(y.shape[0]) * (1.0 / sr)
94
+
95
+ signal = cls(y=y, t=t)
96
+ signal.set_plot_labels(title=fp.stem)
97
+
98
+ return signal
99
+
100
+ #----------------------
101
+ # Setters
102
+ #----------------------
103
+
104
+ @validate_args_type()
105
+ def set_units(self, y_unit: str | None = None, t_unit: str | None = None) -> Self:
106
+
107
+ if y_unit is not None:
108
+ self._y_unit = y_unit
109
+ if t_unit is not None:
110
+ self._t_unit = t_unit
111
+
112
+ return self
113
+
114
+ @validate_args_type()
115
+ def set_plot_labels(self, title: str | None = None, y_label: str | None = None, t_label: str | None = None) -> Self:
116
+
117
+ if title is not None:
118
+ self._title = title
119
+ if y_label is not None:
120
+ self._y_label = y_label
121
+ if t_label is not None:
122
+ self._t_label = t_label
123
+
124
+ return self
125
+
126
+
127
+ #----------------------
128
+ # Properties
129
+ #----------------------
130
+
131
+ @immutable_property("Create a new object instead.")
132
+ def y(self) -> np.ndarray:
133
+ """"""
134
+ return self.data
135
+
136
+ @immutable_property("Create a new object instead.")
137
+ def t(self) -> np.ndarray:
138
+ """"""
139
+ return self.data_idx
140
+
141
+ @immutable_property("Create a new object instead.")
142
+ def sr(self) -> np.ndarray:
143
+ """"""
144
+ return 1.0 / self.t[1] - self.t[0]
145
+
146
+ @immutable_property("Use `.set_units` instead.")
147
+ def y_unit(self) -> str:
148
+ """"""
149
+ return self._y_unit
150
+
151
+ @immutable_property("Use `set_units` instead.")
152
+ def t_unit(self) -> str:
153
+ """"""
154
+ return self._t_unit
155
+
156
+ @immutable_property("Use `.set_plot_labels` instead.")
157
+ def title(self) -> str:
158
+ """"""
159
+ return self._title
160
+
161
+ @immutable_property("Use `.set_plot_labels` instead.")
162
+ def y_label(self) -> str:
163
+ """"""
164
+ return self._y_label
165
+
166
+ @immutable_property("Use `.set_plot_labels` instead.")
167
+ def t_label(self) -> str:
168
+ """"""
169
+ return self._t_label
170
+
171
+ @immutable_property("Mutation not allowed.")
172
+ def Ts(self) -> int:
173
+ """"""
174
+ return self.t[1] - self.t[0]
175
+
176
+ @immutable_property("Mutation not allowed.")
177
+ def duration(self) -> int:
178
+ """"""
179
+ return self.t[-1]
180
+
181
+ @immutable_property("Use `.set_labels` instead.")
182
+ def labels(self) -> tuple[str, str, str]:
183
+ """Labels in a tuple format appropriate for the plots."""
184
+ return (self.title, f"{self.y_label} ({self.y_unit})", f"{self.t_label} ({self.t_unit})")
185
+
186
+
187
+ #----------------------
188
+ # Plugins Access
189
+ #----------------------
190
+ @validate_args_type()
191
+ def plot(
192
+ self,
193
+ scale_y: tuple[float, float] | None = None,
194
+ scale_t: tuple[float, float] | None = None,
195
+ ax: plt.Axes | None = None,
196
+ color: str = "b",
197
+ marker: str | None = None,
198
+ linestyle: str | None = None,
199
+ stem: bool | None = None,
200
+ labels: tuple[str, str, str] | None = None,
201
+ legend_loc: str | None = None,
202
+ zoom: tuple[float, float] | None = None,
203
+ highlight: list[tuple[float, float]] | None = None,
204
+ ) -> plt.Figure:
205
+ """
206
+ Applies `modusa.plugins.PlotTimeDomainSignal` Plugin.
207
+ """
208
+
209
+ from modusa.plugins import PlotTimeDomainSignalPlugin
210
+
211
+ labels = labels or self.labels
212
+ stem = stem or False
213
+
214
+ fig: plt.Figure | None = PlotTimeDomainSignalPlugin().apply(
215
+ signal=self,
216
+ scale_y=scale_y,
217
+ scale_t=scale_t,
218
+ ax=ax,
219
+ color=color,
220
+ marker=marker,
221
+ linestyle=linestyle,
222
+ stem=stem,
223
+ labels=labels,
224
+ legend_loc=legend_loc,
225
+ zoom=zoom,
226
+ highlight=highlight
227
+ )
228
+
229
+ return fig
230
+
modusa/signals/base.py ADDED
@@ -0,0 +1,294 @@
1
+ #!/usr/bin/env python3
2
+
3
+ from modusa import excp
4
+ from modusa.decorators import immutable_property, validate_args_type
5
+ from abc import ABC, abstractmethod
6
+ from typing import Self
7
+ import numpy as np
8
+ import matplotlib.pyplot as plt
9
+
10
+ class ModusaSignal(ABC):
11
+ """
12
+ Base class prototype for any signal.
13
+
14
+ Note
15
+ ----
16
+ - Serves as the foundation for all signal types in the Modusa framework
17
+ - Intended to be subclassed
18
+ - Subclass shoud implement a **read-only** `data` property (e.g., amplitude or spectral data) and a `plot()` method to visualize the signal
19
+
20
+ Warning
21
+ -------
22
+ - You cannot create a subclass without `data` and `plot()` implemented. It will throw an error on instantiating the subclass.
23
+
24
+ Example
25
+ -------
26
+ .. code-block:: python
27
+
28
+ from modusa.signals import ModusaSignal
29
+ from modusa.decorators import validate_args_type
30
+
31
+ class MySignal(ModusaSignal):
32
+
33
+ @validate_args_type()
34
+ def __init__(self, y: nd.ndarray):
35
+ super().__init__() # Very important for proper initialisation
36
+ self._y = y
37
+
38
+ @property
39
+ def data(self):
40
+ return self._y
41
+
42
+ @validate_args_type()
43
+ def plot(self):
44
+ # Your plotting logic here
45
+ pass
46
+ """
47
+
48
+ #--------Meta Information----------
49
+ name = "Modusa Signal"
50
+ description = "Base class for any signal types in the Modusa framework."
51
+ author_name = "Ankit Anand"
52
+ author_email = "ankit0.anand0@gmail.com"
53
+ created_at = "2025-06-23"
54
+ #----------------------------------
55
+
56
+ @validate_args_type()
57
+ def __init__(self, data: np.ndarray, data_idx: np.ndarray):
58
+ self._data = data
59
+ self._data_idx = data_idx
60
+ self._plugin_chain = []
61
+
62
+ #----------------------------
63
+ # Setters
64
+ #----------------------------
65
+ @validate_args_type()
66
+ def set_name(self, name: str) -> Self:
67
+ self.name = name
68
+
69
+ return name
70
+
71
+
72
+ #----------------------------
73
+ # Properties
74
+ #----------------------------
75
+
76
+ @property
77
+ def data(self) -> np.ndarray:
78
+ """
79
+ The core signal data as a NumPy array.
80
+
81
+ Note
82
+ ----
83
+ - Different signals might need to have different variable names to store data, e.g. y(t) -> y, M(x, y) -> M, x(t) -> x
84
+ - This `data` property must return the correct data for any given subclass (y for y(t), M for M(x, y)), see the example.
85
+ - Must return `np.ndarray`
86
+ """
87
+ return self._data
88
+
89
+ @immutable_property("Create a new object instead.")
90
+ def data_idx(self) -> np.ndarray:
91
+ """
92
+ The coordinate values associated with each element of the signal `data`.
93
+
94
+ Note
95
+ ----
96
+ - This is often a 1D array of the same length as the first axis of `data`.
97
+ - For time-domain signals, this typically represents timestamps.
98
+ - For spectrograms or other 2D signals, you may use a dict mapping axes to coordinate arrays.
99
+ - This property is read-only; to modify it, create a new signal object.
100
+
101
+ Returns
102
+ -------
103
+ np.ndarray
104
+ An array of coordinates or indices corresponding to the signal data.
105
+ """
106
+ return self._data_idx
107
+
108
+ @immutable_property("Read-only property.")
109
+ def shape(self) -> tuple[int]:
110
+ """Shape of the signal."""
111
+ return self.data.shape
112
+
113
+
114
+ @immutable_property("plugin_chain is read-only. It is automatically updated.")
115
+ def plugin_chain(self) -> list[str]:
116
+ """
117
+ List of plugin names applied to the signal.
118
+
119
+ Note
120
+ ----
121
+ - Reflects the signal’s processing history
122
+ - Ordered by application sequence
123
+ - Managed automatically (read-only)
124
+ - Use `info` for a formatted summary
125
+ """
126
+ return self._plugin_chain.copy()
127
+
128
+ @immutable_property("Mutation not allowed, generated automatically.")
129
+ def info(self) -> None:
130
+ """
131
+ Prints a quick summary of the signal.
132
+
133
+ Note
134
+ ----
135
+ - Output is printed to the console.
136
+ - Returns nothing.
137
+
138
+ """
139
+
140
+ print("\n".join([
141
+ f"{self.__class__.__name__}(",
142
+ f" Signal Name: '{self.name}',",
143
+ f" Inheritance: {' → '.join(cls.__name__ for cls in self.__class__.mro()[:-1])}",
144
+ f" Plugin Chain: {' → '.join(self._plugin_chain) or '(none)'}",
145
+ f")"
146
+ ]))
147
+
148
+ #-------------------------------
149
+ # Basic functionalities
150
+ #-------------------------------
151
+
152
+ @abstractmethod
153
+ def plot(self) -> plt.Figure:
154
+ """
155
+ Plot a visual representation of the signal.
156
+
157
+ Note
158
+ ----
159
+ - For any signal, one must implement `plot` method with useful features to make users happy
160
+ - Try to return `matplotlib.figure.Figure` for customization/saving but other plotting libraries are also welcome
161
+ """
162
+ pass
163
+
164
+ @abstractmethod
165
+ def _with_data(self, new_data: np.ndarray, new_data_idx: np.ndarray) -> Self:
166
+ """Subclasses must override this to return a copy with new data."""
167
+ pass
168
+
169
+
170
+ def trim(self, start: float | None = None, stop: float | None = None):
171
+ """
172
+ Returns a new signal trimmed between two data_idx values.
173
+
174
+ Parameters
175
+ ----------
176
+ start : float or None
177
+ The starting data_idx value (inclusive). If None, starts from the beginning.
178
+ stop : float or None
179
+ The stopping data_idx value (exclusive). If None, goes till the end.
180
+
181
+ Returns
182
+ -------
183
+ ModusaSignal
184
+ A new signal with trimmed data and data_idx.
185
+ """
186
+ # Define bounds
187
+ start_v = -np.inf if start is None else start
188
+ stop_v = np.inf if stop is None else stop
189
+
190
+ # Build a mask over data_idx values
191
+ mask = (self.data_idx >= start_v) & (self.data_idx < stop_v)
192
+ idx = np.where(mask)[0]
193
+
194
+ return self._with_data(new_data=self.data[idx], new_data_idx=self.data_idx[idx])
195
+
196
+ #----------------------------
197
+ # Dunder method
198
+ #----------------------------
199
+
200
+
201
+ #----------------------------
202
+ # Slicing
203
+ #----------------------------
204
+
205
+ def __getitem__(self, key):
206
+ if isinstance(key, (int, slice)):
207
+ # Normal Python-style slicing by index
208
+ sliced_data = self.data[key]
209
+ sliced_idx = self.data_idx[key]
210
+ return self._with_data(new_data=sliced_data, new_data_idx=sliced_idx)
211
+
212
+ else:
213
+ raise TypeError(f"Indexing with type {type(key)} is not supported. Use int or slice.")
214
+
215
+
216
+
217
+ def __str__(self):
218
+ cls = self.__class__.__name__
219
+ data = self.data
220
+
221
+ arr_str = np.array2string(
222
+ data,
223
+ separator=", ",
224
+ threshold=50, # limit number of elements shown
225
+ edgeitems=3, # show first/last 3 rows and columns
226
+ max_line_width=120, # avoid wrapping
227
+ formatter={'float_kind': lambda x: f"{x:.4g}"}
228
+ )
229
+
230
+ return f"{cls}({arr_str}, shape={data.shape})"
231
+
232
+ def __repr__(self):
233
+ cls = self.__class__.__name__
234
+ data = self.data
235
+
236
+ arr_str = np.array2string(
237
+ data,
238
+ separator=", ",
239
+ threshold=50, # limit number of elements shown
240
+ edgeitems=3, # show first/last 3 rows and columns
241
+ max_line_width=120, # avoid wrapping
242
+ formatter={'float_kind': lambda x: f"{x:.4g}"}
243
+ )
244
+
245
+ return f"{cls}({arr_str}, shape={data.shape})"
246
+
247
+
248
+ #----------------------------
249
+ # Math ops
250
+ #----------------------------
251
+
252
+ def __array__(self, dtype=None):
253
+ return self.data if dtype is None else self.data.astype(dtype)
254
+
255
+ def __array_ufunc__(self, ufunc, method, *inputs, **kwargs):
256
+ if method != '__call__':
257
+ return NotImplemented
258
+
259
+ # Replace ModusaSignal instances with their data
260
+ new_inputs = [i.data if isinstance(i, ModusaSignal) else i for i in inputs]
261
+
262
+ try:
263
+ result = ufunc(*new_inputs, **kwargs)
264
+ except Exception as e:
265
+ raise TypeError(f"Ufunc {ufunc.__name__} failed: {e}")
266
+
267
+ return self._with_data(result)
268
+
269
+ def _apply_op(self, other, op, label):
270
+ if isinstance(other, ModusaSignal):
271
+ other = other.data # extract data
272
+
273
+ try:
274
+ result = op(self.data, other)
275
+ except Exception as e:
276
+ raise TypeError(f"Operation {label} failed: {e}")
277
+
278
+ return self._with_data(result)
279
+
280
+ def __add__(self, other): return self._apply_op(other, np.add, "+")
281
+ def __sub__(self, other): return self._apply_op(other, np.subtract, "-")
282
+ def __mul__(self, other): return self._apply_op(other, np.multiply, "*")
283
+ def __truediv__(self, other): return self._apply_op(other, np.divide, "/")
284
+ def __pow__(self, other): return self._apply_op(other, np.power, "**")
285
+
286
+ def __radd__(self, other): return self.__add__(other)
287
+ def __rsub__(self, other): return self._apply_op(other, lambda a, b: b - a, "r-")
288
+ def __rmul__(self, other): return self.__mul__(other)
289
+ def __rtruediv__(self, other): return self._apply_op(other, lambda a, b: b / a, "r/")
290
+ def __rpow__(self, other): return self._apply_op(other, lambda a, b: b ** a, "r**")
291
+
292
+ def __neg__(self): return self._with_data(-self.data)
293
+ def __abs__(self): return self._with_data(np.abs(self.data))
294
+