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,159 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
from modusa import excp
|
|
5
|
+
from modusa.decorators import validate_args_type
|
|
6
|
+
from modusa.engines.base import ModusaEngine
|
|
7
|
+
from typing import Any
|
|
8
|
+
import numpy as np
|
|
9
|
+
import matplotlib.pyplot as plt
|
|
10
|
+
from matplotlib.patches import Rectangle
|
|
11
|
+
from matplotlib.ticker import MaxNLocator
|
|
12
|
+
|
|
13
|
+
class Plot2DMatrixEngine(ModusaEngine):
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
#--------Meta Information----------
|
|
19
|
+
name = "Plot 2D Matrix"
|
|
20
|
+
description = ""
|
|
21
|
+
author_name = "Ankit Anand"
|
|
22
|
+
author_email = "ankit0.anand0@gmail.com"
|
|
23
|
+
created_at = "2025-07-02"
|
|
24
|
+
#----------------------------------
|
|
25
|
+
|
|
26
|
+
def __init__(self):
|
|
27
|
+
super().__init__()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@staticmethod
|
|
31
|
+
def compute_centered_extent(r: np.ndarray, c: np.ndarray, origin: str) -> list[float]:
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
"""
|
|
35
|
+
dc = np.diff(c).mean() if len(c) > 1 else 1
|
|
36
|
+
dr = np.diff(r).mean() if len(r) > 1 else 1
|
|
37
|
+
left = c[0] - dc / 2
|
|
38
|
+
right = c[-1] + dc / 2
|
|
39
|
+
bottom = r[0] - dr / 2
|
|
40
|
+
top = r[-1] + dr / 2
|
|
41
|
+
return [left, right, top, bottom] if origin == "upper" else [left, right, bottom, top]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@validate_args_type()
|
|
45
|
+
def run(
|
|
46
|
+
self,
|
|
47
|
+
M: np.ndarray,
|
|
48
|
+
r: np.ndarray,
|
|
49
|
+
c: np.ndarray,
|
|
50
|
+
log_compression_factor: int | float | None,
|
|
51
|
+
ax: plt.Axes | None,
|
|
52
|
+
labels: tuple[str, str, str, str] | None,
|
|
53
|
+
zoom: tuple[float, float, float, float] | None, # (r1, r2, c1, c2)
|
|
54
|
+
highlight: list[tuple[float, float, float, float]] | None,
|
|
55
|
+
cmap: str,
|
|
56
|
+
origin: str, # or "lower"
|
|
57
|
+
show_colorbar: bool,
|
|
58
|
+
cax: plt.Axes | None,
|
|
59
|
+
show_grid: bool,
|
|
60
|
+
tick_mode: str, # "center" or "edge"
|
|
61
|
+
n_ticks: tuple[int, int],
|
|
62
|
+
value_range: tuple[float, float] | None,
|
|
63
|
+
) -> plt.Figure:
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
# Validate the important args and get the signal that needs to be plotted
|
|
67
|
+
if M.ndim != 2:
|
|
68
|
+
raise excp.InputValueError(f"`M` must have 2 dimension not {M.ndim}")
|
|
69
|
+
if r.ndim != 1 and c.ndim != 1:
|
|
70
|
+
raise excp.InputValueError(f"`r` and `c` must have 2 dimension not r:{r.ndim}, c:{c.ndim}")
|
|
71
|
+
|
|
72
|
+
if r.shape[0] != M.shape[0]:
|
|
73
|
+
raise excp.InputValueError(f"`r` must have shape as `M row` not {r.shape}")
|
|
74
|
+
if c.shape[0] != M.shape[1]:
|
|
75
|
+
raise excp.InputValueError(f"`c` must have shape as `M column` not {c.shape}")
|
|
76
|
+
|
|
77
|
+
# Scale the signal if needed
|
|
78
|
+
if log_compression_factor is not None:
|
|
79
|
+
M = np.log1p(float(log_compression_factor) * M)
|
|
80
|
+
|
|
81
|
+
# Create a figure
|
|
82
|
+
if ax is None:
|
|
83
|
+
fig, ax = plt.subplots(figsize=(15, 4))
|
|
84
|
+
created_fig = True
|
|
85
|
+
else:
|
|
86
|
+
fig = ax.get_figure()
|
|
87
|
+
created_fig = False
|
|
88
|
+
|
|
89
|
+
# Plot the signal with right configurations
|
|
90
|
+
# Compute extent
|
|
91
|
+
extent = Plot2DMatrixEngine.compute_centered_extent(r, c, origin)
|
|
92
|
+
|
|
93
|
+
# Plot image
|
|
94
|
+
im = ax.imshow(
|
|
95
|
+
M,
|
|
96
|
+
aspect="auto",
|
|
97
|
+
cmap=cmap,
|
|
98
|
+
origin=origin,
|
|
99
|
+
extent=extent,
|
|
100
|
+
vmin=value_range[0] if value_range else None,
|
|
101
|
+
vmax=value_range[1] if value_range else None,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Set the ticks and labels
|
|
105
|
+
if tick_mode == "center":
|
|
106
|
+
ax.yaxis.set_major_locator(MaxNLocator(nbins=n_ticks[0]))
|
|
107
|
+
ax.xaxis.set_major_locator(MaxNLocator(nbins=n_ticks[1])) # limits ticks
|
|
108
|
+
|
|
109
|
+
elif tick_mode == "edge":
|
|
110
|
+
dc = np.diff(c).mean() if len(c) > 1 else 1
|
|
111
|
+
dr = np.diff(r).mean() if len(r) > 1 else 1
|
|
112
|
+
ax.set_xticks(np.append(c, c[-1] + dc) - dc / 2)
|
|
113
|
+
ax.set_yticks(np.append(r, r[-1] + dr) - dr / 2)
|
|
114
|
+
|
|
115
|
+
if labels is not None:
|
|
116
|
+
if len(labels) > 0:
|
|
117
|
+
ax.set_title(labels[0])
|
|
118
|
+
if len(labels) > 2:
|
|
119
|
+
ax.set_ylabel(labels[2])
|
|
120
|
+
if len(labels) > 3:
|
|
121
|
+
ax.set_xlabel(labels[3])
|
|
122
|
+
|
|
123
|
+
# Zoom into a region
|
|
124
|
+
if zoom is not None:
|
|
125
|
+
r1, r2, c1, c2 = zoom
|
|
126
|
+
ax.set_xlim(min(c1, c2), max(c1, c2))
|
|
127
|
+
ax.set_ylim(
|
|
128
|
+
(min(r1, r2), max(r1, r2)) if origin == "lower" else (max(r1, r2), min(r1, r2))
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# Highlight a list of regions
|
|
132
|
+
if highlight is not None:
|
|
133
|
+
for r1, r2, c1, c2 in highlight:
|
|
134
|
+
row_min, row_max = min(r1, r2), max(r1, r2)
|
|
135
|
+
col_min, col_max = min(c1, c2), max(c1, c2)
|
|
136
|
+
width = col_max - col_min
|
|
137
|
+
height = row_max - row_min
|
|
138
|
+
ax.add_patch(Rectangle((col_min, row_min), width, height, color='red', alpha=0.2, zorder=10))
|
|
139
|
+
|
|
140
|
+
# Show colorbar
|
|
141
|
+
if show_colorbar is not None:
|
|
142
|
+
cbar = fig.colorbar(im, ax=ax, cax=cax)
|
|
143
|
+
if len(labels) > 1:
|
|
144
|
+
cbar.set_label(labels[1])
|
|
145
|
+
|
|
146
|
+
# Show grid
|
|
147
|
+
if show_grid:
|
|
148
|
+
ax.grid(True, color="gray", linestyle="--", linewidth=0.5) # TODO
|
|
149
|
+
|
|
150
|
+
# Show/Return the figure as per needed
|
|
151
|
+
if created_fig:
|
|
152
|
+
fig.tight_layout()
|
|
153
|
+
try:
|
|
154
|
+
get_ipython
|
|
155
|
+
plt.close(fig)
|
|
156
|
+
return fig
|
|
157
|
+
except NameError:
|
|
158
|
+
plt.show()
|
|
159
|
+
return fig
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
from modusa import excp
|
|
4
|
+
from modusa.decorators import validate_args_type, immutable_property
|
|
5
|
+
from modusa.signals import ModusaSignal
|
|
6
|
+
from abc import ABC, abstractmethod
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
class ModusaGenerator(ABC):
|
|
10
|
+
"""
|
|
11
|
+
Base class for any generator.
|
|
12
|
+
|
|
13
|
+
Generates instance of different `ModusaSignal` subclass.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
#--------Meta Information----------
|
|
17
|
+
name = ""
|
|
18
|
+
description = ""
|
|
19
|
+
author_name = "Ankit Anand"
|
|
20
|
+
author_email = "ankit0.anand0@gmail.com"
|
|
21
|
+
created_at = "2025-07-04"
|
|
22
|
+
#----------------------------------
|
|
23
|
+
|
|
24
|
+
def __init__(self, signal_cls: Any):
|
|
25
|
+
|
|
26
|
+
if not issubclass(signal_cls, ModusaSignal):
|
|
27
|
+
raise excp.InputValueError(f"`signal_cls` must be a subclass of ModusaSignal, got {signal_cls.__name__}")
|
|
28
|
+
if signal_cls not in self.allowed_output_signal_types:
|
|
29
|
+
raise excp.InputValueError(f"`signal_cls` must be a one of the allowed types {self.allowed_output_signal_types}, got {signal_cls.__name__}")
|
|
30
|
+
|
|
31
|
+
self._signal_cls = signal_cls
|
|
32
|
+
|
|
33
|
+
@immutable_property("Mutation not allowed.")
|
|
34
|
+
def signal_cls(self) -> Any:
|
|
35
|
+
return self._signal_cls
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
@abstractmethod
|
|
39
|
+
def allowed_output_signal_types(self) -> tuple[type, ...]:
|
|
40
|
+
return
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
from modusa.decorators import validate_args_type
|
|
5
|
+
from modusa.generators.base import ModusaGenerator
|
|
6
|
+
from modusa.signals import UniformTimeDomainSignal, AudioSignal
|
|
7
|
+
from typing import Any
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
class BasicWaveformGenerator(ModusaGenerator):
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
#--------Meta Information----------
|
|
16
|
+
name = ""
|
|
17
|
+
description = ""
|
|
18
|
+
author_name = "Ankit Anand"
|
|
19
|
+
author_email = "ankit0.anand0@gmail.com"
|
|
20
|
+
created_at = "2025-07-04"
|
|
21
|
+
#----------------------------------
|
|
22
|
+
|
|
23
|
+
def __init__(self, signal_cls: Any | None = None):
|
|
24
|
+
if signal_cls is None:
|
|
25
|
+
signal_cls = self.allowed_output_signal_types[0] # Automatically select the first signal
|
|
26
|
+
super().__init__(signal_cls)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def allowed_output_signal_types(self) -> tuple[type, ...]:
|
|
31
|
+
return (UniformTimeDomainSignal, AudioSignal)
|
|
32
|
+
|
|
33
|
+
#----------------------------
|
|
34
|
+
# Generate functions
|
|
35
|
+
#----------------------------
|
|
36
|
+
|
|
37
|
+
def generate_example(self):
|
|
38
|
+
"""
|
|
39
|
+
Generates an instance of `TimeDomainSignal` to test out the features quicky.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
T = 0.01 # Time period
|
|
43
|
+
|
|
44
|
+
t = np.arange(0, 10, T)
|
|
45
|
+
y = np.sin(2 * np.pi * 10 * t)
|
|
46
|
+
signal: UniformTimeDomainSignal | AudioSignal = self.signal_cls(y=y, t=t)
|
|
47
|
+
signal.set_units(t_unit="sec")
|
|
48
|
+
signal.set_name("Random")
|
|
49
|
+
signal.set_plot_labels(title=signal.name, y_label="Amplitude", t_label="Time")
|
|
50
|
+
|
|
51
|
+
return signal
|
|
52
|
+
|
|
53
|
+
def generate_random(self):
|
|
54
|
+
"""
|
|
55
|
+
Generates an instance of `TimeDomainSignal` with random initialisation.
|
|
56
|
+
Good for testing purposes.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
T = np.random.random()
|
|
60
|
+
|
|
61
|
+
t = np.arange(0, np.random.randint(10, 40), T)
|
|
62
|
+
y = np.random.random(size=t.shape[0])
|
|
63
|
+
signal: UniformTimeDomainSignal | AudioSignal = self.signal_cls(y=y, t=t)
|
|
64
|
+
signal.set_units(t_unit="sec")
|
|
65
|
+
signal.set_name("Random")
|
|
66
|
+
signal.set_plot_labels(title=signal.name, y_label="Amplitude", t_label="Time")
|
|
67
|
+
|
|
68
|
+
return signal
|
|
69
|
+
|
|
70
|
+
@validate_args_type()
|
|
71
|
+
def generate_sinusoid(
|
|
72
|
+
self,
|
|
73
|
+
A: float | int = 1.0,
|
|
74
|
+
f: float | int = 10.0,
|
|
75
|
+
phi: float | int = 0.0,
|
|
76
|
+
duration: float | int = 1.0,
|
|
77
|
+
Ts: float | int = 0.01,
|
|
78
|
+
):
|
|
79
|
+
"""
|
|
80
|
+
Generates a sinusoidal `TimeDomainSignal` with specified frequency, duration, sampling period, and phase.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
A, f, phi, duration, Ts = float(A), float(f), float(phi), float(duration), float(Ts)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
t = np.arange(0, duration, Ts)
|
|
87
|
+
y = A * np.sin(2 * np.pi * f * t + phi)
|
|
88
|
+
|
|
89
|
+
signal = self.signal_cls(y=y, t=t)
|
|
90
|
+
signal.set_name(name=f"Sinusoid: {f}Hz")
|
|
91
|
+
signal.set_plot_labels(
|
|
92
|
+
title=signal.name,
|
|
93
|
+
y_label="Amplitude",
|
|
94
|
+
t_label="Time"
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
return signal
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@validate_args_type()
|
|
101
|
+
def generate_square(
|
|
102
|
+
self,
|
|
103
|
+
A: float | int = 1.0,
|
|
104
|
+
f: float | int = 10.0,
|
|
105
|
+
phi: float | int = 0.0,
|
|
106
|
+
duration: float | int = 1.0,
|
|
107
|
+
Ts: float | int = 0.01,
|
|
108
|
+
):
|
|
109
|
+
"""
|
|
110
|
+
Generates a square wave
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
A, f, phi, duration, Ts = float(A), float(f), float(phi), float(duration), float(Ts)
|
|
114
|
+
|
|
115
|
+
t = np.arange(0, duration, Ts)
|
|
116
|
+
y = A * np.sign(np.sin(2 * np.pi * f * t + phi))
|
|
117
|
+
|
|
118
|
+
signal: UniformTimeDomainSignal | AudioSignal = self.signal_cls(y=y, t=t)
|
|
119
|
+
signal.set_units(t_unit="sec")
|
|
120
|
+
signal.set_name(name=f"Square: {f}Hz")
|
|
121
|
+
signal.set_plot_labels(
|
|
122
|
+
title=signal.name,
|
|
123
|
+
y_label="Amplitude",
|
|
124
|
+
t_label="Time"
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
return signal
|
|
128
|
+
|
|
129
|
+
@validate_args_type()
|
|
130
|
+
def generate_sawtooth(
|
|
131
|
+
self,
|
|
132
|
+
A: float | int = 1.0,
|
|
133
|
+
f: float | int = 10.0,
|
|
134
|
+
phi: float | int = 0.0,
|
|
135
|
+
duration: float | int = 1.0,
|
|
136
|
+
Ts: float | int = 0.01,
|
|
137
|
+
):
|
|
138
|
+
"""
|
|
139
|
+
Generates a sawtooth wave.
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
A, f, phi, duration, Ts = float(A), float(f), float(phi), float(duration), float(Ts)
|
|
143
|
+
|
|
144
|
+
t = np.arange(0, duration, Ts)
|
|
145
|
+
y = A * (2 * (t * f + phi) % 1 - 1)
|
|
146
|
+
|
|
147
|
+
signal: UniformTimeDomainSignal | AudioSignal = self.signal_cls(y=y, t=t)
|
|
148
|
+
signal.set_units(t_unit="sec")
|
|
149
|
+
signal.set_name(name=f"Sawtooth: {f}Hz")
|
|
150
|
+
signal.set_plot_labels(
|
|
151
|
+
title=signal.name,
|
|
152
|
+
y_label="Amplitude",
|
|
153
|
+
t_label="Time"
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
return signal
|
|
157
|
+
|
|
158
|
+
@validate_args_type()
|
|
159
|
+
def generate_triangle(
|
|
160
|
+
self,
|
|
161
|
+
A: float | int = 1.0,
|
|
162
|
+
f: float | int = 10.0,
|
|
163
|
+
phi: float | int = 0.0,
|
|
164
|
+
duration: float | int = 1.0,
|
|
165
|
+
Ts: float | int = 0.01,
|
|
166
|
+
):
|
|
167
|
+
"""
|
|
168
|
+
Generates a triangle wave
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
A, f, phi, duration, Ts = float(A), float(f), float(phi), float(duration), float(Ts)
|
|
172
|
+
|
|
173
|
+
t = np.arange(0, duration, Ts)
|
|
174
|
+
signal = A * np.abs(2 * (t * f + phi) % 1 - 1)
|
|
175
|
+
|
|
176
|
+
signal: UniformTimeDomainSignal | AudioSignal = self.signal_cls(y=y)
|
|
177
|
+
signal.set_units(t_unit="sec")
|
|
178
|
+
signal.set_name(name=f"Triangle: {f}Hz")
|
|
179
|
+
signal.set_plot_labels(
|
|
180
|
+
title=signal.name,
|
|
181
|
+
y_label="Amplitude",
|
|
182
|
+
t_label="Time"
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
return signal
|
modusa/main.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
from line_profiler import LineProfiler
|
|
5
|
+
from modusa.engines.plot_2dmatrix import Plot2DMatrixEngine
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
M = np.abs(np.random.rand(1000, 1000))
|
|
9
|
+
r = np.linspace(0, 1, M.shape[0])
|
|
10
|
+
c = np.linspace(0, 1, M.shape[1])
|
|
11
|
+
|
|
12
|
+
engine = Plot2DMatrixEngine()
|
|
13
|
+
|
|
14
|
+
lp = LineProfiler()
|
|
15
|
+
lp.add_function(engine.run.__wrapped__)
|
|
16
|
+
|
|
17
|
+
lp_wrapper = lp(engine.run)
|
|
18
|
+
lp_wrapper(
|
|
19
|
+
M, r, c,
|
|
20
|
+
log_compression_factor=None,
|
|
21
|
+
ax=None,
|
|
22
|
+
labels=("Title", "Colorbar", "Y", "X"),
|
|
23
|
+
zoom=None,
|
|
24
|
+
highlight=None,
|
|
25
|
+
cmap="gray_r",
|
|
26
|
+
origin="lower",
|
|
27
|
+
show_colorbar=True,
|
|
28
|
+
cax=None,
|
|
29
|
+
show_grid=False,
|
|
30
|
+
tick_mode="center",
|
|
31
|
+
n_ticks=(10, 10),
|
|
32
|
+
value_range=None
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
lp.print_stats()
|
modusa/plugins/base.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
from modusa.decorators import plugin_safety_check
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
class ModusaPlugin(ABC):
|
|
8
|
+
"""
|
|
9
|
+
Base class for any Plugin that follows the modusa framework.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
@property
|
|
13
|
+
@abstractmethod
|
|
14
|
+
def allowed_input_signal_types(self) -> tuple[type, ...]:
|
|
15
|
+
"""
|
|
16
|
+
Define the expected signal types for this plugin.
|
|
17
|
+
|
|
18
|
+
Note
|
|
19
|
+
----
|
|
20
|
+
- Must be implemented by every `ModusaPlugin` subclass.
|
|
21
|
+
- Clearly states what signal types are accepted in `.apply()`.
|
|
22
|
+
- Return a tuple of accepted signal classes.
|
|
23
|
+
|
|
24
|
+
Examples
|
|
25
|
+
--------
|
|
26
|
+
.. code-block:: python
|
|
27
|
+
|
|
28
|
+
# Single type
|
|
29
|
+
from modusa.signals import Signal1D
|
|
30
|
+
return (Signal1D, )
|
|
31
|
+
|
|
32
|
+
# Multiple types
|
|
33
|
+
from modusa.signals import Signal1D, Signal2D
|
|
34
|
+
return (Signal1D, Signal2D)
|
|
35
|
+
"""
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
@abstractmethod
|
|
40
|
+
def allowed_output_signal_types(self) -> tuple[type, ...]:
|
|
41
|
+
"""
|
|
42
|
+
Defines the allowed return types from the `.apply()` method.
|
|
43
|
+
|
|
44
|
+
Note
|
|
45
|
+
----
|
|
46
|
+
- Must be implemented by every `ModusaPlugin` subclass.
|
|
47
|
+
- Clearly declares what return types are valid.
|
|
48
|
+
- Return a tuple of accepted types (usually signal classes).
|
|
49
|
+
|
|
50
|
+
Examples
|
|
51
|
+
--------
|
|
52
|
+
.. code-block:: python
|
|
53
|
+
|
|
54
|
+
# Single type allowed
|
|
55
|
+
from modusa.signals import Signal1D
|
|
56
|
+
return (Signal1D, )
|
|
57
|
+
|
|
58
|
+
# Multiple types allowed
|
|
59
|
+
from modusa.signals import Signal1D, Signal2D
|
|
60
|
+
return (Signal1D, Signal2D)
|
|
61
|
+
|
|
62
|
+
# Return type can also be None (e.g., for plot-only plugins)
|
|
63
|
+
from modusa.signals import Signal1D, Signal2D
|
|
64
|
+
return (Signal1D, Signal2D, type(None))
|
|
65
|
+
"""
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
@plugin_safety_check()
|
|
69
|
+
@abstractmethod
|
|
70
|
+
def apply(self, signal: Any) -> Any:
|
|
71
|
+
"""
|
|
72
|
+
Defines the main processing logic of the plugin.
|
|
73
|
+
|
|
74
|
+
Note
|
|
75
|
+
----
|
|
76
|
+
- Must be implemented by every `ModusaPlugin` subclass.
|
|
77
|
+
- It is highly advised to wrap it with `@plugin_safety_check()` to:
|
|
78
|
+
- Validate input and output types.
|
|
79
|
+
- Enforce plugin contracts for safe execution.
|
|
80
|
+
|
|
81
|
+
Warning
|
|
82
|
+
-------
|
|
83
|
+
- You should not make this method as `classmethod` or `staticmethod`, this will break plugin safety check.
|
|
84
|
+
|
|
85
|
+
Example
|
|
86
|
+
-------
|
|
87
|
+
.. code-block:: python
|
|
88
|
+
|
|
89
|
+
@plugin_safety_check()
|
|
90
|
+
def apply(self, signal: "TimeDomainSignal") -> "TimeDomainSignal":
|
|
91
|
+
from modusa.engines import SomeEngine
|
|
92
|
+
new_signal: TimeDomainSignal = SomeEngine.run(signal)
|
|
93
|
+
|
|
94
|
+
return new_signal
|
|
95
|
+
"""
|
|
96
|
+
pass
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
|
|
@@ -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 Plot1DSignalPlugin(ModusaPlugin):
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
#--------Meta Information----------
|
|
14
|
+
name = "Plot 1D Signal Plugin"
|
|
15
|
+
description = "A 1D signal plotter plugin with various features."
|
|
16
|
+
author_name = "Ankit Anand"
|
|
17
|
+
author_email = "ankit0.anand0@gmail.com"
|
|
18
|
+
created_at = "2025-07-02"
|
|
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 Signal1D
|
|
27
|
+
return (Signal1D, )
|
|
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)) # None is returned when we want to plot on a given axes, no figure is created in that case
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@plugin_safety_check()
|
|
36
|
+
@validate_args_type()
|
|
37
|
+
def apply(
|
|
38
|
+
self,
|
|
39
|
+
signal: "Signal1D",
|
|
40
|
+
scale_y: tuple[float, float] | None = None,
|
|
41
|
+
scale_x: tuple[float, float] | None = None,
|
|
42
|
+
ax: plt.Axes | None = None,
|
|
43
|
+
color: str | None = "k",
|
|
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.x, scale_y=scale_y, scale_x=scale_x, 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,76 @@
|
|
|
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 Plot2DMatrixPlugin(ModusaPlugin):
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
#--------Meta Information----------
|
|
14
|
+
name = "Plot 2D Matrix Plugin"
|
|
15
|
+
description = "A 2D matrix plotter plugin with various features."
|
|
16
|
+
author_name = "Ankit Anand"
|
|
17
|
+
author_email = "ankit0.anand0@gmail.com"
|
|
18
|
+
created_at = "2025-07-02"
|
|
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 Signal2D
|
|
27
|
+
return (Signal2D, )
|
|
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)) # None is returned when we want to plot on a given axes, no figure is created in that case
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@plugin_safety_check()
|
|
36
|
+
@validate_args_type()
|
|
37
|
+
def apply(self,
|
|
38
|
+
signal: "Signal2D",
|
|
39
|
+
log_compression_factor: int | float | None = None,
|
|
40
|
+
ax: plt.Axes | None = None,
|
|
41
|
+
labels: tuple[str, str, str, str] | None = None,
|
|
42
|
+
zoom: tuple[float, float, float, float] | None = None, # (r1, r2, c1, c2)
|
|
43
|
+
highlight: list[tuple[float, float, float, float]] | None = None,
|
|
44
|
+
cmap: str = "gray_r",
|
|
45
|
+
origin: str = "upper", # or "lower"
|
|
46
|
+
show_colorbar: bool = True,
|
|
47
|
+
cax: plt.Axes | None = None,
|
|
48
|
+
show_grid: bool = False,
|
|
49
|
+
tick_mode: str = "center", # or "edge"
|
|
50
|
+
n_ticks: tuple[int, int] = (11, 21),
|
|
51
|
+
value_range: tuple[float, float] | None = None,
|
|
52
|
+
|
|
53
|
+
) -> "Signal2D":
|
|
54
|
+
|
|
55
|
+
# Run the engine here
|
|
56
|
+
from modusa.engines import Plot2DMatrixEngine
|
|
57
|
+
|
|
58
|
+
fig: plt.Figure = Plot2DMatrixEngine().run(
|
|
59
|
+
M=signal.M,
|
|
60
|
+
r=signal.r,
|
|
61
|
+
c=signal.c,
|
|
62
|
+
log_compression_factor=log_compression_factor,
|
|
63
|
+
ax=ax,
|
|
64
|
+
labels=signal.labels,
|
|
65
|
+
zoom=zoom,
|
|
66
|
+
highlight=highlight,
|
|
67
|
+
cmap=cmap,
|
|
68
|
+
origin=origin,
|
|
69
|
+
show_colorbar=show_colorbar,
|
|
70
|
+
cax=cax,
|
|
71
|
+
show_grid=show_grid,
|
|
72
|
+
tick_mode=tick_mode,
|
|
73
|
+
n_ticks=n_ticks,
|
|
74
|
+
value_range=value_range)
|
|
75
|
+
|
|
76
|
+
return fig
|