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
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,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
|
+
|
modusa/devtools/main.py
ADDED
|
@@ -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
|
+
|
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
|