modusa 0.3.2__tar.gz → 0.3.4.dev2__tar.gz

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 (92) hide show
  1. {modusa-0.3.2 → modusa-0.3.4.dev2}/PKG-INFO +1 -1
  2. {modusa-0.3.2 → modusa-0.3.4.dev2}/pyproject.toml +1 -1
  3. {modusa-0.3.2 → modusa-0.3.4.dev2}/src/modusa/__init__.py +4 -3
  4. {modusa-0.3.2 → modusa-0.3.4.dev2}/src/modusa/devtools/generate_docs_source.py +6 -10
  5. {modusa-0.3.2 → modusa-0.3.4.dev2}/src/modusa/tools/__init__.py +3 -2
  6. modusa-0.3.4.dev2/src/modusa/tools/ann_loader.py +38 -0
  7. {modusa-0.3.2 → modusa-0.3.4.dev2}/src/modusa/tools/plotter.py +288 -42
  8. modusa-0.3.2/src/modusa/main.py +0 -3
  9. modusa-0.3.2/src/modusa/tmp.py +0 -98
  10. {modusa-0.3.2 → modusa-0.3.4.dev2}/LICENSE.md +0 -0
  11. {modusa-0.3.2 → modusa-0.3.4.dev2}/README.md +0 -0
  12. {modusa-0.3.2 → modusa-0.3.4.dev2}/src/modusa/.DS_Store +0 -0
  13. {modusa-0.3.2 → modusa-0.3.4.dev2}/src/modusa/config.py +0 -0
  14. {modusa-0.3.2 → modusa-0.3.4.dev2}/src/modusa/decorators.py +0 -0
  15. {modusa-0.3.2 → modusa-0.3.4.dev2}/src/modusa/devtools/generate_template.py +0 -0
  16. {modusa-0.3.2 → modusa-0.3.4.dev2}/src/modusa/devtools/list_authors.py +0 -0
  17. {modusa-0.3.2 → modusa-0.3.4.dev2}/src/modusa/devtools/list_plugins.py +0 -0
  18. {modusa-0.3.2 → modusa-0.3.4.dev2}/src/modusa/devtools/main.py +0 -0
  19. {modusa-0.3.2 → modusa-0.3.4.dev2}/src/modusa/devtools/templates/generator.py +0 -0
  20. {modusa-0.3.2 → modusa-0.3.4.dev2}/src/modusa/devtools/templates/io.py +0 -0
  21. {modusa-0.3.2 → modusa-0.3.4.dev2}/src/modusa/devtools/templates/model.py +0 -0
  22. {modusa-0.3.2 → modusa-0.3.4.dev2}/src/modusa/devtools/templates/plugin.py +0 -0
  23. {modusa-0.3.2 → modusa-0.3.4.dev2}/src/modusa/devtools/templates/test.py +0 -0
  24. {modusa-0.3.2 → modusa-0.3.4.dev2}/src/modusa/devtools/templates/tool.py +0 -0
  25. {modusa-0.3.2 → modusa-0.3.4.dev2}/src/modusa/generators/__init__.py +0 -0
  26. {modusa-0.3.2 → modusa-0.3.4.dev2}/src/modusa/generators/audio.py +0 -0
  27. {modusa-0.3.2 → modusa-0.3.4.dev2}/src/modusa/generators/audio_waveforms.py +0 -0
  28. {modusa-0.3.2 → modusa-0.3.4.dev2}/src/modusa/generators/base.py +0 -0
  29. {modusa-0.3.2 → modusa-0.3.4.dev2}/src/modusa/generators/ftds.py +0 -0
  30. {modusa-0.3.2 → modusa-0.3.4.dev2}/src/modusa/generators/s1d.py +0 -0
  31. {modusa-0.3.2 → modusa-0.3.4.dev2}/src/modusa/generators/s2d.py +0 -0
  32. {modusa-0.3.2 → modusa-0.3.4.dev2}/src/modusa/generators/s_ax.py +0 -0
  33. {modusa-0.3.2 → modusa-0.3.4.dev2}/src/modusa/generators/t_ax.py +0 -0
  34. {modusa-0.3.2 → modusa-0.3.4.dev2}/src/modusa/generators/tds.py +0 -0
  35. {modusa-0.3.2 → modusa-0.3.4.dev2}/src/modusa/models/__init__.py +0 -0
  36. {modusa-0.3.2 → modusa-0.3.4.dev2}/src/modusa/models/__pycache__/signal1D.cpython-312.pyc.4443461152 +0 -0
  37. {modusa-0.3.2 → modusa-0.3.4.dev2}/src/modusa/models/audio.py +0 -0
  38. {modusa-0.3.2 → modusa-0.3.4.dev2}/src/modusa/models/base.py +0 -0
  39. {modusa-0.3.2 → modusa-0.3.4.dev2}/src/modusa/models/data.py +0 -0
  40. {modusa-0.3.2 → modusa-0.3.4.dev2}/src/modusa/models/ftds.py +0 -0
  41. {modusa-0.3.2 → modusa-0.3.4.dev2}/src/modusa/models/s1d.py +0 -0
  42. {modusa-0.3.2 → modusa-0.3.4.dev2}/src/modusa/models/s2d.py +0 -0
  43. {modusa-0.3.2 → modusa-0.3.4.dev2}/src/modusa/models/s_ax.py +0 -0
  44. {modusa-0.3.2 → modusa-0.3.4.dev2}/src/modusa/models/t_ax.py +0 -0
  45. {modusa-0.3.2 → modusa-0.3.4.dev2}/src/modusa/models/tds.py +0 -0
  46. {modusa-0.3.2 → modusa-0.3.4.dev2}/src/modusa/plugins/__init__.py +0 -0
  47. {modusa-0.3.2 → modusa-0.3.4.dev2}/src/modusa/plugins/base.py +0 -0
  48. {modusa-0.3.2 → modusa-0.3.4.dev2}/src/modusa/tools/audio_converter.py +0 -0
  49. {modusa-0.3.2 → modusa-0.3.4.dev2}/src/modusa/tools/audio_loader.py +0 -0
  50. {modusa-0.3.2 → modusa-0.3.4.dev2}/src/modusa/tools/audio_player.py +0 -0
  51. {modusa-0.3.2 → modusa-0.3.4.dev2}/src/modusa/tools/base.py +0 -0
  52. {modusa-0.3.2 → modusa-0.3.4.dev2}/src/modusa/tools/math_ops.py +0 -0
  53. {modusa-0.3.2 → modusa-0.3.4.dev2}/src/modusa/tools/youtube_downloader.py +0 -0
  54. {modusa-0.3.2 → modusa-0.3.4.dev2}/src/modusa/utils/.DS_Store +0 -0
  55. {modusa-0.3.2 → modusa-0.3.4.dev2}/src/modusa/utils/__init__.py +0 -0
  56. {modusa-0.3.2 → modusa-0.3.4.dev2}/src/modusa/utils/config.py +0 -0
  57. {modusa-0.3.2 → modusa-0.3.4.dev2}/src/modusa/utils/excp.py +0 -0
  58. {modusa-0.3.2 → modusa-0.3.4.dev2}/src/modusa/utils/logger.py +0 -0
  59. {modusa-0.3.2 → modusa-0.3.4.dev2}/src/modusa/utils/np_func_cat.py +0 -0
  60. {modusa-0.3.2 → modusa-0.3.4.dev2}/src/modusa/utils/plot.py +0 -0
  61. {modusa-0.3.2 → modusa-0.3.4.dev2}/tests/__init__.py +0 -0
  62. {modusa-0.3.2 → modusa-0.3.4.dev2}/tests/data/song1.mp3 +0 -0
  63. {modusa-0.3.2 → modusa-0.3.4.dev2}/tests/data/song1.wav +0 -0
  64. {modusa-0.3.2 → modusa-0.3.4.dev2}/tests/test_generators/audio_waveform.py +0 -0
  65. {modusa-0.3.2 → modusa-0.3.4.dev2}/tests/test_generators/test_audio.py +0 -0
  66. {modusa-0.3.2 → modusa-0.3.4.dev2}/tests/test_generators/test_ftds.py +0 -0
  67. {modusa-0.3.2 → modusa-0.3.4.dev2}/tests/test_generators/test_s1d.py +0 -0
  68. {modusa-0.3.2 → modusa-0.3.4.dev2}/tests/test_generators/test_s2d.py +0 -0
  69. {modusa-0.3.2 → modusa-0.3.4.dev2}/tests/test_generators/test_s_ax.py +0 -0
  70. {modusa-0.3.2 → modusa-0.3.4.dev2}/tests/test_generators/test_signal.py +0 -0
  71. {modusa-0.3.2 → modusa-0.3.4.dev2}/tests/test_generators/test_signal_generator.py +0 -0
  72. {modusa-0.3.2 → modusa-0.3.4.dev2}/tests/test_generators/test_t_ax.py +0 -0
  73. {modusa-0.3.2 → modusa-0.3.4.dev2}/tests/test_generators/test_tds.py +0 -0
  74. {modusa-0.3.2 → modusa-0.3.4.dev2}/tests/test_io/audio_player.py +0 -0
  75. {modusa-0.3.2 → modusa-0.3.4.dev2}/tests/test_io/plotter.py +0 -0
  76. {modusa-0.3.2 → modusa-0.3.4.dev2}/tests/test_models/test_data.py +0 -0
  77. {modusa-0.3.2 → modusa-0.3.4.dev2}/tests/test_models/test_t_ax.py +0 -0
  78. {modusa-0.3.2 → modusa-0.3.4.dev2}/tests/test_plugins/youtube_audio_loader.py +0 -0
  79. {modusa-0.3.2 → modusa-0.3.4.dev2}/tests/test_signals/frequency_domain_signal.py +0 -0
  80. {modusa-0.3.2 → modusa-0.3.4.dev2}/tests/test_signals/spectrogram.py +0 -0
  81. {modusa-0.3.2 → modusa-0.3.4.dev2}/tests/test_signals/test_axis.py +0 -0
  82. {modusa-0.3.2 → modusa-0.3.4.dev2}/tests/test_signals/test_feature_time_domain_signal.py +0 -0
  83. {modusa-0.3.2 → modusa-0.3.4.dev2}/tests/test_signals/test_frequency_time_domain_signal.py +0 -0
  84. {modusa-0.3.2 → modusa-0.3.4.dev2}/tests/test_signals/test_signal1D.py +0 -0
  85. {modusa-0.3.2 → modusa-0.3.4.dev2}/tests/test_signals/test_signal2D.py +0 -0
  86. {modusa-0.3.2 → modusa-0.3.4.dev2}/tests/test_signals/test_time_domain_signal.py +0 -0
  87. {modusa-0.3.2 → modusa-0.3.4.dev2}/tests/test_signals/test_u_ax.py +0 -0
  88. {modusa-0.3.2 → modusa-0.3.4.dev2}/tests/test_signals/test_window_signal.py +0 -0
  89. {modusa-0.3.2 → modusa-0.3.4.dev2}/tests/test_signals/time_domain_signal.py +0 -0
  90. {modusa-0.3.2 → modusa-0.3.4.dev2}/tests/test_tools/test_audio_converter.py +0 -0
  91. {modusa-0.3.2 → modusa-0.3.4.dev2}/tests/test_tools/test_fourier_tranform.py +0 -0
  92. {modusa-0.3.2 → modusa-0.3.4.dev2}/tests/test_tools/test_math_ops.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: modusa
