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
modusa/.DS_Store ADDED
Binary file
modusa/__init__.py ADDED
@@ -0,0 +1 @@
1
+ from modusa.utils import excp, config
modusa/config.py ADDED
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env python3
2
+
3
+ import logging
4
+
5
+ class Config:
6
+ LOG_LEVEL = logging.WARNING
7
+ SR = 44100 # Default sampling rate
8
+ TIME_UNIT = "sec"
9
+
10
+
11
+ def __str__(self):
12
+ return self.__dict__
13
+
14
+ def __repr__(self):
15
+ return self.__dict__
16
+
17
+ # Create a singleton instance
18
+ config = Config()
modusa/decorators.py ADDED
@@ -0,0 +1,176 @@
1
+ #!/usr/bin/env python3
2
+
3
+ from modusa import excp
4
+ from functools import wraps
5
+ from typing import Any, Callable, Type
6
+ from inspect import signature, Parameter
7
+ from typing import get_origin, get_args, Union
8
+ import types
9
+
10
+
11
+ #----------------------------------------------------
12
+ # Safety check for plugin (apply method)
13
+ # Check if the input type, output type is allowed,
14
+ # Also logs plugin usage.
15
+ #----------------------------------------------------
16
+ def plugin_safety_check(
17
+ validate_plugin_input: bool = True,
18
+ validate_plugin_output: bool = True,
19
+ track_plugin_usage: bool = True
20
+ ):
21
+ def decorator(func: Callable) -> Callable:
22
+ @wraps(func)
23
+ def wrapper(self, signal: Any, *args, **kwargs):
24
+
25
+ if validate_plugin_input:
26
+ if not hasattr(self, 'allowed_input_signal_types'):
27
+ raise excp.AttributeNotFoundError(f"{self.__class__.__name__} must define `allowed_input_signal_types`.")
28
+
29
+ if type(signal) not in self.allowed_input_signal_types:
30
+ raise excp.PluginInputError(f"{self.__class__.__name__} must take input signal of type {self.allowed_input_signal_types} but got {type(signal)}")
31
+
32
+ if track_plugin_usage:
33
+ if not hasattr(signal, '_plugin_chain'):
34
+ raise excp.AttributeNotFoundError(f"Signal of type {type(signal).__name__} must have a `_plugin_chain` attribute for plugin tracking.")
35
+
36
+ if not isinstance(signal._plugin_chain, list):
37
+ raise excp.TypeError(f"`_plugin_chain` must be a list, but got {type(signal._plugin_chain)}")
38
+
39
+ signal._plugin_chain.append(self.__class__.__name__)
40
+
41
+ result = func(self, signal, *args, **kwargs)
42
+
43
+ if validate_plugin_output:
44
+ if not hasattr(self, 'allowed_output_signal_types'):
45
+ raise excp.AttributeNotFoundError(f"{self.__class__.__name__} must define `allowed_output_signal_types`.")
46
+ if type(result) not in self.allowed_output_signal_types:
47
+ raise excp.PluginInputError(f"{self.__class__.__name__} must return output of type {self.allowed_output_signal_types} but returned {type(result)}")
48
+ return result
49
+
50
+ return wrapper
51
+ return decorator
52
+
53
+
54
+ #----------------------------------------------------
55
+ # Safety check for generators (generate method)
56
+ # Check if the ouput type is allowed.
57
+ #----------------------------------------------------
58
+ def generator_safety_check():
59
+ """
60
+ We assume that the first argument is self, so that we can actually extract properties to
61
+ validate.
62
+ """
63
+ def decorator(func: Callable) -> Callable:
64
+ @wraps(func)
65
+ def wrapper(self, *args, **kwargs):
66
+ result = func(self, *args, **kwargs)
67
+
68
+ if not hasattr(self, 'allowed_output_signal_types'):
69
+ raise excp.AttributeNotFoundError(
70
+ f"{self.__class__.__name__} must define `allowed_output_signal_types`."
71
+ )
72
+ if type(result) not in self.allowed_output_signal_types:
73
+ raise excp.PluginInputError(
74
+ f"{self.__class__.__name__} must return output of type {self.allowed_output_signal_types}, "
75
+ f"but returned {type(result)}"
76
+ )
77
+
78
+ return result
79
+ return wrapper
80
+ return decorator
81
+
82
+
83
+ #----------------------------------------------------
84
+ # Validation for args type
85
+ # When this decorator is added to a function, it
86
+ # automatically checks all the arguments with their
87
+ # expected types. (self, forward type references are
88
+ # ignored)
89
+ #----------------------------------------------------
90
+
91
+ def validate_arg(arg_name: str, value: Any, expected_type: Any) -> None:
92
+ """
93
+ Checks if `value_type` matches `expected_type`.
94
+ Raises TypeError if not.
95
+ """
96
+ import types
97
+ from typing import get_origin, get_args, Union
98
+
99
+ origin = get_origin(expected_type)
100
+
101
+ # Handle Union (e.g. int | None)
102
+ if origin in (Union, types.UnionType):
103
+ union_args = get_args(expected_type)
104
+ for typ in union_args:
105
+ typ_origin = get_origin(typ) or typ
106
+ if type(value) is typ_origin:
107
+ return
108
+
109
+ # ❌ If none match
110
+ expected_names = ", ".join(
111
+ get_origin(t).__name__ if get_origin(t) else t.__name__ for t in union_args
112
+ )
113
+ raise excp.ValidationError(
114
+ f"Argument '{arg_name}' must be one of ({expected_names}), got {type(value).__name__}"
115
+ )
116
+
117
+ # Handle generic types like list[float], tuple[int, str]
118
+ elif origin is not None:
119
+ if type(value) is not origin:
120
+ raise excp.ValidationError(
121
+ f"Argument '{arg_name}' must be exactly of type {origin.__name__}, got {type(value).__name__}"
122
+ )
123
+ return
124
+
125
+ # ✅ Handle plain types
126
+ elif isinstance(expected_type, type):
127
+ if type(value) is not expected_type:
128
+ raise excp.ValidationError(
129
+ f"Argument '{arg_name}' must be exactly {expected_type.__name__}, got {type(value).__name__}"
130
+ )
131
+ return
132
+ # ❌ Unsupported type structure
133
+ else:
134
+ raise excp.ValidationError(f"Unsupported annotation for '{arg_name}': {expected_type}")
135
+
136
+ def validate_args_type() -> Callable:
137
+ def decorator(func: Callable) -> Callable:
138
+ @wraps(func)
139
+ def wrapper(*args, **kwargs):
140
+ sig = signature(func)
141
+ bound = sig.bind(*args, **kwargs)
142
+ bound.apply_defaults()
143
+
144
+ for arg_name, value in bound.arguments.items():
145
+ param = sig.parameters[arg_name]
146
+ expected_type = param.annotation
147
+
148
+ # Skip unannotated or special args
149
+ if expected_type is Parameter.empty or arg_name in ("self", "cls") or isinstance(expected_type, str):
150
+ continue
151
+
152
+ validate_arg(arg_name, value, expected_type) # <- this is assumed to be defined elsewhere
153
+
154
+ return func(*args, **kwargs)
155
+ return wrapper
156
+ return decorator
157
+
158
+ #-----------------------------------
159
+ # Making a property immutable
160
+ # and raising custom error message
161
+ # during attempt to modify the values
162
+ #-----------------------------------
163
+ def immutable_property(error_msg: str):
164
+ """
165
+ Returns a read-only property. Raises an error with a custom message on mutation.
166
+ """
167
+ def decorator(getter):
168
+ name = getter.__name__
169
+ private_name = f"_{name}"
170
+
171
+ def setter(self, value):
172
+ raise excp.ImmutableAttributeError(error_msg)
173
+
174
+ return property(getter, setter)
175
+
176
+ return decorator
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env python3
2
+
3
+ from datetime import date
4
+ from pathlib import Path
5
+ import questionary
6
+ import sys
7
+
8
+ ROOT_DIR = Path(__file__).parents[3].resolve()
9
+ TEMPLATES_DIR = ROOT_DIR / "src/modusa/devtools/templates"
10
+
11
+ class TemplateGenerator():
12
+ """
13
+ Generates template for `plugin`, `engine`, `signal`, `generator`.
14
+ """
15
+
16
+ @staticmethod
17
+ def ask_questions(for_what: str) -> dict:
18
+ print("----------------------")
19
+ print(for_what.upper())
20
+ print("----------------------")
21
+ module_name = questionary.text("Module name (snake_case): ").ask()
22
+ if module_name is None:
23
+ sys.exit(1)
24
+ if Path(f"src/modusa/{for_what}/{module_name}.py").exists():
25
+ print(f"⚠️ File already exists, choose another name.")
26
+ sys.exit(1)
27
+
28
+ class_name = questionary.text("Class name (CamelCase): ").ask()
29
+ if class_name is None:
30
+ sys.exit(1)
31
+
32
+ author_name = questionary.text("Author name: ").ask()
33
+ if author_name is None:
34
+ sys.exit(1)
35
+
36
+ author_email = questionary.text("Author email: ").ask()
37
+ if author_email is None:
38
+ sys.exit(1)
39
+
40
+ answers = {"module_name": module_name, "class_name": class_name, "author_name": author_name, "author_email": author_email, "date_created": date.today()}
41
+
42
+ return answers
43
+
44
+ @staticmethod
45
+ def load_template_file(for_what: str) -> str:
46
+ template_path = TEMPLATES_DIR / f"{for_what}.py"
47
+ if not template_path.exists():
48
+ print(f"❌ Template not found: {template_path}")
49
+ sys.exit(1)
50
+
51
+ template_code = template_path.read_text()
52
+
53
+ return template_code
54
+
55
+ @staticmethod
56
+ def fill_placeholders(template_code: str, placehoders_dict: dict) -> str:
57
+ template_code = template_code.format(**placehoders_dict) # Fill placeholders
58
+ return template_code
59
+
60
+ @staticmethod
61
+ def save_file(content: str, output_path: Path) -> None:
62
+ output_path.parent.mkdir(parents=True, exist_ok=True)
63
+ output_path.write_text(content)
64
+ print(f"✅ Successfully created.\n\n open {output_path.resolve()}")
65
+
66
+ @staticmethod
67
+ def create_template(for_what: str) -> None:
68
+
69
+ # Ask basic questions to create the template for `plugin`, `generator`, ...
70
+ answers: dict = TemplateGenerator.ask_questions(for_what)
71
+
72
+ # Load the correct template file
73
+ template_code: str = TemplateGenerator.load_template_file(for_what)
74
+
75
+ # Update the dynamic values based on the answers
76
+ template_code: str = TemplateGenerator.fill_placeholders(template_code, answers)
77
+
78
+ # Save it to a file and put it in the correct folder
79
+ TemplateGenerator.save_file(content=template_code, output_path=ROOT_DIR / f"src/modusa/{for_what}/{answers['module_name']}.py")
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env python3
2
+
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env python3
2
+
3
+ import inspect
4
+ from pathlib import Path
5
+
6
+ PLUGIN_PATH = Path(__file__).parent.parent / "plugins" # This directory contains all the plugins
7
+
8
+ def find_plugin_files():
9
+ return [
10
+ path for path in PLUGIN_PATH.rglob("*.py")
11
+ if path.name not in {"__init__.py", "base.py"} # We do not want to show base plugins
12
+ ]
13
+
14
+ def load_plugin_class_from_file(file_path):
15
+ import importlib.util
16
+ from modusa.plugins.base import ModusaPlugin
17
+
18
+ spec = importlib.util.spec_from_file_location(file_path.stem, file_path)
19
+ module = importlib.util.module_from_spec(spec)
20
+ try:
21
+ spec.loader.exec_module(module)
22
+ except Exception as e:
23
+ print(f"❌ Error loading {file_path}: {e}")
24
+ return []
25
+
26
+ plugin_classes = []
27
+ for _, obj in inspect.getmembers(module, inspect.isclass):
28
+ if issubclass(obj, ModusaPlugin) and obj is not ModusaPlugin:
29
+ plugin_classes.append(obj)
30
+
31
+ return plugin_classes
32
+
33
+ def list_plugins():
34
+ from rich.console import Console
35
+ from rich.table import Table
36
+ from modusa.plugins.base import ModusaPlugin
37
+
38
+ console = Console()
39
+ table = Table(title="🔌 Available Modusa Plugins")
40
+
41
+ table.add_column("Plugin", style="bold green")
42
+ table.add_column("Module", style="dim")
43
+ table.add_column("Description", style="white")
44
+
45
+ all_plugins = []
46
+
47
+ for file_path in find_plugin_files():
48
+ plugin_classes = load_plugin_class_from_file(file_path)
49
+ for cls in plugin_classes:
50
+ name = cls.__name__
51
+ module = file_path.relative_to(PLUGIN_PATH.parent)
52
+ author = getattr(cls, "author_name", "—")
53
+ email = getattr(cls, "author_email", "—")
54
+ desc = getattr(cls, "description", "—")
55
+ table.add_row(name, str(module), desc)
56
+ table.add_row("")
57
+ all_plugins.append(cls)
58
+
59
+ console.print(table)
60
+
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env python3
2
+
3
+
4
+ from .generate_template import TemplateGenerator
5
+ from .list_plugins import list_plugins
6
+ from . import list_authors
7
+
8
+ import argparse
9
+ import sys
10
+
11
+ def main():
12
+ try:
13
+ parser = argparse.ArgumentParser(
14
+ prog="modusa-dev",
15
+ description="Modusa CLI Tools"
16
+ )
17
+ subparsers = parser.add_subparsers(dest="group", required=True)
18
+
19
+ # --- CREATE group ---
20
+ create_parser = subparsers.add_parser("create", help="Create new Modusa components")
21
+ create_subparsers = create_parser.add_subparsers(dest="what", required=True)
22
+
23
+ create_subparsers.add_parser("engine", help="Create a new engine class").set_defaults(func=lambda:TemplateGenerator.create_template("engines"))
24
+ create_subparsers.add_parser("plugin", help="Create a new plugin class").set_defaults(func=lambda:TemplateGenerator.create_template("plugins"))
25
+ create_subparsers.add_parser("signal", help="Create a new signal class").set_defaults(func=lambda:TemplateGenerator.create_template("signals"))
26
+ create_subparsers.add_parser("generator", help="Create a new signal generator class").set_defaults(func=lambda:TemplateGenerator.create_template("generators"))
27
+
28
+ # --- LIST group ---
29
+ list_parser = subparsers.add_parser("list", help="List information about Modusa components")
30
+ list_subparsers = list_parser.add_subparsers(dest="what", required=True)
31
+
32
+ list_subparsers.add_parser("plugins", help="List available plugins").set_defaults(func=list_plugins)
33
+ list_subparsers.add_parser("authors", help="List plugin authors").set_defaults(func=list_authors)
34
+
35
+ # --- Parse and execute ---
36
+ args = parser.parse_args()
37
+ args.func()
38
+
39
+ except KeyboardInterrupt:
40
+ print("\n❌ Aborted by user.")
41
+ sys.exit(1)
42
+
@@ -0,0 +1,28 @@
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
+
9
+ class {class_name}(ModusaEngine):
10
+ """
11
+
12
+ """
13
+
14
+ #--------Meta Information----------
15
+ name = ""
16
+ description = ""
17
+ author_name = "{author_name}"
18
+ author_email = "{author_email}"
19
+ created_at = "{date_created}"
20
+ #----------------------------------
21
+
22
+ def __init__(self):
23
+ super().__init__()
24
+
25
+
26
+ @validate_args_type()
27
+ def run(self) -> Any:
28
+ pass
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env python3
2
+
3
+
4
+ from modusa.decorators import validate_args_type
5
+ from modusa.generators.base import ModusaGenerator
6
+
7
+
8
+ class {class_name}(ModusaGenerator):
9
+ """
10
+
11
+ """
12
+
13
+ #--------Meta Information----------
14
+ name = ""
15
+ description = ""
16
+ author_name = "{author_name}"
17
+ author_email = "{author_email}"
18
+ created_at = "{date_created}"
19
+ #----------------------------------
20
+
21
+ def __init__(self):
22
+ super().__init__()
23
+
24
+
25
+ def generate(self) -> Any:
26
+ pass
@@ -0,0 +1,40 @@
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
+
7
+
8
+ class {class_name}(ModusaPlugin):
9
+ """
10
+
11
+ """
12
+
13
+ #--------Meta Information----------
14
+ name = ""
15
+ description = ""
16
+ author_name = "{author_name}"
17
+ author_email = "{author_email}"
18
+ created_at = "{date_created}"
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
+ return ()
27
+
28
+
29
+ @immutable_property(error_msg="Mutation not allowed.")
30
+ def allowed_output_signal_types(self) -> tuple[type, ...]:
31
+ return ()
32
+
33
+
34
+ @plugin_safety_check()
35
+ @validate_args_type()
36
+ def apply(self, signal: "") -> "":
37
+
38
+ # Run the engine here
39
+
40
+ return
@@ -0,0 +1,63 @@
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
+
10
+ class {class_name}(ModusaSignal):
11
+ """
12
+
13
+ """
14
+
15
+ #--------Meta Information----------
16
+ name = ""
17
+ description = ""
18
+ author_name = "{author_name}"
19
+ author_email = "{author_email}"
20
+ created_at = "{date_created}"
21
+ #----------------------------------
22
+
23
+ def __init__(self):
24
+ super().__init__() # Instantiating `ModusaSignal` class
25
+
26
+
27
+ def _with_data(self, new_data: np.ndarray) -> Self:
28
+ """Subclasses must override this to return a copy with new data."""
29
+ raise NotImplementedError("Subclasses must implement _with_data")
30
+
31
+
32
+ #----------------------
33
+ # From methods
34
+ #----------------------
35
+ @classmethod
36
+ def from_array(cls) -> Self:
37
+ pass
38
+
39
+
40
+ #----------------------
41
+ # Setters
42
+ #----------------------
43
+
44
+
45
+
46
+
47
+ #----------------------
48
+ # Properties
49
+ #----------------------
50
+ @immutable_property("Create a new object instead.")
51
+ def data(self) -> np.ndarray:
52
+ """"""
53
+ pass
54
+
55
+ #----------------------
56
+ # Plugins Access
57
+ #----------------------
58
+ def plot(self) -> Any:
59
+ """
60
+
61
+ """
62
+ pass
63
+
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env python3
2
+
3
+ from .plot_1dsignal import Plot1DSignalEngine
4
+ from .plot_2dmatrix import Plot2DMatrixEngine
modusa/engines/base.py ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env python3
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import Any
5
+
6
+ class ModusaEngine(ABC):
7
+ """
8
+ Base class for all core logic components in the Modusa system.
9
+ Every subclass must implement the `run` method.
10
+ """
11
+
12
+ @abstractmethod
13
+ def run(self, *args, **kwargs) -> Any:
14
+ pass
@@ -0,0 +1,130 @@
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
+
12
+
13
+ class Plot1DSignalEngine(ModusaEngine):
14
+ """
15
+
16
+ """
17
+
18
+ #--------Meta Information----------
19
+ name = "Plot 1D Signal"
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
+ @validate_args_type()
31
+ def run(
32
+ self,
33
+ y: np.ndarray,
34
+ x: np.ndarray | None,
35
+ scale_y: tuple[float, float] | None,
36
+ scale_x: tuple[float, float] | None ,
37
+ ax: plt.Axes | None,
38
+ color: str,
39
+ marker: str | None,
40
+ linestyle: str | None,
41
+ stem: bool | None,
42
+ labels: tuple[str, str, str] | None,
43
+ legend_loc: str | None,
44
+ zoom: tuple | None,
45
+ highlight: list[tuple[float, float], ...] | None,
46
+ ) -> plt.Figure | None:
47
+
48
+
49
+ # Validate the important args and get the signal that needs to be plotted
50
+ if y.ndim != 1:
51
+ raise excp.InputValueError(f"`y` must be of dimension 1 not {y.ndim}.")
52
+ if y.shape[0] < 1:
53
+ raise excp.InputValueError(f"`y` must not be empty.")
54
+
55
+ if x is None:
56
+ x = np.arange(y.shape[0])
57
+ elif x.ndim != 1:
58
+ raise excp.InputValueError(f"`x` must be of dimension 1 not {x.ndim}.")
59
+ elif x.shape[0] < 1:
60
+ raise excp.InputValueError(f"`x` must not be empty.")
61
+
62
+ if x.shape[0] != y.shape[0]:
63
+ raise excp.InputValueError(f"`y` and `x` must be of same shape")
64
+
65
+ # Scale the signal if needed
66
+ if scale_y is not None:
67
+ if len(scale_y) != 2:
68
+ raise excp.InputValueError(f"`scale_y` must be tuple of two values (1, 2) => 1y+2")
69
+ a, b = scale_y
70
+ y = a * y + b
71
+
72
+ if scale_x is not None:
73
+ if len(scale_x) != 2:
74
+ raise excp.InputValueError(f"`scale_x` must be tuple of two values (1, 2) => 1x+2")
75
+ a, b = scale_x
76
+ x = a * x + b
77
+
78
+ # Create a figure
79
+ if ax is None:
80
+ fig, ax = plt.subplots(figsize=(15, 2))
81
+ created_fig = True
82
+ else:
83
+ fig = ax.get_figure()
84
+ created_fig = False
85
+
86
+ # Plot the signal with right configurations
87
+ plot_label = labels[0] if labels is not None and len(labels) > 0 else None
88
+ if stem:
89
+ ax.stem(x, y, linefmt=color, markerfmt='o', label=plot_label)
90
+ elif marker is not None:
91
+ ax.plot(x, y, c=color, linestyle=linestyle, lw=1.5, marker=marker, label=plot_label)
92
+ else:
93
+ ax.plot(x, y, c=color, linestyle=linestyle, lw=1.5, label=plot_label)
94
+
95
+ # Add legend
96
+ if plot_label is not None:
97
+ legend_loc = "upper right" if legend_loc is None else legend_loc
98
+ ax.legend(loc=legend_loc)
99
+
100
+ # Set the labels
101
+ if labels is not None:
102
+ if len(labels) > 0:
103
+ ax.set_title(labels[0])
104
+ if len(labels) > 1:
105
+ ax.set_ylabel(labels[1])
106
+ if len(labels) > 2:
107
+ ax.set_xlabel(labels[2])
108
+
109
+ # Zoom into a region
110
+ if zoom is not None:
111
+ ax.set_xlim(zoom)
112
+
113
+ # Highlight a list of regions
114
+ if highlight is not None:
115
+ for highlight_region in highlight:
116
+ if len(highlight_region) != 2:
117
+ raise excp.InputValueError(f"`highlight should be a list of tuple of 2 values (left, right) => (1, 10.5)")
118
+ l, r = highlight_region
119
+ ax.add_patch(Rectangle((l, np.min(y)), r - l, np.max(y) - np.min(y), color='red', alpha=0.2, zorder=10))
120
+
121
+ # Show/Return the figure as per needed
122
+ if created_fig:
123
+ fig.tight_layout()
124
+ try:
125
+ get_ipython
126
+ plt.close(fig) # Without this, you will see two plots in the jupyter notebook
127
+ return fig
128
+ except NameError:
129
+ plt.show()
130
+ return fig