modusa 0.3.52__tar.gz → 0.3.53__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.
- {modusa-0.3.52 → modusa-0.3.53}/PKG-INFO +1 -1
- {modusa-0.3.52 → modusa-0.3.53}/pyproject.toml +1 -1
- {modusa-0.3.52 → modusa-0.3.53}/src/modusa/__init__.py +1 -1
- {modusa-0.3.52 → modusa-0.3.53}/src/modusa/tools/__init__.py +0 -2
- modusa-0.3.53/src/modusa/tools/plotter.py +617 -0
- modusa-0.3.52/src/modusa/tools/plotter.py +0 -1340
- {modusa-0.3.52 → modusa-0.3.53}/LICENSE.md +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/README.md +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/src/modusa/.DS_Store +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/src/modusa/config.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/src/modusa/decorators.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/src/modusa/devtools/generate_docs_source.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/src/modusa/devtools/generate_template.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/src/modusa/devtools/list_authors.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/src/modusa/devtools/list_plugins.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/src/modusa/devtools/main.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/src/modusa/devtools/templates/generator.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/src/modusa/devtools/templates/io.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/src/modusa/devtools/templates/model.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/src/modusa/devtools/templates/plugin.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/src/modusa/devtools/templates/test.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/src/modusa/devtools/templates/tool.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/src/modusa/fonts/NotoSansDevanagari-Regular.ttf +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/src/modusa/generators/__init__.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/src/modusa/generators/audio.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/src/modusa/generators/audio_waveforms.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/src/modusa/generators/base.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/src/modusa/generators/ftds.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/src/modusa/generators/s1d.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/src/modusa/generators/s2d.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/src/modusa/generators/s_ax.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/src/modusa/generators/t_ax.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/src/modusa/generators/tds.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/src/modusa/models/__init__.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/src/modusa/models/__pycache__/signal1D.cpython-312.pyc.4443461152 +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/src/modusa/models/audio.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/src/modusa/models/base.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/src/modusa/models/data.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/src/modusa/models/ftds.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/src/modusa/models/s1d.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/src/modusa/models/s2d.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/src/modusa/models/s_ax.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/src/modusa/models/t_ax.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/src/modusa/models/tds.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/src/modusa/plugins/__init__.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/src/modusa/plugins/base.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/src/modusa/tools/_plotter_old.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/src/modusa/tools/ann_loader.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/src/modusa/tools/audio_converter.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/src/modusa/tools/audio_loader.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/src/modusa/tools/audio_player.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/src/modusa/tools/audio_recorder.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/src/modusa/tools/base.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/src/modusa/tools/math_ops.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/src/modusa/tools/youtube_downloader.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/src/modusa/utils/.DS_Store +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/src/modusa/utils/__init__.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/src/modusa/utils/config.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/src/modusa/utils/excp.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/src/modusa/utils/logger.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/src/modusa/utils/np_func_cat.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/src/modusa/utils/plot.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/tests/__init__.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/tests/data/song1.mp3 +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/tests/data/song1.wav +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/tests/test_generators/audio_waveform.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/tests/test_generators/test_audio.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/tests/test_generators/test_ftds.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/tests/test_generators/test_s1d.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/tests/test_generators/test_s2d.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/tests/test_generators/test_s_ax.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/tests/test_generators/test_signal.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/tests/test_generators/test_signal_generator.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/tests/test_generators/test_t_ax.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/tests/test_generators/test_tds.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/tests/test_io/audio_player.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/tests/test_io/plotter.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/tests/test_models/test_data.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/tests/test_models/test_t_ax.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/tests/test_plugins/youtube_audio_loader.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/tests/test_signals/frequency_domain_signal.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/tests/test_signals/spectrogram.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/tests/test_signals/test_axis.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/tests/test_signals/test_feature_time_domain_signal.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/tests/test_signals/test_frequency_time_domain_signal.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/tests/test_signals/test_signal1D.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/tests/test_signals/test_signal2D.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/tests/test_signals/test_time_domain_signal.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/tests/test_signals/test_u_ax.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/tests/test_signals/test_window_signal.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/tests/test_signals/time_domain_signal.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/tests/test_tools/test_audio_converter.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/tests/test_tools/test_fourier_tranform.py +0 -0
- {modusa-0.3.52 → modusa-0.3.53}/tests/test_tools/test_math_ops.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
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
|
|
4
|
+
from modusa.tools import plot_dist, fig
|
|
5
5
|
#=====
|
|
6
6
|
|
|
7
7
|
from modusa.tools import play, convert, record
|
|
@@ -0,0 +1,617 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
#---------------------------------
|
|
4
|
+
# Author: Ankit Anand
|
|
5
|
+
# Date: 26/08/25
|
|
6
|
+
# Email: ankit0.anand0@gmail.com
|
|
7
|
+
#---------------------------------
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
import matplotlib as mpl
|
|
11
|
+
import matplotlib.font_manager as fm
|
|
12
|
+
import matplotlib.pyplot as plt
|
|
13
|
+
import matplotlib.gridspec as gridspec
|
|
14
|
+
from matplotlib.patches import Rectangle
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
|
|
18
|
+
#===== Loading Devanagari font ========
|
|
19
|
+
def _load_devanagari_font():
|
|
20
|
+
"""
|
|
21
|
+
Load devanagari font as it works for both English and Hindi.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
# Path to your bundled font
|
|
25
|
+
font_path = Path(__file__).resolve().parents[1] / "fonts" / "NotoSansDevanagari-Regular.ttf"
|
|
26
|
+
|
|
27
|
+
# Register the font with matplotlib
|
|
28
|
+
fm.fontManager.addfont(str(font_path))
|
|
29
|
+
|
|
30
|
+
# Get the font family name from the file
|
|
31
|
+
hindi_font = fm.FontProperties(fname=str(font_path))
|
|
32
|
+
|
|
33
|
+
# Set as default rcParam
|
|
34
|
+
mpl.rcParams['font.family'] = hindi_font.get_name()
|
|
35
|
+
|
|
36
|
+
_load_devanagari_font()
|
|
37
|
+
#==============
|
|
38
|
+
|
|
39
|
+
class Fig:
|
|
40
|
+
"""
|
|
41
|
+
A utility class that provides easy-to-use API for
|
|
42
|
+
plotting 1D/2D signals along with clean representations
|
|
43
|
+
of annotations, events.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(self, arrangement="asm", xlim=None):
|
|
47
|
+
|
|
48
|
+
self._xlim = xlim
|
|
49
|
+
self._curr_row_idx = 1 # Starting from 1 because row 0 is reserved for reference subplot
|
|
50
|
+
self._curr_color_idx = 0 # So that we have different color across all the subplots to avoid legend confusion
|
|
51
|
+
|
|
52
|
+
# Subplot setup
|
|
53
|
+
self._fig, self._axs = self._generate_subplots(arrangement) # This will fill in the all the above variables
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _get_curr_row(self):
|
|
57
|
+
"""
|
|
58
|
+
Get the active row where you can add
|
|
59
|
+
either annotations or events.
|
|
60
|
+
"""
|
|
61
|
+
curr_row = self._axs[self._curr_row_idx]
|
|
62
|
+
self._curr_row_idx += 1
|
|
63
|
+
|
|
64
|
+
return curr_row
|
|
65
|
+
|
|
66
|
+
def _get_new_color(self):
|
|
67
|
+
"""
|
|
68
|
+
Get a new color for different lines.
|
|
69
|
+
"""
|
|
70
|
+
colors = plt.rcParams['axes.prop_cycle'].by_key()['color']
|
|
71
|
+
self._curr_color_idx += 1
|
|
72
|
+
|
|
73
|
+
return colors[self._curr_color_idx]
|
|
74
|
+
|
|
75
|
+
def _calculate_extent(self, x, y):
|
|
76
|
+
# Handle spacing safely
|
|
77
|
+
if len(x) > 1:
|
|
78
|
+
dx = x[1] - x[0]
|
|
79
|
+
else:
|
|
80
|
+
dx = 1 # Default spacing for single value
|
|
81
|
+
if len(y) > 1:
|
|
82
|
+
dy = y[1] - y[0]
|
|
83
|
+
else:
|
|
84
|
+
dy = 1 # Default spacing for single value
|
|
85
|
+
|
|
86
|
+
return [x[0] - dx / 2, x[-1] + dx / 2, y[0] - dy / 2, y[-1] + dy / 2]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _generate_subplots(self, arrangement):
|
|
90
|
+
"""
|
|
91
|
+
Generate subplots based on the configuration.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
xlim = self._xlim
|
|
95
|
+
|
|
96
|
+
n_aux_sp = arrangement.count("a")
|
|
97
|
+
n_signal_sp = arrangement.count("s")
|
|
98
|
+
n_matrix_sp = arrangement.count("m")
|
|
99
|
+
n_sp = 1 + n_aux_sp + n_signal_sp + n_matrix_sp # +1 is for the first reference subplot
|
|
100
|
+
|
|
101
|
+
# Decide heights of different subplots type
|
|
102
|
+
height = {}
|
|
103
|
+
height["r"] = 0.0 # Reference height
|
|
104
|
+
height["a"] = 0.4 # Aux height
|
|
105
|
+
height["s"] = 2.0 # Signal height
|
|
106
|
+
height["m"] = 4.0 # Matrix height
|
|
107
|
+
cbar_width = 0.01
|
|
108
|
+
|
|
109
|
+
arrangement = "r" + arrangement # "r" is to include the reference
|
|
110
|
+
|
|
111
|
+
# Calculate height ratios list based on the arrangement
|
|
112
|
+
for char in arrangement:
|
|
113
|
+
height_ratios = [height[char] for char in arrangement]
|
|
114
|
+
|
|
115
|
+
# Calculate total fig height
|
|
116
|
+
fig_height = height["r"] + (n_aux_sp * height["a"]) + (n_signal_sp * height["s"]) + (n_matrix_sp * height["m"])
|
|
117
|
+
|
|
118
|
+
# Create figure and axs
|
|
119
|
+
fig, axs = plt.subplots(n_sp, 2, figsize=(16, fig_height), height_ratios=height_ratios, width_ratios=[1, cbar_width])
|
|
120
|
+
|
|
121
|
+
for i, char in enumerate(arrangement): # For each of the subplots, we modify the layout accordingly
|
|
122
|
+
if char == "r":
|
|
123
|
+
axs[i, 0].axis("off")
|
|
124
|
+
axs[i, 1].axis("off")
|
|
125
|
+
elif char == "a": # Remove ticks and labels from all the aux subplots
|
|
126
|
+
axs[i, 0].tick_params(left=False, bottom=False, labelleft=False, labelbottom=False)
|
|
127
|
+
axs[i, 0].grid(True, linestyle=':', linewidth=0.7, color='gray', alpha=0.7)
|
|
128
|
+
axs[i, 1].axis("off")
|
|
129
|
+
elif char == "s":
|
|
130
|
+
axs[i, 0].tick_params(bottom=False, labelbottom=False)
|
|
131
|
+
axs[i, 0].grid(True, linestyle=':', linewidth=0.7, color='gray', alpha=0.7)
|
|
132
|
+
axs[i, 1].axis("off")
|
|
133
|
+
elif char == "m":
|
|
134
|
+
axs[i, 0].grid(True, linestyle=':', linewidth=0.7, color='gray', alpha=0.7)
|
|
135
|
+
axs[i, 0].tick_params(bottom=False, labelbottom=False)
|
|
136
|
+
|
|
137
|
+
axs[i, 0].sharex(axs[0, 0])
|
|
138
|
+
|
|
139
|
+
axs[-1, 0].tick_params(bottom=True, labelbottom=True)
|
|
140
|
+
|
|
141
|
+
# xlim should be applied on reference subplot, rest all subplots will automatically adjust
|
|
142
|
+
if xlim is not None:
|
|
143
|
+
axs[0, 0].set_xlim(xlim)
|
|
144
|
+
|
|
145
|
+
fig.subplots_adjust(hspace=0.2, wspace=0.05)
|
|
146
|
+
|
|
147
|
+
return fig, axs
|
|
148
|
+
|
|
149
|
+
def add_signal(self, y, x=None, c=None, ls=None, lw=None, m=None, ms=3, label=None, ylabel=None, ylim=None, ax=None):
|
|
150
|
+
"""
|
|
151
|
+
Add signal to the figure.
|
|
152
|
+
|
|
153
|
+
Parameters
|
|
154
|
+
----------
|
|
155
|
+
y: np.ndarray
|
|
156
|
+
- Signal y values.
|
|
157
|
+
x: np.ndarray | None
|
|
158
|
+
- Signal x values.
|
|
159
|
+
- Default: None (indices will be used)
|
|
160
|
+
c: str
|
|
161
|
+
- Color of the line.
|
|
162
|
+
- Default: None
|
|
163
|
+
ls: str
|
|
164
|
+
- Linestyle
|
|
165
|
+
- Default: None
|
|
166
|
+
lw: Number
|
|
167
|
+
- Linewidth
|
|
168
|
+
- Default: None
|
|
169
|
+
m: str
|
|
170
|
+
- Marker
|
|
171
|
+
- Default: None
|
|
172
|
+
ms: number
|
|
173
|
+
- Markersize
|
|
174
|
+
- Default: 3
|
|
175
|
+
label: str
|
|
176
|
+
- Label for the plot.
|
|
177
|
+
- Legend will use this.
|
|
178
|
+
- Default: None
|
|
179
|
+
ylabel: str
|
|
180
|
+
- y-label for the plot.
|
|
181
|
+
- Default: None
|
|
182
|
+
ylim: tuple
|
|
183
|
+
- y-lim for the plot.
|
|
184
|
+
- Default: None
|
|
185
|
+
ax: int
|
|
186
|
+
- Which specific axis to plot (1, 2, 3, ...)
|
|
187
|
+
- None
|
|
188
|
+
|
|
189
|
+
Returns
|
|
190
|
+
-------
|
|
191
|
+
None
|
|
192
|
+
"""
|
|
193
|
+
|
|
194
|
+
curr_row = self._get_curr_row() if ax is None else self._axs[ax]
|
|
195
|
+
|
|
196
|
+
if x is None: x = np.arange(y.size)
|
|
197
|
+
|
|
198
|
+
if c is None: c = self._get_new_color()
|
|
199
|
+
|
|
200
|
+
curr_row[0].plot(x, y, color=c, linestyle=ls, linewidth=lw, marker=m, markersize=ms, label=label)
|
|
201
|
+
|
|
202
|
+
if ylabel is not None: curr_row[0].set_ylabel(ylabel)
|
|
203
|
+
|
|
204
|
+
if ylim is not None: curr_row[0].set_ylim(ylim)
|
|
205
|
+
|
|
206
|
+
def add_matrix(self, M, y=None, x=None, c="gray_r", o="lower", label=None, ylabel=None, ylim=None, cbar=True, ax=None):
|
|
207
|
+
"""
|
|
208
|
+
Add matrix to the figure.
|
|
209
|
+
|
|
210
|
+
Parameters
|
|
211
|
+
----------
|
|
212
|
+
M: np.ndarray
|
|
213
|
+
- Matrix (2D) array
|
|
214
|
+
y: np.ndarray | None
|
|
215
|
+
- y axis values.
|
|
216
|
+
x: np.ndarray | None (indices will be used)
|
|
217
|
+
- x axis values.
|
|
218
|
+
- Default: None (indices will be used)
|
|
219
|
+
c: str
|
|
220
|
+
- cmap for the matrix.
|
|
221
|
+
- Default: None
|
|
222
|
+
o: str
|
|
223
|
+
- origin
|
|
224
|
+
- Default: "lower"
|
|
225
|
+
label: str
|
|
226
|
+
- Label for the plot.
|
|
227
|
+
- Legend will use this.
|
|
228
|
+
- Default: None
|
|
229
|
+
ylabel: str
|
|
230
|
+
- y-label for the plot.
|
|
231
|
+
- Default: None
|
|
232
|
+
ylim: tuple
|
|
233
|
+
- y-lim for the plot.
|
|
234
|
+
- Default: None
|
|
235
|
+
cbar: bool
|
|
236
|
+
- Show colorbar
|
|
237
|
+
- Default: True
|
|
238
|
+
ax: int
|
|
239
|
+
- Which specific axis to plot (1, 2, 3, ...)
|
|
240
|
+
- None
|
|
241
|
+
|
|
242
|
+
Returns
|
|
243
|
+
-------
|
|
244
|
+
None
|
|
245
|
+
"""
|
|
246
|
+
if x is None: x = np.arange(M.shape[1])
|
|
247
|
+
if y is None: y = np.arange(M.shape[0])
|
|
248
|
+
|
|
249
|
+
curr_row = self._get_curr_row() if ax is None else self._axs[ax]
|
|
250
|
+
|
|
251
|
+
extent = self._calculate_extent(x, y)
|
|
252
|
+
im = curr_row[0].imshow(M, aspect="auto", origin=o, cmap=c, extent=extent)
|
|
253
|
+
|
|
254
|
+
if ylabel is not None: curr_row[0].set_ylabel(ylabel)
|
|
255
|
+
|
|
256
|
+
if ylim is not None: curr_row[0].set_ylim(ylim)
|
|
257
|
+
|
|
258
|
+
if cbar is True:
|
|
259
|
+
cbar = plt.colorbar(im, cax=curr_row[1])
|
|
260
|
+
if label is not None:
|
|
261
|
+
cbar.set_label(label, labelpad=5)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def add_events(self, events, c=None, ls=None, lw=None, label=None, ax=None):
|
|
265
|
+
"""
|
|
266
|
+
Add events to the figure.
|
|
267
|
+
|
|
268
|
+
Parameters
|
|
269
|
+
----------
|
|
270
|
+
events: np.ndarray
|
|
271
|
+
- All the event marker values.
|
|
272
|
+
c: str
|
|
273
|
+
- Color of the event marker.
|
|
274
|
+
- Default: "k"
|
|
275
|
+
ls: str
|
|
276
|
+
- Line style.
|
|
277
|
+
- Default: "-"
|
|
278
|
+
lw: float
|
|
279
|
+
- Linewidth.
|
|
280
|
+
- Default: 1.5
|
|
281
|
+
label: str
|
|
282
|
+
- Label for the event type.
|
|
283
|
+
- This will appear in the legend.
|
|
284
|
+
- Default: None
|
|
285
|
+
ax: int
|
|
286
|
+
- Which specific axis to plot (1, 2, 3, ...)
|
|
287
|
+
- None
|
|
288
|
+
|
|
289
|
+
Returns
|
|
290
|
+
-------
|
|
291
|
+
None
|
|
292
|
+
"""
|
|
293
|
+
|
|
294
|
+
curr_row = self._get_curr_row() if ax is None else self._axs[ax]
|
|
295
|
+
|
|
296
|
+
if c is None: c = self._get_new_color()
|
|
297
|
+
|
|
298
|
+
xlim = self._xlim
|
|
299
|
+
|
|
300
|
+
for i, event in enumerate(events):
|
|
301
|
+
if xlim is not None:
|
|
302
|
+
if xlim[0] <= event <= xlim[1]:
|
|
303
|
+
if i == 0: # Label should be set only once for all the events
|
|
304
|
+
curr_row[0].axvline(x=event, color=c, linestyle=ls, linewidth=lw, label=label)
|
|
305
|
+
else:
|
|
306
|
+
curr_row[0].axvline(x=event, color=c, linestyle=ls, linewidth=lw)
|
|
307
|
+
else:
|
|
308
|
+
if i == 0: # Label should be set only once for all the events
|
|
309
|
+
curr_row[0].axvline(x=event, color=c, linestyle=ls, linewidth=lw, label=label)
|
|
310
|
+
else:
|
|
311
|
+
curr_row[0].axvline(x=event, color=c, linestyle=ls, linewidth=lw)
|
|
312
|
+
|
|
313
|
+
def add_annotation(self, ann, label=None, ax=None):
|
|
314
|
+
"""
|
|
315
|
+
Add annotation to the figure.
|
|
316
|
+
|
|
317
|
+
Parameters
|
|
318
|
+
----------
|
|
319
|
+
ann : list[tuple[Number, Number, str]] | None
|
|
320
|
+
- A list of annotation spans. Each tuple should be (start, end, label).
|
|
321
|
+
- Default: None (no annotations).
|
|
322
|
+
label: str
|
|
323
|
+
- Label for the annotation type.
|
|
324
|
+
- This will appear to the right of the aux plot.
|
|
325
|
+
- Default: None
|
|
326
|
+
ax: int
|
|
327
|
+
- Which specific axis to plot (1, 2, 3, ...)
|
|
328
|
+
- None
|
|
329
|
+
Returns
|
|
330
|
+
-------
|
|
331
|
+
None
|
|
332
|
+
"""
|
|
333
|
+
curr_row = self._get_curr_row() if ax is None else self._axs[ax]
|
|
334
|
+
|
|
335
|
+
xlim = self._xlim
|
|
336
|
+
|
|
337
|
+
for i, (start, end, tag) in enumerate(ann):
|
|
338
|
+
# We make sure that we only plot annotation that are within the x range of the current view
|
|
339
|
+
if xlim is not None:
|
|
340
|
+
if start >= xlim[1] or end <= xlim[0]:
|
|
341
|
+
continue
|
|
342
|
+
|
|
343
|
+
# Clip boundaries to xlim
|
|
344
|
+
start = max(start, xlim[0])
|
|
345
|
+
end = min(end, xlim[1])
|
|
346
|
+
|
|
347
|
+
box_colors = ["gray", "lightgray"] # Alternates color between two
|
|
348
|
+
box_color = box_colors[i % 2]
|
|
349
|
+
|
|
350
|
+
width = end - start
|
|
351
|
+
rect = Rectangle((start, 0), width, 1, facecolor=box_color, edgecolor="black", alpha=0.7)
|
|
352
|
+
curr_row[0].add_patch(rect)
|
|
353
|
+
|
|
354
|
+
text_obj = curr_row[0].text(
|
|
355
|
+
(start + end) / 2, 0.5, tag,
|
|
356
|
+
ha='center', va='center',
|
|
357
|
+
fontsize=10, color="black", fontweight='bold', zorder=10, clip_on=True
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
text_obj.set_clip_path(rect)
|
|
361
|
+
else:
|
|
362
|
+
box_colors = ["gray", "lightgray"] # Alternates color between two
|
|
363
|
+
box_color = box_colors[i % 2]
|
|
364
|
+
|
|
365
|
+
width = end - start
|
|
366
|
+
rect = Rectangle((start, 0), width, 1, facecolor=box_color, edgecolor="black", alpha=0.7)
|
|
367
|
+
curr_row[0].add_patch(rect)
|
|
368
|
+
|
|
369
|
+
text_obj = curr_row[0].text(
|
|
370
|
+
(start + end) / 2, 0.5, tag,
|
|
371
|
+
ha='center', va='center',
|
|
372
|
+
fontsize=10, color="black", fontweight='bold', zorder=10, clip_on=True
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
text_obj.set_clip_path(rect)
|
|
376
|
+
|
|
377
|
+
if label is not None:
|
|
378
|
+
curr_row[0].set_ylabel(label, rotation=0, ha="center", va="center")
|
|
379
|
+
curr_row[0].yaxis.set_label_position("right")
|
|
380
|
+
curr_row[0].yaxis.set_label_coords(1.05, 0.75)
|
|
381
|
+
|
|
382
|
+
def add_legend(self, ypos=1.0):
|
|
383
|
+
"""
|
|
384
|
+
Add legend to the figure.
|
|
385
|
+
|
|
386
|
+
Parameters
|
|
387
|
+
----------
|
|
388
|
+
ypos: float
|
|
389
|
+
- y position from the top.
|
|
390
|
+
- > 1 to push it higher, < 1 to push it lower
|
|
391
|
+
- Default: 1.3
|
|
392
|
+
|
|
393
|
+
Returns
|
|
394
|
+
-------
|
|
395
|
+
None
|
|
396
|
+
"""
|
|
397
|
+
axs = self._axs
|
|
398
|
+
fig = self._fig
|
|
399
|
+
|
|
400
|
+
all_handles, all_labels = [], []
|
|
401
|
+
|
|
402
|
+
for ax in axs:
|
|
403
|
+
handles, labels = ax[0].get_legend_handles_labels()
|
|
404
|
+
all_handles.extend(handles)
|
|
405
|
+
all_labels.extend(labels)
|
|
406
|
+
|
|
407
|
+
# remove duplicates if needed
|
|
408
|
+
fig.legend(all_handles, all_labels, loc='upper right', bbox_to_anchor=(0.95, ypos), ncol=3, frameon=True, bbox_transform=fig.transFigure)
|
|
409
|
+
|
|
410
|
+
def add_title(self, title=None, s=13):
|
|
411
|
+
"""
|
|
412
|
+
Add title to the figure.
|
|
413
|
+
|
|
414
|
+
Parameters
|
|
415
|
+
----------
|
|
416
|
+
title: str | None
|
|
417
|
+
- Title of the figure.
|
|
418
|
+
- Default: None
|
|
419
|
+
s: Number
|
|
420
|
+
- Font size.
|
|
421
|
+
- Default: None
|
|
422
|
+
"""
|
|
423
|
+
axs = self._axs
|
|
424
|
+
ref_ax = axs[0, 0] # Title is added to the top subplot (ref subplot)
|
|
425
|
+
|
|
426
|
+
if title is not None:
|
|
427
|
+
ref_ax.set_title(title, pad=10, size=s)
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def add_xlabel(self, xlabel=None, s=None):
|
|
431
|
+
"""
|
|
432
|
+
Add shared x-label to the figure.
|
|
433
|
+
|
|
434
|
+
Parameters
|
|
435
|
+
----------
|
|
436
|
+
xlabel: str | None
|
|
437
|
+
- xlabel for the figure.
|
|
438
|
+
- Default: None
|
|
439
|
+
s: Number
|
|
440
|
+
- Font size.
|
|
441
|
+
- Default: None
|
|
442
|
+
"""
|
|
443
|
+
axs = self._axs
|
|
444
|
+
ref_ax = axs[-1, 0] # X-label is added to the last subplot
|
|
445
|
+
if xlabel is not None:
|
|
446
|
+
ref_ax.set_xlabel(xlabel, size=s)
|
|
447
|
+
|
|
448
|
+
def add_xticks():
|
|
449
|
+
raise NotImplementedError("Please raise a github issue `https://github.com/meluron-toolbox/modusa/issues`")
|
|
450
|
+
|
|
451
|
+
def save(self, path="./figure.png"):
|
|
452
|
+
"""
|
|
453
|
+
Save the figure.
|
|
454
|
+
|
|
455
|
+
Parameters
|
|
456
|
+
----------
|
|
457
|
+
path: str
|
|
458
|
+
- Path to the output file.
|
|
459
|
+
|
|
460
|
+
Returns
|
|
461
|
+
-------
|
|
462
|
+
None
|
|
463
|
+
"""
|
|
464
|
+
fig = self._fig
|
|
465
|
+
fig.savefig(path, bbox_inches="tight")
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
#======== Plot distribution ===========
|
|
470
|
+
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):
|
|
471
|
+
"""
|
|
472
|
+
Plot distribution.
|
|
473
|
+
|
|
474
|
+
.. code-block:: python
|
|
475
|
+
|
|
476
|
+
import modusa as ms
|
|
477
|
+
import numpy as np
|
|
478
|
+
np.random.seed(42)
|
|
479
|
+
data = np.random.normal(loc=1, scale=1, size=1000)
|
|
480
|
+
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")
|
|
481
|
+
|
|
482
|
+
Parameters
|
|
483
|
+
----------
|
|
484
|
+
*args: ndarray
|
|
485
|
+
- Data arrays for which distribution needs to be plotted.
|
|
486
|
+
- Arrays will be flattened.
|
|
487
|
+
ann : list[tuple[Number, Number, str] | None
|
|
488
|
+
- A list of annotations to mark specific points. Each tuple should be of the form (start, end, label).
|
|
489
|
+
- Default: None => No annotation.
|
|
490
|
+
events : list[Number] | None
|
|
491
|
+
- A list of x-values where vertical lines (event markers) will be drawn.
|
|
492
|
+
- Default: None
|
|
493
|
+
xlim : tuple[Number, Number] | None
|
|
494
|
+
- Limits for the x-axis as (xmin, xmax).
|
|
495
|
+
- Default: None
|
|
496
|
+
ylim : tuple[Number, Number] | None
|
|
497
|
+
- Limits for the y-axis as (ymin, ymax).
|
|
498
|
+
- Default: None
|
|
499
|
+
xlabel : str | None
|
|
500
|
+
- Label for the x-axis.
|
|
501
|
+
- - Default: None
|
|
502
|
+
ylabel : str | None
|
|
503
|
+
- Label for the y-axis.
|
|
504
|
+
- Default: None
|
|
505
|
+
title : str | None
|
|
506
|
+
- Title of the plot.
|
|
507
|
+
- Default: None
|
|
508
|
+
legend : list[str] | None
|
|
509
|
+
- List of legend labels corresponding to each signal if plotting multiple distributions.
|
|
510
|
+
- Default: None
|
|
511
|
+
show_hist: bool
|
|
512
|
+
- Want to show histogram as well.
|
|
513
|
+
npoints: int
|
|
514
|
+
- Number of points for which gaussian needs to be computed between min and max.
|
|
515
|
+
- Higher value means more points are evaluated with the fitted gaussian, thereby higher resolution.
|
|
516
|
+
bins: int
|
|
517
|
+
- The number of bins for histogram.
|
|
518
|
+
- This is used only to plot the histogram.
|
|
519
|
+
|
|
520
|
+
Returns
|
|
521
|
+
-------
|
|
522
|
+
plt.Figure
|
|
523
|
+
- Matplotlib figure.
|
|
524
|
+
"""
|
|
525
|
+
from scipy.stats import gaussian_kde
|
|
526
|
+
|
|
527
|
+
if isinstance(legend, str):
|
|
528
|
+
legend = (legend, )
|
|
529
|
+
|
|
530
|
+
if legend is not None:
|
|
531
|
+
if len(legend) < len(args):
|
|
532
|
+
raise ValueError(f"Legend should be provided for each signal.")
|
|
533
|
+
|
|
534
|
+
# Create figure
|
|
535
|
+
fig = plt.figure(figsize=(16, 4))
|
|
536
|
+
gs = gridspec.GridSpec(2, 1, height_ratios=[0.1, 1])
|
|
537
|
+
|
|
538
|
+
colors = plt.get_cmap('tab10').colors
|
|
539
|
+
|
|
540
|
+
dist_ax = fig.add_subplot(gs[1, 0])
|
|
541
|
+
annotation_ax = fig.add_subplot(gs[0, 0], sharex=dist_ax)
|
|
542
|
+
|
|
543
|
+
# Set limits
|
|
544
|
+
if xlim is not None:
|
|
545
|
+
dist_ax.set_xlim(xlim)
|
|
546
|
+
|
|
547
|
+
if ylim is not None:
|
|
548
|
+
dist_ax.set_ylim(ylim)
|
|
549
|
+
|
|
550
|
+
# Add plot
|
|
551
|
+
for i, data in enumerate(args):
|
|
552
|
+
# Fit gaussian to the data
|
|
553
|
+
kde = gaussian_kde(data)
|
|
554
|
+
|
|
555
|
+
# Create points to evaluate KDE
|
|
556
|
+
x = np.linspace(np.min(data), np.max(data), npoints)
|
|
557
|
+
y = kde(x)
|
|
558
|
+
|
|
559
|
+
if legend is not None:
|
|
560
|
+
dist_ax.plot(x, y, color=colors[i], label=legend[i])
|
|
561
|
+
if show_hist is True:
|
|
562
|
+
dist_ax.hist(data, bins=bins, density=True, alpha=0.3, facecolor=colors[i], edgecolor='black', label=legend[i])
|
|
563
|
+
else:
|
|
564
|
+
dist_ax.plot(x, y, color=colors[i])
|
|
565
|
+
if show_hist is True:
|
|
566
|
+
dist_ax.hist(data, bins=bins, density=True, alpha=0.3, facecolor=colors[i], edgecolor='black')
|
|
567
|
+
|
|
568
|
+
# Add annotations
|
|
569
|
+
if ann is not None:
|
|
570
|
+
annotation_ax.set_ylim(0, 1) # For consistent layout
|
|
571
|
+
# Determine visible x-range
|
|
572
|
+
x_view_min = xlim[0] if xlim is not None else np.min(x)
|
|
573
|
+
x_view_max = xlim[1] if xlim is not None else np.max(x)
|
|
574
|
+
for i, (start, end, tag) in enumerate(ann):
|
|
575
|
+
# We make sure that we only plot annotation that are within the x range of the current view
|
|
576
|
+
if start >= x_view_max or end <= x_view_min:
|
|
577
|
+
continue
|
|
578
|
+
|
|
579
|
+
# Clip boundaries to xlim
|
|
580
|
+
start = max(start, x_view_min)
|
|
581
|
+
end = min(end, x_view_max)
|
|
582
|
+
|
|
583
|
+
color = colors[i % len(colors)]
|
|
584
|
+
width = end - start
|
|
585
|
+
rect = Rectangle((start, 0), width, 1, color=color, alpha=0.7)
|
|
586
|
+
annotation_ax.add_patch(rect)
|
|
587
|
+
|
|
588
|
+
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)
|
|
589
|
+
text_obj.set_clip_path(rect)
|
|
590
|
+
|
|
591
|
+
# Add legend
|
|
592
|
+
if legend is not None:
|
|
593
|
+
handles, labels = dist_ax.get_legend_handles_labels()
|
|
594
|
+
fig.legend(handles, labels, loc='upper right', bbox_to_anchor=(0.9, 1.1), ncol=len(legend), frameon=True)
|
|
595
|
+
|
|
596
|
+
# Set title, labels
|
|
597
|
+
if title is not None:
|
|
598
|
+
annotation_ax.set_title(title, pad=10, size=11)
|
|
599
|
+
if xlabel is not None:
|
|
600
|
+
dist_ax.set_xlabel(xlabel)
|
|
601
|
+
if ylabel is not None:
|
|
602
|
+
dist_ax.set_ylabel(ylabel)
|
|
603
|
+
|
|
604
|
+
# Remove the boundaries and ticks from annotation axis
|
|
605
|
+
if ann is not None:
|
|
606
|
+
annotation_ax.tick_params(left=False, bottom=False, labelleft=False, labelbottom=False)
|
|
607
|
+
else:
|
|
608
|
+
annotation_ax.axis("off")
|
|
609
|
+
|
|
610
|
+
fig.subplots_adjust(hspace=0.01, wspace=0.05)
|
|
611
|
+
plt.close()
|
|
612
|
+
return fig
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
|