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.
- modusa/.DS_Store +0 -0
- modusa/__init__.py +1 -0
- modusa/config.py +18 -0
- modusa/decorators.py +176 -0
- modusa/devtools/generate_template.py +79 -0
- modusa/devtools/list_authors.py +2 -0
- modusa/devtools/list_plugins.py +60 -0
- modusa/devtools/main.py +42 -0
- modusa/devtools/templates/engines.py +28 -0
- modusa/devtools/templates/generators.py +26 -0
- modusa/devtools/templates/plugins.py +40 -0
- modusa/devtools/templates/signals.py +63 -0
- modusa/engines/__init__.py +4 -0
- modusa/engines/base.py +14 -0
- modusa/engines/plot_1dsignal.py +130 -0
- modusa/engines/plot_2dmatrix.py +159 -0
- modusa/generators/__init__.py +3 -0
- modusa/generators/base.py +40 -0
- modusa/generators/basic_waveform.py +185 -0
- modusa/main.py +35 -0
- modusa/plugins/__init__.py +7 -0
- modusa/plugins/base.py +100 -0
- modusa/plugins/plot_1dsignal.py +59 -0
- modusa/plugins/plot_2dmatrix.py +76 -0
- modusa/plugins/plot_time_domain_signal.py +59 -0
- modusa/signals/__init__.py +9 -0
- modusa/signals/audio_signal.py +230 -0
- modusa/signals/base.py +294 -0
- modusa/signals/signal1d.py +311 -0
- modusa/signals/signal2d.py +226 -0
- modusa/signals/uniform_time_domain_signal.py +212 -0
- modusa/utils/.DS_Store +0 -0
- modusa/utils/__init__.py +1 -0
- modusa/utils/config.py +25 -0
- modusa/utils/excp.py +71 -0
- modusa/utils/logger.py +18 -0
- modusa-0.1.0.dist-info/METADATA +86 -0
- modusa-0.1.0.dist-info/RECORD +41 -0
- modusa-0.1.0.dist-info/WHEEL +4 -0
- modusa-0.1.0.dist-info/entry_points.txt +5 -0
- 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,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
|
+
|