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,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,3 @@
1
+ #!/usr/bin/env python3
2
+
3
+ from .basic_waveform import BasicWaveformGenerator
@@ -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()
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env python3
2
+
3
+ from .base import ModusaPlugin
4
+
5
+ from .plot_1dsignal import Plot1DSignalPlugin
6
+ from .plot_2dmatrix import Plot2DMatrixPlugin
7
+ from .plot_time_domain_signal import PlotTimeDomainSignalPlugin
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