3
- Version: 0.3.2
3
+ Version: 0.3.4.dev2
4
4
  Summary: A modular signal analysis python library.
5
5
  Author-Email: Ankit Anand <ankit0.anand0@gmail.com>
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "modusa"
3
- version = "0.3.2"
3
+ version = "0.3.4.dev2"
4
4
  description = "A modular signal analysis python library."
5
5
  authors = [
6
6
  { name = "Ankit Anand", email = "ankit0.anand0@gmail.com" },
@@ -1,8 +1,9 @@
1
1
  from modusa.utils import excp, config
2
2
 
3
3
  #=====Giving access to plot functions to plot multiple signals.=====
4
- from modusa.tools import plot1d, plot2d
4
+ from modusa.tools import plot1d, plot2d, plot_dist
5
+ #=====
6
+
5
7
  from modusa.tools import play, convert
6
8
  from modusa.tools import download
7
- from modusa.tools import load
8
- #=====
9
+ from modusa.tools import load, load_ann
@@ -9,17 +9,15 @@ from collections import defaultdict
9
9
  # === Configuration ===
10
10
  BASE_MODULES = [
11
11
  'modusa.tools',
12
- 'modusa.models',
13
- 'modusa.generators',
14
- 'modusa.plugins',
15
- # 'modusa.io'
12
+ # 'modusa.models',
13
+ # 'modusa.generators',
14
+ # 'modusa.plugins',
16
15
  ]
17
16
  OUTPUT_DIRS = [
18
17
  Path('docs/source/tools'),
19
- Path('docs/source/models'),
20
- Path('docs/source/generators'),
21
- Path('docs/source/plugins'),
22
- # Path('docs/source/io')
18
+ # Path('docs/source/models'),
19
+ # Path('docs/source/generators'),
20
+ # Path('docs/source/plugins'),
23
21
  ]
24
22
 
25
23
  # Ensure output directories exist
@@ -89,8 +87,6 @@ def generate_docs_source():
89
87
  write_module_rst_file(module_path, class_list, output_dir)
90
88
 
91
89
  section_name = base_module.split('.')[-1].capitalize()
92
- if section_name == "Io":
93
- section_name = "IO"
94
90
 
95
91
  write_index_rst_file(module_class_map, output_dir, section_name=section_name)
96
92
  print(f"✅ Documentation generated for {base_module} in {output_dir}")
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env python3
2
2
 
3
- from .plotter import plot1d, plot2d
3
+ from .plotter import plot1d, plot2d, plot_dist
4
4
  from .audio_player import play
5
5
  from .audio_converter import convert
6
6
  from .youtube_downloader import download
7
- from .audio_loader import load
7
+ from .audio_loader import load
8
+ from .ann_loader import load_ann
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env python3
2
+
3
+ #---------------------------------
4
+ # Author: Ankit Anand
5
+ # Date: 12/08/25
6
+ # Email: ankit0.anand0@gmail.com
7
+ #---------------------------------
8
+
9
+ from pathlib import Path
10
+
11
+ def load_ann(path):
12
+ """
13
+ Load annotation from audatity label text file.
14
+
15
+ Parameters
16
+ ----------
17
+ path: str
18
+ label text file path.
19
+
20
+ Returns
21
+ -------
22
+ list[tuple, ...]
23
+ - annotation data structure
24
+ - [(start, end, tag), ...]
25
+ """
26
+
27
+ if not isinstance(path, (str, Path)):
28
+ raise ValueError(f"`path` must be one of (str, Path), got {type(path)}")
29
+
30
+ ann = []
31
+ with open(str(path), "r") as f:
32
+ lines = [line.rstrip("\n") for line in f]
33
+ for line in lines:
34
+ start, end, tag = line.split("\t")
35
+ start, end = float(start), float(end)
36
+ ann.append((start, end, tag))
37
+
38
+ return ann
@@ -7,8 +7,27 @@ import matplotlib.gridspec as gridspec
7
7
  from matplotlib.patches import Rectangle
8
8
  from mpl_toolkits.axes_grid1.inset_locator import inset_axes
9
9
 
10
+ # Helper for 2D plot
11
+ def _calculate_extent(x, y):
12
+ # Handle spacing safely
13
+ if len(x) > 1:
14
+ dx = x[1] - x[0]
15
+ else:
16
+ dx = 1 # Default spacing for single value
17
+ if len(y) > 1:
18
+ dy = y[1] - y[0]
19
+ else:
20
+ dy = 1 # Default spacing for single value
21
+
22
+ return [
23
+ x[0] - dx / 2,
24
+ x[-1] + dx / 2,
25
+ y[0] - dy / 2,
26
+ y[-1] + dy / 2
27
+ ]
28
+
10
29
  #======== 1D ===========
11
- def plot1d(*args, ann=None, events=None, xlim=None, ylim=None, xlabel=None, ylabel=None, title=None, legend=None):
30
+ def plot1d(*args, ann=None, events=None, xlim=None, ylim=None, xlabel=None, ylabel=None, title=None, legend=None, fmt=None, show_grid=False, show_stem=False):
12
31
  """
13
32
  Plots a 1D signal using matplotlib.
14
33
 
@@ -53,26 +72,40 @@ def plot1d(*args, ann=None, events=None, xlim=None, ylim=None, xlabel=None, ylab
53
72
  legend : list[str] | None
54
73
  - List of legend labels corresponding to each signal if plotting multiple lines.
55
74
  - Default: None
75
+ fmt: list[str] | None
76
+ - linefmt for different line plots.
77
+ - Default: None
78
+ show_grid: bool
79
+ - If you want to show the grid.
80
+ - Default: False
81
+ show_stem: bool:
82
+ - If you want stem plot.
83
+ - Default: False
56
84
 
57
85
  Returns
58
86
  -------
59
87
  plt.Figure
60
88
  Matplolib figure.
61
89
  """
62
-
90
+
63
91
  for arg in args:
64
92
  if len(arg) not in [1, 2]: # 1 if it just provides values, 2 if it provided axis as well
65
93
  raise ValueError(f"1D signal needs to have max 2 arrays (y, x) or simply (y, )")
94
+
66
95
  if isinstance(legend, str): legend = (legend, )
67
-
68
96
  if legend is not None:
69
97
  if len(legend) < len(args):
70
- raise ValueError(f"Legend should be provided for each signal.")
98
+ raise ValueError(f"`legend` should be provided for each signal.")
99
+
100
+ if isinstance(fmt, str): fmt = [fmt]
101
+ if fmt is not None:
102
+ if len(fmt) < len(args):
103
+ raise ValueError(f"`fmt` should be provided for each signal.")
104
+
105
+ colors = plt.get_cmap('tab10').colors
71
106
 
72
107
  fig = plt.figure(figsize=(16, 2))
73
108
  gs = gridspec.GridSpec(2, 1, height_ratios=[0.2, 1])
74
-
75
- colors = plt.get_cmap('tab10').colors
76
109
 
77
110
  signal_ax = fig.add_subplot(gs[1, 0])
78
111
  annotation_ax = fig.add_subplot(gs[0, 0], sharex=signal_ax)
@@ -83,41 +116,91 @@ def plot1d(*args, ann=None, events=None, xlim=None, ylim=None, xlabel=None, ylab
83
116
 
84
117
  if ylim is not None:
85
118
  signal_ax.set_ylim(ylim)
86
-
87
119
 
88
120
  # Add signal plot
89
121
  for i, signal in enumerate(args):
90
122
  if len(signal) == 1:
91
123
  y = signal[0]
124
+ x = np.arange(y.size)
92
125
  if legend is not None:
93
- signal_ax.plot(y, label=legend[i])
126
+ if show_stem is True:
127
+ markerline, stemlines, baseline = signal_ax.stem(x, y, label=legend[i])
128
+ markerline.set_color(colors[i])
129
+ stemlines.set_color(colors[i])
130
+ baseline.set_color("k")
131
+ else:
132
+ if fmt is not None:
133
+ signal_ax.plot(x, y, fmt[i], markersize=4, label=legend[i])
134
+ else:
135
+ signal_ax.plot(x, y, color=colors[i], label=legend[i])
94
136
  else:
95
- signal_ax.plot(y)
137
+ if show_stem is True:
138
+ markerline, stemlines, baseline = signal_ax.stem(x, y)
139
+ markerline.set_color(colors[i])
140
+ stemlines.set_color(colors[i])
141
+ baseline.set_color("k")
142
+ else:
143
+ if fmt is not None:
144
+ signal_ax.plot(x, y, fmt[i], markersize=4)
145
+ else:
146
+ signal_ax.plot(x, y, color=colors[i])
147
+
96
148
  elif len(signal) == 2:
97
149
  y, x = signal[0], signal[1]
98
150
  if legend is not None:
99
- signal_ax.plot(x, y, label=legend[i])
151
+ if show_stem is True:
152
+ markerline, stemlines, baseline = signal_ax.stem(x, y, label=legend[i])
153
+ markerline.set_color(colors[i])
154
+ stemlines.set_color(colors[i])
155
+ baseline.set_color("k")
156
+ else:
157
+ if fmt is not None:
158
+ signal_ax.plot(x, y, fmt[i], markersize=4, label=legend[i])
159
+ else:
160
+ signal_ax.plot(x, y, color=colors[i], label=legend[i])
100
161
  else:
101
- signal_ax.plot(x, y)
162
+ if show_stem is True:
163
+ markerline, stemlines, baseline = signal_ax.stem(x, y)
164
+ markerline.set_color(colors[i])
165
+ stemlines.set_color(colors[i])
166
+ baseline.set_color("k")
167
+ else:
168
+ if fmt is not None:
169
+ signal_ax.plot(x, y, fmt[i], markersize=4)
170
+ else:
171
+ signal_ax.plot(x, y, color=colors[i])
172
+
102
173
 
103
174
  # Add annotations
104
175
  if ann is not None:
105
- annotation_ax.set_ylim(0, 1)
176
+ annotation_ax.set_ylim(0, 1) # For consistent layout
177
+ # Determine visible x-range
178
+ x_view_min = xlim[0] if xlim is not None else np.min(x)
179
+ x_view_max = xlim[1] if xlim is not None else np.max(x)
180
+
106
181
  for i, (start, end, tag) in enumerate(ann):
107
- if xlim is not None:
108
- if end < xlim[0] or start > xlim[1]:
109
- continue # Skip out-of-view regions
110
- # Clip boundaries to xlim
111
- start = max(start, xlim[0])
112
- end = min(end, xlim[1])
182
+ # We make sure that we only plot annotation that are within the x range of the current view
183
+ if start >= x_view_max or end <= x_view_min:
184
+ continue
185
+
186
+ # Clip boundaries to xlim
187
+ start = max(start, x_view_min)
188
+ end = min(end, x_view_max)
113
189
 
114
190
  color = colors[i % len(colors)]
115
191
  width = end - start
116
192
  rect = Rectangle((start, 0), width, 1, color=color, alpha=0.7)
117
193
  annotation_ax.add_patch(rect)
118
- annotation_ax.text((start + end) / 2, 0.5, tag,
119
- ha='center', va='center',
120
- fontsize=10, color='white', fontweight='bold', zorder=10)
194
+
195
+ text_obj = annotation_ax.text(
196
+ (start + end) / 2, 0.5, tag,
197
+ ha='center', va='center',
198
+ fontsize=10, color='white', fontweight='bold', zorder=10, clip_on=True
199
+ )
200
+
201
+ text_obj.set_clip_path(rect)
202
+
203
+
121
204
  # Add vlines
122
205
  if events is not None:
123
206
  for xpos in events:
@@ -130,7 +213,7 @@ def plot1d(*args, ann=None, events=None, xlim=None, ylim=None, xlabel=None, ylab
130
213
  # Add legend
131
214
  if legend is not None:
132
215
  handles, labels = signal_ax.get_legend_handles_labels()
133
- fig.legend(handles, labels, loc='upper right', bbox_to_anchor=(0.9, 1.2), ncol=len(legend), frameon=False)
216
+ fig.legend(handles, labels, loc='upper right', bbox_to_anchor=(0.9, 1.2), ncol=len(legend), frameon=True)
134
217
 
135
218
  # Set title, labels
136
219
  if title is not None:
@@ -139,8 +222,12 @@ def plot1d(*args, ann=None, events=None, xlim=None, ylim=None, xlabel=None, ylab
139
222
  signal_ax.set_xlabel(xlabel)
140
223
  if ylabel is not None:
141
224
  signal_ax.set_ylabel(ylabel)
225
+
226
+ # Add grid to the plot
227
+ if show_grid is True:
228
+ signal_ax.grid(True, linestyle=':', linewidth=0.7, color='gray', alpha=0.7)
142
229
 
143
- # Decorating annotation axis thicker
230
+ # Remove the boundaries and ticks from an axis
144
231
  if ann is not None:
145
232
  annotation_ax.tick_params(left=False, bottom=False, labelleft=False, labelbottom=False)
146
233
  else:
@@ -152,7 +239,7 @@ def plot1d(*args, ann=None, events=None, xlim=None, ylim=None, xlabel=None, ylab
152
239
  return fig
153
240
 
154
241
  #======== 2D ===========
155
- def plot2d(*args, ann=None, events=None, xlim=None, ylim=None, origin="lower", Mlabel=None, xlabel=None, ylabel=None, title=None, legend=None, lm=False):
242
+ def plot2d(*args, ann=None, events=None, xlim=None, ylim=None, origin="lower", Mlabel=None, xlabel=None, ylabel=None, title=None, legend=None, lm=False, show_grid=False):
156
243
  """
157
244
  Plots a 2D matrix (e.g., spectrogram or heatmap) with optional annotations and events.
158
245
 
@@ -206,6 +293,9 @@ def plot2d(*args, ann=None, events=None, xlim=None, ylim=None, origin="lower", M
206
293
  - Adds a circular marker for the line.
207
294
  - Default: False
208
295
  - Useful to show the data points.
296
+ show_grid: bool
297
+ - If you want to show the grid.
298
+ - Default: False
209
299
 
210
300
  Returns
211
301
  -------
@@ -246,6 +336,8 @@ def plot2d(*args, ann=None, events=None, xlim=None, ylim=None, origin="lower", M
246
336
  if data.ndim == 1: # 1D
247
337
  if len(signal) == 1: # It means that the axis was not passed
248
338
  x = np.arange(data.shape[0])
339
+ else:
340
+ x = signal[1]
249
341
 
250
342
  if lm is False:
251
343
  if legend is not None:
@@ -267,36 +359,42 @@ def plot2d(*args, ann=None, events=None, xlim=None, ylim=None, origin="lower", M
267
359
  if len(signal) == 1: # It means that the axes were not passed
268
360
  y = np.arange(M.shape[0])
269
361
  x = np.arange(M.shape[1])
270
- dx = x[1] - x[0]
271
- dy = y[1] - y[0]
272
- extent=[x[0] - dx/2, x[-1] + dx/2, y[0] - dy/2, y[-1] + dy/2]
362
+ extent = _calculate_extent(x, y)
273
363
  im = signal_ax.imshow(M, aspect="auto", origin=origin, cmap="gray_r", extent=extent)
274
364
 
275
365
  elif len(signal) == 3: # It means that the axes were passed
276
366
  M, y, x = signal[0], signal[1], signal[2]
277
- dx = x[1] - x[0]
278
- dy = y[1] - y[0]
279
- extent=[x[0] - dx/2, x[-1] + dx/2, y[0] - dy/2, y[-1] + dy/2]
367
+ extent = _calculate_extent(x, y)
280
368
  im = signal_ax.imshow(M, aspect="auto", origin=origin, cmap="gray_r", extent=extent)
281
369
 
282
370
  # Add annotations
283
371
  if ann is not None:
284
- annotation_ax.set_ylim(0, 1)
372
+ annotation_ax.set_ylim(0, 1) # For consistent layout
373
+ # Determine visible x-range
374
+ x_view_min = xlim[0] if xlim is not None else np.min(x)
375
+ x_view_max = xlim[1] if xlim is not None else np.max(x)
376
+
285
377
  for i, (start, end, tag) in enumerate(ann):
286
- if xlim is not None:
287
- if end < xlim[0] or start > xlim[1]:
288
- continue # Skip out-of-view regions
289
- # Clip boundaries to xlim
290
- start = max(start, xlim[0])
291
- end = min(end, xlim[1])
378
+ # We make sure that we only plot annotation that are within the x range of the current view
379
+ if start >= x_view_max or end <= x_view_min:
380
+ continue
381
+
382
+ # Clip boundaries to xlim
383
+ start = max(start, x_view_min)
384
+ end = min(end, x_view_max)
292
385
 
293
386
  color = colors[i % len(colors)]
294
387
  width = end - start
295
388
  rect = Rectangle((start, 0), width, 1, color=color, alpha=0.7)
296
389
  annotation_ax.add_patch(rect)
297
- annotation_ax.text((start + end) / 2, 0.5, tag,
298
- ha='center', va='center',
299
- fontsize=10, color='white', fontweight='bold', zorder=10)
390
+ text_obj = annotation_ax.text(
391
+ (start + end) / 2, 0.5, tag,
392
+ ha='center', va='center',
393
+ fontsize=10, color='white', fontweight='bold', zorder=10, clip_on=True
394
+ )
395
+
396
+ text_obj.set_clip_path(rect)
397
+
300
398
  # Add vlines
301
399
  if events is not None:
302
400
  for xpos in events:
@@ -338,7 +436,10 @@ def plot2d(*args, ann=None, events=None, xlim=None, ylim=None, origin="lower", M
338
436
  signal_ax.set_xlabel(xlabel)
339
437
  if ylabel is not None:
340
438
  signal_ax.set_ylabel(ylabel)
341
-
439
+
440
+ # Add grid to the plot
441
+ if show_grid is True:
442
+ signal_ax.grid(True, linestyle=':', linewidth=0.7, color='gray', alpha=0.7)
342
443
 
343
444
  # Making annotation axis spines thicker
344
445
  if ann is not None:
@@ -348,4 +449,149 @@ def plot2d(*args, ann=None, events=None, xlim=None, ylim=None, origin="lower", M
348
449
 
349
450
  fig.subplots_adjust(hspace=0.01, wspace=0.05)
350
451
  plt.close()
351
- return fig
452
+ return fig
453
+
454
+ #======== Plot distribution ===========
455
+ def plot_dist(*args, ann=None, xlim=None, ylim=None, ylabel=None, xlabel=None, title=None, legend=None, show_hist=True, npoints=200, bins=30):
456
+ """
457
+ Plot distribution.
458
+
459
+ .. code-block:: python
460
+
461
+ import modusa as ms
462
+ import numpy as np
463
+ np.random.seed(42)
464
+ data = np.random.normal(loc=1, scale=1, size=1000)
465
+ ms.plot_dist(data, data+5, data-10, ann=[(0, 1, "A")], legend=("D1", "D2", "D3"), ylim=(0, 1), xlabel="X", ylabel="Counts", title="Distribution")
466
+
467
+ Parameters
468
+ ----------
469
+ *args: ndarray
470
+ - Data arrays for which distribution needs to be plotted.
471
+ - Arrays will be flattened.
472
+ ann : list[tuple[Number, Number, str] | None
473
+ - A list of annotations to mark specific points. Each tuple should be of the form (start, end, label).
474
+ - Default: None => No annotation.
475
+ events : list[Number] | None
476
+ - A list of x-values where vertical lines (event markers) will be drawn.
477
+ - Default: None
478
+ xlim : tuple[Number, Number] | None
479
+ - Limits for the x-axis as (xmin, xmax).
480
+ - Default: None
481
+ ylim : tuple[Number, Number] | None
482
+ - Limits for the y-axis as (ymin, ymax).
483
+ - Default: None
484
+ xlabel : str | None
485
+ - Label for the x-axis.
486
+ - - Default: None
487
+ ylabel : str | None
488
+ - Label for the y-axis.
489
+ - Default: None
490
+ title : str | None
491
+ - Title of the plot.
492
+ - Default: None
493
+ legend : list[str] | None
494
+ - List of legend labels corresponding to each signal if plotting multiple distributions.
495
+ - Default: None
496
+ show_hist: bool
497
+ - Want to show histogram as well.
498
+ npoints: int
499
+ - Number of points for which gaussian needs to be computed between min and max.
500
+ - Higher value means more points are evaluated with the fitted gaussian, thereby higher resolution.
501
+ bins: int
502
+ - The number of bins for histogram.
503
+ - This is used only to plot the histogram.
504
+
505
+ Returns
506
+ -------
507
+ plt.Figure
508
+ - Matplotlib figure.
509
+ """
510
+ from scipy.stats import gaussian_kde
511
+
512
+ if isinstance(legend, str):
513
+ legend = (legend, )
514
+
515
+ if legend is not None:
516
+ if len(legend) < len(args):
517
+ raise ValueError(f"Legend should be provided for each signal.")
518
+
519
+ # Create figure
520
+ fig = plt.figure(figsize=(16, 4))
521
+ gs = gridspec.GridSpec(2, 1, height_ratios=[0.1, 1])
522
+
523
+ colors = plt.get_cmap('tab10').colors
524
+
525
+ dist_ax = fig.add_subplot(gs[1, 0])
526
+ annotation_ax = fig.add_subplot(gs[0, 0], sharex=dist_ax)
527
+
528
+ # Set limits
529
+ if xlim is not None:
530
+ dist_ax.set_xlim(xlim)
531
+
532
+ if ylim is not None:
533
+ dist_ax.set_ylim(ylim)
534
+
535
+ # Add plot
536
+ for i, data in enumerate(args):
537
+ # Fit gaussian to the data
538
+ kde = gaussian_kde(data)
539
+
540
+ # Create points to evaluate KDE
541
+ x = np.linspace(np.min(data), np.max(data), npoints)
542
+ y = kde(x)
543
+
544
+ if legend is not None:
545
+ dist_ax.plot(x, y, color=colors[i], label=legend[i])
546
+ if show_hist is True:
547
+ dist_ax.hist(data, bins=bins, density=True, alpha=0.3, facecolor=colors[i], edgecolor='black', label=legend[i])
548
+ else:
549
+ dist_ax.plot(x, y, color=colors[i])
550
+ if show_hist is True:
551
+ dist_ax.hist(data, bins=bins, density=True, alpha=0.3, facecolor=colors[i], edgecolor='black')
552
+
553
+ # Add annotations
554
+ if ann is not None:
555
+ annotation_ax.set_ylim(0, 1) # For consistent layout
556
+ # Determine visible x-range
557
+ x_view_min = xlim[0] if xlim is not None else np.min(x)
558
+ x_view_max = xlim[1] if xlim is not None else np.max(x)
559
+ for i, (start, end, tag) in enumerate(ann):
560
+ # We make sure that we only plot annotation that are within the x range of the current view
561
+ if start >= x_view_max or end <= x_view_min:
562
+ continue
563
+
564
+ # Clip boundaries to xlim
565
+ start = max(start, x_view_min)
566
+ end = min(end, x_view_max)
567
+
568
+ color = colors[i % len(colors)]
569
+ width = end - start
570
+ rect = Rectangle((start, 0), width, 1, color=color, alpha=0.7)
571
+ annotation_ax.add_patch(rect)
572
+
573
+ text_obj = annotation_ax.text((start + end) / 2, 0.5, tag, ha='center', va='center', fontsize=10, color='white', fontweight='bold', zorder=10, clip_on=True)
574
+ text_obj.set_clip_path(rect)
575
+
576
+ # Add legend
577
+ if legend is not None:
578
+ handles, labels = dist_ax.get_legend_handles_labels()
579
+ fig.legend(handles, labels, loc='upper right', bbox_to_anchor=(0.9, 1.1), ncol=len(legend), frameon=True)
580
+
581
+ # Set title, labels
582
+ if title is not None:
583
+ annotation_ax.set_title(title, pad=10, size=11)
584
+ if xlabel is not None:
585
+ dist_ax.set_xlabel(xlabel)
586
+ if ylabel is not None:
587
+ dist_ax.set_ylabel(ylabel)
588
+
589
+ # Remove the boundaries and ticks from annotation axis
590
+ if ann is not None:
591
+ annotation_ax.tick_params(left=False, bottom=False, labelleft=False, labelbottom=False)
592
+ else:
593
+ annotation_ax.axis("off")
594
+
595
+ fig.subplots_adjust(hspace=0.01, wspace=0.05)
596
+ plt.close()
597
+ return fig
@@ -1,3 +0,0 @@
1
- #!/usr/bin/env python3
2
-
3
-
@@ -1,98 +0,0 @@
1
- #def autocorr(self) -> Self:
2
- # """
3
- #
4
- # """
5
- # raise NotImplementedError
6
- # r = np.correlate(self.data, self.data, mode="full")
7
- # r = r[self.data.shape[0] - 1:]
8
- # r_signal = self.__class__(data=r, sr=self.sr, t0=self.t0, title=self.title + " [Autocorr]")
9
- # return r_signal
10
-
11
- # #----------------------------
12
- # # To different signals
13
- # #----------------------------
14
- # def to_audio_signal(self) -> "AudioSignal":
15
- # """
16
- # Moves TimeDomainSignal to AudioSignal
17
- # """
18
- # raise NotImplementedError
19
- # from modusa.signals.audio_signal import AudioSignal
20
- #
21
- # return AudioSignal(data=self.data, sr=self.sr, t0=self.t0, title=self.title)
22
- #
23
- # def to_spectrogram(
24
- # self,
25
- # n_fft: int = 2048,
26
- # hop_length: int = 512,
27
- # win_length: int | None = None,
28
- # window: str = "hann"
29
- # ) -> "Spectrogram":
30
- # """
31
- # Compute the Short-Time Fourier Transform (STFT) and return a Spectrogram object.
32
- #
33
- # Parameters
34
- # ----------
35
- # n_fft : int
36
- # FFT size.
37
- # win_length : int or None
38
- # Window length. Defaults to `n_fft` if None.
39
- # hop_length : int
40
- # Hop length between frames.
41
- # window : str
42
- # Type of window function to use (e.g., 'hann', 'hamming').
43
- #
44
- # Returns
45
- # -------
46
- # Spectrogram
47
- # Spectrogram object containing S (complex STFT), t (time bins), and f (frequency bins).
48
- # """
49
- # raise NotImplementedError
50
- # import warnings
51
- # warnings.filterwarnings("ignore", category=UserWarning, module="librosa.core.intervals")
52
- #
53
- # from modusa.signals.feature_time_domain_signal import FeatureTimeDomainSignal
54
- # import librosa
55
- #
56
- # S = librosa.stft(self.data, n_fft=n_fft, win_length=win_length, hop_length=hop_length, window=window)
57
- # f = librosa.fft_frequencies(sr=self.sr, n_fft=n_fft)
58
- # t = librosa.frames_to_time(np.arange(S.shape[1]), sr=self.sr, hop_length=hop_length)
59
- # frame_rate = self.sr / hop_length
60
- # spec = FeatureTimeDomainSignal(data=S, feature=f, feature_label="Freq (Hz)", frame_rate=frame_rate, t0=self.t0, time_label="Time (sec)", title=self.title)
61
- # if self.title != self._name: # Means title of the audio was reset so we pass that info to spec
62
- # spec = spec.set_meta_info(title=self.title)
63
- #
64
- # return spec
65
- # #=====================================
66
-
67
- #=====================================
68
-
69
- #--------------------------
70
- # Other signal ops
71
- #--------------------------
72
-
73
- # def interpolate(self, to: TimeDomainSignal, kind: str = "linear", fill_value: str | float = "extrapolate") -> TimeDomainSignal:
74
- # """
75
- # Interpolate the current signal to match the time axis of `to`.
76
- #
77
- # Parameters:
78
- # to (TimeDomainSignal): The signal whose time axis will be used.
79
- # kind (str): Interpolation method ('linear', 'nearest', etc.)
80
- # fill_value (str or float): Value used to fill out-of-bounds.
81
- #
82
- # Returns:
83
- # TimeDomainSignal: A new signal with values interpolated at `to.t`.
84
- # """
85
- # assert self.y.ndim == 1, "Only 1D signals supported for interpolation"
86
- #
87
- # interpolator = interp1d(
88
- # self.t,
89
- # self.y,
90
- # kind=kind,
91
- # fill_value=fill_value,
92
- # bounds_error=False,
93
- # assume_sorted=True
94
- # )
95
- #
96
- # y_interp = interpolator(to.y)
97
-
98
- # return self.__class__(y=y_interp, sr=to.sr, t0=to.t0, title=f"{self.title} → interpolated")
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes