modusa 0.3.52__py3-none-any.whl → 0.3.55__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/__init__.py +1 -1
- modusa/tools/__init__.py +0 -2
- modusa/tools/plotter.py +176 -896
- {modusa-0.3.52.dist-info → modusa-0.3.55.dist-info}/METADATA +1 -1
- {modusa-0.3.52.dist-info → modusa-0.3.55.dist-info}/RECORD +8 -8
- {modusa-0.3.52.dist-info → modusa-0.3.55.dist-info}/WHEEL +0 -0
- {modusa-0.3.52.dist-info → modusa-0.3.55.dist-info}/entry_points.txt +0 -0
- {modusa-0.3.52.dist-info → modusa-0.3.55.dist-info}/licenses/LICENSE.md +0 -0
modusa/__init__.py
CHANGED
|
@@ -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
|
modusa/tools/__init__.py
CHANGED
modusa/tools/plotter.py
CHANGED
|
@@ -36,887 +36,15 @@ def _load_devanagari_font():
|
|
|
36
36
|
_load_devanagari_font()
|
|
37
37
|
#==============
|
|
38
38
|
|
|
39
|
-
#--------------------------
|
|
40
|
-
# Figuure 1D
|
|
41
|
-
#--------------------------
|
|
42
|
-
class Figure1D:
|
|
43
|
-
"""
|
|
44
|
-
A utility class that provides easy-to-use API
|
|
45
|
-
for plotting 1D signals along with clean
|
|
46
|
-
representations of annotations, events.
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
Parameters
|
|
51
|
-
----------
|
|
52
|
-
n_aux_subplots: int
|
|
53
|
-
- Total number of auxiliary subplots
|
|
54
|
-
- These include annotations and events subplots.
|
|
55
|
-
- Default: 0
|
|
56
|
-
title: str
|
|
57
|
-
- Title of the figure.
|
|
58
|
-
- Default: Title
|
|
59
|
-
xlim: tuple[number, number]
|
|
60
|
-
- xlim for the figure.
|
|
61
|
-
- All subplots will be automatically adjusted.
|
|
62
|
-
- Default: None
|
|
63
|
-
ylim: tuple[number, number]
|
|
64
|
-
- ylim for the signal.
|
|
65
|
-
- Default: None
|
|
66
|
-
"""
|
|
67
|
-
|
|
68
|
-
def __init__(self, n_aux_subplots=0, xlim=None, ylim=None, sharex=None):
|
|
69
|
-
self._n_aux_subplots: int = n_aux_subplots
|
|
70
|
-
self._active_subplot_idx: int = 1 # Any addition will happen on this subplot (0 is reserved for reference axis)
|
|
71
|
-
if sharex is not None:
|
|
72
|
-
xlim = sharex._xlim
|
|
73
|
-
self._xlim = xlim # Many add functions depend on this, so we fix it while instantiating the class
|
|
74
|
-
self._ylim = ylim
|
|
75
|
-
self._subplots, self._fig = self._generate_subplots() # Will contain all the subplots (list, fig)
|
|
76
|
-
|
|
77
|
-
def _get_active_subplot(self):
|
|
78
|
-
"""
|
|
79
|
-
Get the active subplot where you can add
|
|
80
|
-
either annotations or events.
|
|
81
|
-
"""
|
|
82
|
-
active_subplot = self._subplots[self._active_subplot_idx]
|
|
83
|
-
self._active_subplot_idx += 1
|
|
84
|
-
|
|
85
|
-
return active_subplot
|
|
86
|
-
|
|
87
|
-
def _generate_subplots(self):
|
|
88
|
-
"""
|
|
89
|
-
Generate subplots based on the configuration.
|
|
90
|
-
"""
|
|
91
|
-
|
|
92
|
-
n_aux_subplots = self._n_aux_subplots
|
|
93
|
-
|
|
94
|
-
# Fixed heights per subplot type
|
|
95
|
-
ref_height = 0.0
|
|
96
|
-
aux_height = 0.4
|
|
97
|
-
signal_height = 2.0
|
|
98
|
-
|
|
99
|
-
# Total number of subplots
|
|
100
|
-
n_subplots = 1 + n_aux_subplots + 1
|
|
101
|
-
|
|
102
|
-
# Compute total fig height
|
|
103
|
-
fig_height = ref_height + n_aux_subplots * aux_height + signal_height
|
|
104
|
-
|
|
105
|
-
# Define height ratios
|
|
106
|
-
height_ratios = [ref_height] + [aux_height] * n_aux_subplots + [signal_height]
|
|
107
|
-
|
|
108
|
-
# Create figure and grid
|
|
109
|
-
fig = plt.figure(figsize=(16, fig_height))
|
|
110
|
-
gs = gridspec.GridSpec(n_subplots, 1, height_ratios=height_ratios)
|
|
111
|
-
|
|
112
|
-
# Add subplots
|
|
113
|
-
subplots_list = []
|
|
114
|
-
|
|
115
|
-
ref_subplot = fig.add_subplot(gs[0, 0])
|
|
116
|
-
ref_subplot.axis("off")
|
|
117
|
-
subplots_list.append(ref_subplot)
|
|
118
|
-
|
|
119
|
-
for i in range(1, n_subplots):
|
|
120
|
-
subplots_list.append(fig.add_subplot(gs[i, 0], sharex=ref_subplot))
|
|
121
|
-
|
|
122
|
-
for i in range(n_subplots - 1):
|
|
123
|
-
subplots_list[i].tick_params(left=False, bottom=False, labelleft=False, labelbottom=False)
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
# Set xlim
|
|
127
|
-
if self._xlim is not None: # xlim should be applied on reference subplot, rest all sharex
|
|
128
|
-
ref_subplot.set_xlim(self._xlim)
|
|
129
|
-
|
|
130
|
-
# Set ylim
|
|
131
|
-
if self._ylim is not None: # ylim should be applied on the signal subplot
|
|
132
|
-
subplots_list[-1].set_ylim(self._ylim)
|
|
133
|
-
|
|
134
|
-
fig.subplots_adjust(hspace=0.01, wspace=0.05)
|
|
135
|
-
|
|
136
|
-
return subplots_list, fig
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
def add_events(self, events, c="k", ls="-", lw=1.5, label="Event Label"):
|
|
140
|
-
"""
|
|
141
|
-
Add events to the figure.
|
|
142
|
-
|
|
143
|
-
Parameters
|
|
144
|
-
----------
|
|
145
|
-
events: np.ndarray
|
|
146
|
-
- All the event marker values.
|
|
147
|
-
c: str
|
|
148
|
-
- Color of the event marker.
|
|
149
|
-
- Default: "k"
|
|
150
|
-
ls: str
|
|
151
|
-
- Line style.
|
|
152
|
-
- Default: "-"
|
|
153
|
-
lw: float
|
|
154
|
-
- Linewidth.
|
|
155
|
-
- Default: 1.5
|
|
156
|
-
label: str
|
|
157
|
-
- Label for the event type.
|
|
158
|
-
- This will appear in the legend.
|
|
159
|
-
- Default: "Event label"
|
|
160
|
-
|
|
161
|
-
Returns
|
|
162
|
-
-------
|
|
163
|
-
None
|
|
164
|
-
"""
|
|
165
|
-
event_subplot = self._get_active_subplot()
|
|
166
|
-
xlim = self._xlim
|
|
167
|
-
|
|
168
|
-
for i, event in enumerate(events):
|
|
169
|
-
if xlim is not None:
|
|
170
|
-
if xlim[0] <= event <= xlim[1]:
|
|
171
|
-
if i == 0: # Label should be set only once for all the events
|
|
172
|
-
event_subplot.axvline(x=event, color=c, linestyle=ls, linewidth=lw, label=label)
|
|
173
|
-
else:
|
|
174
|
-
event_subplot.axvline(x=event, color=c, linestyle=ls, linewidth=lw)
|
|
175
|
-
else:
|
|
176
|
-
if i == 0: # Label should be set only once for all the events
|
|
177
|
-
event_subplot.axvline(x=event, color=c, linestyle=ls, linewidth=lw, label=label)
|
|
178
|
-
else:
|
|
179
|
-
event_subplot.axvline(x=event, color=c, linestyle=ls, linewidth=lw)
|
|
180
|
-
|
|
181
|
-
def add_annotation(self, ann):
|
|
182
|
-
"""
|
|
183
|
-
Add annotation to the figure.
|
|
184
|
-
|
|
185
|
-
Parameters
|
|
186
|
-
----------
|
|
187
|
-
ann : list[tuple[Number, Number, str]] | None
|
|
188
|
-
- A list of annotation spans. Each tuple should be (start, end, label).
|
|
189
|
-
- Default: None (no annotations).
|
|
190
|
-
|
|
191
|
-
Returns
|
|
192
|
-
-------
|
|
193
|
-
None
|
|
194
|
-
"""
|
|
195
|
-
|
|
196
|
-
ann_subplot = self._get_active_subplot()
|
|
197
|
-
xlim = self._xlim
|
|
198
|
-
|
|
199
|
-
for i, (start, end, tag) in enumerate(ann):
|
|
200
|
-
|
|
201
|
-
# We make sure that we only plot annotation that are within the x range of the current view
|
|
202
|
-
if xlim is not None:
|
|
203
|
-
if start >= xlim[1] or end <= xlim[0]:
|
|
204
|
-
continue
|
|
205
|
-
|
|
206
|
-
# Clip boundaries to xlim
|
|
207
|
-
start = max(start, xlim[0])
|
|
208
|
-
end = min(end, xlim[1])
|
|
209
|
-
|
|
210
|
-
box_colors = ["gray", "lightgray"] # Alternates color between two
|
|
211
|
-
box_color = box_colors[i % 2]
|
|
212
|
-
|
|
213
|
-
width = end - start
|
|
214
|
-
rect = Rectangle((start, 0), width, 1, facecolor=box_color, edgecolor="black", alpha=0.7)
|
|
215
|
-
ann_subplot.add_patch(rect)
|
|
216
|
-
|
|
217
|
-
text_obj = ann_subplot.text(
|
|
218
|
-
(start + end) / 2, 0.5, tag,
|
|
219
|
-
ha='center', va='center',
|
|
220
|
-
fontsize=10, color="black", fontweight='bold', zorder=10, clip_on=True
|
|
221
|
-
)
|
|
222
|
-
|
|
223
|
-
text_obj.set_clip_path(rect)
|
|
224
|
-
else:
|
|
225
|
-
box_colors = ["gray", "lightgray"] # Alternates color between two
|
|
226
|
-
box_color = box_colors[i % 2]
|
|
227
|
-
|
|
228
|
-
width = end - start
|
|
229
|
-
rect = Rectangle((start, 0), width, 1, facecolor=box_color, edgecolor="black", alpha=0.7)
|
|
230
|
-
ann_subplot.add_patch(rect)
|
|
231
|
-
|
|
232
|
-
text_obj = ann_subplot.text(
|
|
233
|
-
(start + end) / 2, 0.5, tag,
|
|
234
|
-
ha='center', va='center',
|
|
235
|
-
fontsize=10, color="black", fontweight='bold', zorder=10, clip_on=True
|
|
236
|
-
)
|
|
237
|
-
|
|
238
|
-
text_obj.set_clip_path(rect)
|
|
239
|
-
|
|
240
|
-
def add_signal(self, y, x=None, c=None, ls="-", lw=1.5, m=None, ms=3, label="Signal"):
|
|
241
|
-
"""
|
|
242
|
-
Add signal to the figure.
|
|
243
|
-
|
|
244
|
-
Parameters
|
|
245
|
-
----------
|
|
246
|
-
y: np.ndarray
|
|
247
|
-
- Signal y values.
|
|
248
|
-
x: np.ndarray | None
|
|
249
|
-
- Signal x values.
|
|
250
|
-
- Default: None (indices will be used)
|
|
251
|
-
c: str
|
|
252
|
-
- Color of the line.
|
|
253
|
-
- Default: None
|
|
254
|
-
ls: str
|
|
255
|
-
- Linestyle
|
|
256
|
-
- Default: "-"
|
|
257
|
-
lw: Number
|
|
258
|
-
- Linewidth
|
|
259
|
-
- Default: 1
|
|
260
|
-
m: str
|
|
261
|
-
- Marker
|
|
262
|
-
- Default: None
|
|
263
|
-
ms: number
|
|
264
|
-
- Markersize
|
|
265
|
-
- Default: 5
|
|
266
|
-
label: str
|
|
267
|
-
- Label for the plot.
|
|
268
|
-
- Legend will use this.
|
|
269
|
-
- Default: "Signal"
|
|
270
|
-
|
|
271
|
-
Returns
|
|
272
|
-
-------
|
|
273
|
-
None
|
|
274
|
-
"""
|
|
275
|
-
if x is None:
|
|
276
|
-
x = np.arange(y.size)
|
|
277
|
-
signal_subplot = self._subplots[-1]
|
|
278
|
-
signal_subplot.plot(x, y, color=c, linestyle=ls, linewidth=lw, marker=m, markersize=ms, label=label)
|
|
279
|
-
|
|
280
|
-
def add_legend(self, ypos=1.3):
|
|
281
|
-
"""
|
|
282
|
-
Add legend to the figure.
|
|
283
|
-
|
|
284
|
-
Parameters
|
|
285
|
-
----------
|
|
286
|
-
ypos: float
|
|
287
|
-
- y position from the top.
|
|
288
|
-
- > 1 to push it higher, < 1 to push it lower
|
|
289
|
-
- Default: 1.3
|
|
290
|
-
|
|
291
|
-
Returns
|
|
292
|
-
-------
|
|
293
|
-
None
|
|
294
|
-
"""
|
|
295
|
-
subplots: list = self._subplots
|
|
296
|
-
fig = self._fig
|
|
297
|
-
|
|
298
|
-
all_handles, all_labels = [], []
|
|
299
|
-
|
|
300
|
-
for subplot in subplots:
|
|
301
|
-
handles, labels = subplot.get_legend_handles_labels()
|
|
302
|
-
all_handles.extend(handles)
|
|
303
|
-
all_labels.extend(labels)
|
|
304
|
-
|
|
305
|
-
# remove duplicates if needed
|
|
306
|
-
fig.legend(all_handles, all_labels, loc='upper right', bbox_to_anchor=(0.9, ypos), ncol=2, frameon=True, bbox_transform=fig.transFigure)
|
|
307
|
-
|
|
308
|
-
def add_meta_info(self, title=None, ylabel=None, xlabel=None, ts=13, ls=11):
|
|
309
|
-
"""
|
|
310
|
-
Add meta info to the figure.
|
|
311
|
-
|
|
312
|
-
Parameters
|
|
313
|
-
----------
|
|
314
|
-
title: str
|
|
315
|
-
- Title of the figure.
|
|
316
|
-
- Default: None
|
|
317
|
-
ylabel: str
|
|
318
|
-
- y label of the signal.
|
|
319
|
-
- It will only appear in the signal subplot.
|
|
320
|
-
- Default: None
|
|
321
|
-
xlabel: str
|
|
322
|
-
- x label of the signal.
|
|
323
|
-
- It will only appear in the signal subplot.
|
|
324
|
-
- Default: None
|
|
325
|
-
ts: Number
|
|
326
|
-
- Title size
|
|
327
|
-
- Default: 10
|
|
328
|
-
ls: Number
|
|
329
|
-
- Label size.
|
|
330
|
-
- Default: 10
|
|
331
|
-
Returns
|
|
332
|
-
-------
|
|
333
|
-
None
|
|
334
|
-
"""
|
|
335
|
-
subplots: list = self._subplots
|
|
336
|
-
fig = self._fig
|
|
337
|
-
|
|
338
|
-
ref_subplot = subplots[0]
|
|
339
|
-
signal_subplot = subplots[-1]
|
|
340
|
-
|
|
341
|
-
if title is not None:
|
|
342
|
-
ref_subplot.set_title(title, pad=10, size=ts)
|
|
343
|
-
if ylabel is not None:
|
|
344
|
-
signal_subplot.set_ylabel(ylabel, size=ls)
|
|
345
|
-
if xlabel is not None:
|
|
346
|
-
signal_subplot.set_xlabel(xlabel, size=ls)
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
def save(self, path="./figure.png"):
|
|
350
|
-
"""
|
|
351
|
-
Save the figure.
|
|
352
|
-
|
|
353
|
-
Parameters
|
|
354
|
-
----------
|
|
355
|
-
path: str
|
|
356
|
-
- Path to the output file.
|
|
357
|
-
|
|
358
|
-
Returns
|
|
359
|
-
-------
|
|
360
|
-
None
|
|
361
|
-
"""
|
|
362
|
-
fig = self._fig
|
|
363
|
-
fig.savefig(path, bbox_inches="tight")
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
#--------------------------
|
|
367
|
-
# Figuure 2D
|
|
368
|
-
#--------------------------
|
|
369
|
-
class Figure2D:
|
|
370
|
-
"""
|
|
371
|
-
A utility class that provides easy-to-use API
|
|
372
|
-
for plotting 2D signals along with clean
|
|
373
|
-
representations of annotations, events.
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
Parameters
|
|
378
|
-
----------
|
|
379
|
-
n_aux_subplots: int
|
|
380
|
-
- Total number of auxiliary subplots
|
|
381
|
-
- These include annotations and events subplots.
|
|
382
|
-
- Default: 0
|
|
383
|
-
title: str
|
|
384
|
-
- Title of the figure.
|
|
385
|
-
- Default: Title
|
|
386
|
-
xlim: tuple[number, number]
|
|
387
|
-
- xlim for the figure.
|
|
388
|
-
- All subplots will be automatically adjusted.
|
|
389
|
-
- Default: None
|
|
390
|
-
ylim: tuple[number, number]
|
|
391
|
-
- ylim for the signal.
|
|
392
|
-
- Default: None
|
|
393
|
-
"""
|
|
394
|
-
|
|
395
|
-
def __init__(self, n_aux_subplots=0, xlim=None, ylim=None, sharex=None):
|
|
396
|
-
self._n_aux_subplots: int = n_aux_subplots
|
|
397
|
-
self._active_subplot_idx: int = 1 # Any addition will happen on this subplot (0 is reserved for reference axis)
|
|
398
|
-
if sharex is not None:
|
|
399
|
-
xlim = sharex._xlim
|
|
400
|
-
self._xlim = xlim # Many add functions depend on this, so we fix it while instantiating the class
|
|
401
|
-
self._ylim = ylim
|
|
402
|
-
self._subplots, self._fig = self._generate_subplots() # Will contain all the subplots (list, fig)
|
|
403
|
-
self._im = None # Useful while creating colorbar for the image
|
|
404
|
-
|
|
405
|
-
def _get_active_subplot(self):
|
|
406
|
-
"""
|
|
407
|
-
Get the active subplot where you can add
|
|
408
|
-
either annotations or events.
|
|
409
|
-
"""
|
|
410
|
-
active_subplot = self._subplots[self._active_subplot_idx]
|
|
411
|
-
self._active_subplot_idx += 1
|
|
412
|
-
|
|
413
|
-
return active_subplot
|
|
414
|
-
|
|
415
|
-
def _calculate_extent(self, x, y):
|
|
416
|
-
# Handle spacing safely
|
|
417
|
-
if len(x) > 1:
|
|
418
|
-
dx = x[1] - x[0]
|
|
419
|
-
else:
|
|
420
|
-
dx = 1 # Default spacing for single value
|
|
421
|
-
if len(y) > 1:
|
|
422
|
-
dy = y[1] - y[0]
|
|
423
|
-
else:
|
|
424
|
-
dy = 1 # Default spacing for single value
|
|
425
|
-
|
|
426
|
-
return [x[0] - dx / 2, x[-1] + dx / 2, y[0] - dy / 2, y[-1] + dy / 2]
|
|
427
|
-
|
|
428
|
-
def _add_colorbar(self, im, label=None, width="20%", height="35%"):
|
|
429
|
-
from mpl_toolkits.axes_grid1.inset_locator import inset_axes
|
|
430
|
-
|
|
431
|
-
ref_subplot = self._subplots[0]
|
|
432
|
-
|
|
433
|
-
# Assume ref_subplot is your reference axes, im is the image
|
|
434
|
-
cax = inset_axes(
|
|
435
|
-
ref_subplot,
|
|
436
|
-
width=width, # width of colorbar
|
|
437
|
-
height=height, # height of colorbar
|
|
438
|
-
loc='right',
|
|
439
|
-
bbox_to_anchor=(0, 1, 1, 1), # move 0.9 right, 1.2 up from the subplot
|
|
440
|
-
bbox_transform=ref_subplot.transAxes, # important: use subplot coords
|
|
441
|
-
borderpad=0
|
|
442
|
-
)
|
|
443
|
-
|
|
444
|
-
cbar = plt.colorbar(im, cax=cax, orientation='horizontal')
|
|
445
|
-
cbar.ax.xaxis.set_ticks_position('top')
|
|
446
|
-
cbar.set_label(label, labelpad=5)
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
def _generate_subplots(self):
|
|
451
|
-
"""
|
|
452
|
-
Generate subplots based on the configuration.
|
|
453
|
-
"""
|
|
454
|
-
|
|
455
|
-
n_aux_subplots = self._n_aux_subplots
|
|
456
|
-
|
|
457
|
-
# Fixed heights per subplot type
|
|
458
|
-
ref_height = 0.4
|
|
459
|
-
aux_height = 0.4
|
|
460
|
-
signal_height = 4.0
|
|
461
|
-
|
|
462
|
-
# Total number of subplots
|
|
463
|
-
n_subplots = 1 + n_aux_subplots + 1
|
|
464
|
-
|
|
465
|
-
# Compute total fig height
|
|
466
|
-
fig_height = ref_height + n_aux_subplots * aux_height + signal_height
|
|
467
|
-
|
|
468
|
-
# Define height ratios
|
|
469
|
-
height_ratios = [ref_height] + [aux_height] * n_aux_subplots + [signal_height]
|
|
470
|
-
|
|
471
|
-
# Create figure and grid
|
|
472
|
-
fig = plt.figure(figsize=(16, fig_height))
|
|
473
|
-
gs = gridspec.GridSpec(n_subplots, 1, height_ratios=height_ratios)
|
|
474
|
-
|
|
475
|
-
# Add subplots
|
|
476
|
-
subplots_list = []
|
|
477
|
-
ref_subplot = fig.add_subplot(gs[0, 0])
|
|
478
|
-
ref_subplot.axis("off")
|
|
479
|
-
subplots_list.append(ref_subplot)
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
for i in range(1, n_subplots):
|
|
483
|
-
subplots_list.append(fig.add_subplot(gs[i, 0], sharex=ref_subplot))
|
|
484
|
-
|
|
485
|
-
for i in range(n_subplots - 1):
|
|
486
|
-
subplots_list[i].tick_params(left=False, bottom=False, labelleft=False, labelbottom=False)
|
|
487
|
-
|
|
488
|
-
# Set xlim
|
|
489
|
-
if self._xlim is not None: # xlim should be applied on reference subplot, rest all sharex
|
|
490
|
-
ref_subplot.set_xlim(self._xlim)
|
|
491
|
-
|
|
492
|
-
# Set ylim
|
|
493
|
-
if self._ylim is not None: # ylim should be applied on the signal subplot
|
|
494
|
-
subplots_list[-1].set_ylim(self._ylim)
|
|
495
|
-
|
|
496
|
-
fig.subplots_adjust(hspace=0.01, wspace=0.05)
|
|
497
|
-
|
|
498
|
-
return subplots_list, fig
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
def add_events(self, events, c="k", ls="-", lw=1.5, label="Event Label"):
|
|
502
|
-
"""
|
|
503
|
-
Add events to the figure.
|
|
504
|
-
|
|
505
|
-
Parameters
|
|
506
|
-
----------
|
|
507
|
-
events: np.ndarray
|
|
508
|
-
- All the event marker values.
|
|
509
|
-
c: str
|
|
510
|
-
- Color of the event marker.
|
|
511
|
-
- Default: "k"
|
|
512
|
-
ls: str
|
|
513
|
-
- Line style.
|
|
514
|
-
- Default: "-"
|
|
515
|
-
lw: float
|
|
516
|
-
- Linewidth.
|
|
517
|
-
- Default: 1.5
|
|
518
|
-
label: str
|
|
519
|
-
- Label for the event type.
|
|
520
|
-
- This will appear in the legend.
|
|
521
|
-
- Default: "Event label"
|
|
522
|
-
|
|
523
|
-
Returns
|
|
524
|
-
-------
|
|
525
|
-
None
|
|
526
|
-
"""
|
|
527
|
-
event_subplot = self._get_active_subplot()
|
|
528
|
-
xlim = self._xlim
|
|
529
|
-
|
|
530
|
-
for i, event in enumerate(events):
|
|
531
|
-
if xlim is not None:
|
|
532
|
-
if xlim[0] <= event <= xlim[1]:
|
|
533
|
-
if i == 0: # Label should be set only once for all the events
|
|
534
|
-
event_subplot.axvline(x=event, color=c, linestyle=ls, linewidth=lw, label=label)
|
|
535
|
-
else:
|
|
536
|
-
event_subplot.axvline(x=event, color=c, linestyle=ls, linewidth=lw)
|
|
537
|
-
else:
|
|
538
|
-
if i == 0: # Label should be set only once for all the events
|
|
539
|
-
event_subplot.axvline(x=event, color=c, linestyle=ls, linewidth=lw, label=label)
|
|
540
|
-
else:
|
|
541
|
-
event_subplot.axvline(x=event, color=c, linestyle=ls, linewidth=lw)
|
|
542
|
-
|
|
543
|
-
def add_annotation(self, ann):
|
|
544
|
-
"""
|
|
545
|
-
Add annotation to the figure.
|
|
546
|
-
|
|
547
|
-
Parameters
|
|
548
|
-
----------
|
|
549
|
-
ann : list[tuple[Number, Number, str]] | None
|
|
550
|
-
- A list of annotation spans. Each tuple should be (start, end, label).
|
|
551
|
-
- Default: None (no annotations).
|
|
552
|
-
|
|
553
|
-
Returns
|
|
554
|
-
-------
|
|
555
|
-
None
|
|
556
|
-
"""
|
|
557
|
-
ann_subplot = self._get_active_subplot()
|
|
558
|
-
xlim = self._xlim
|
|
559
|
-
|
|
560
|
-
for i, (start, end, tag) in enumerate(ann):
|
|
561
|
-
|
|
562
|
-
# We make sure that we only plot annotation that are within the x range of the current view
|
|
563
|
-
if xlim is not None:
|
|
564
|
-
if start >= xlim[1] or end <= xlim[0]:
|
|
565
|
-
continue
|
|
566
|
-
|
|
567
|
-
# Clip boundaries to xlim
|
|
568
|
-
start = max(start, xlim[0])
|
|
569
|
-
end = min(end, xlim[1])
|
|
570
|
-
|
|
571
|
-
box_colors = ["gray", "lightgray"] # Alternates color between two
|
|
572
|
-
box_color = box_colors[i % 2]
|
|
573
|
-
|
|
574
|
-
width = end - start
|
|
575
|
-
rect = Rectangle((start, 0), width, 1, facecolor=box_color, edgecolor="black", alpha=0.7)
|
|
576
|
-
ann_subplot.add_patch(rect)
|
|
577
|
-
|
|
578
|
-
text_obj = ann_subplot.text(
|
|
579
|
-
(start + end) / 2, 0.5, tag,
|
|
580
|
-
ha='center', va='center',
|
|
581
|
-
fontsize=10, color="black", fontweight='bold', zorder=10, clip_on=True
|
|
582
|
-
)
|
|
583
|
-
|
|
584
|
-
text_obj.set_clip_path(rect)
|
|
585
|
-
else:
|
|
586
|
-
box_colors = ["gray", "lightgray"] # Alternates color between two
|
|
587
|
-
box_color = box_colors[i % 2]
|
|
588
|
-
|
|
589
|
-
width = end - start
|
|
590
|
-
rect = Rectangle((start, 0), width, 1, facecolor=box_color, edgecolor="black", alpha=0.7)
|
|
591
|
-
ann_subplot.add_patch(rect)
|
|
592
|
-
|
|
593
|
-
text_obj = ann_subplot.text(
|
|
594
|
-
(start + end) / 2, 0.5, tag,
|
|
595
|
-
ha='center', va='center',
|
|
596
|
-
fontsize=10, color="black", fontweight='bold', zorder=10, clip_on=True
|
|
597
|
-
)
|
|
598
|
-
|
|
599
|
-
text_obj.set_clip_path(rect)
|
|
600
|
-
|
|
601
|
-
def add_matrix(self, M, y=None, x=None, c="gray_r", o="lower", label="Matrix"):
|
|
602
|
-
"""
|
|
603
|
-
Add matrix to the figure.
|
|
604
|
-
|
|
605
|
-
Parameters
|
|
606
|
-
----------
|
|
607
|
-
M: np.ndarray
|
|
608
|
-
- Matrix (2D) array
|
|
609
|
-
y: np.ndarray | None
|
|
610
|
-
- y axis values.
|
|
611
|
-
x: np.ndarray | None (indices will be used)
|
|
612
|
-
- x axis values.
|
|
613
|
-
- Default: None (indices will be used)
|
|
614
|
-
c: str
|
|
615
|
-
- cmap for the matrix.
|
|
616
|
-
- Default: None
|
|
617
|
-
o: str
|
|
618
|
-
- origin
|
|
619
|
-
- Default: "lower"
|
|
620
|
-
label: str
|
|
621
|
-
- Label for the plot.
|
|
622
|
-
- Legend will use this.
|
|
623
|
-
- Default: "Signal"
|
|
624
|
-
|
|
625
|
-
Returns
|
|
626
|
-
-------
|
|
627
|
-
None
|
|
628
|
-
"""
|
|
629
|
-
if x is None: x = np.arange(M.shape[1])
|
|
630
|
-
if y is None: y = np.arange(M.shape[0])
|
|
631
|
-
|
|
632
|
-
matrix_subplot = self._subplots[-1]
|
|
633
|
-
extent = self._calculate_extent(x, y)
|
|
634
|
-
im = matrix_subplot.imshow(M, aspect="auto", origin=o, cmap=c, extent=extent)
|
|
635
|
-
|
|
636
|
-
self._add_colorbar(im=im, label=label)
|
|
637
|
-
|
|
638
|
-
def add_signal(self, y, x=None, c=None, ls="-", lw=1.5, m="o", ms=3, label="Signal"):
|
|
639
|
-
"""
|
|
640
|
-
Add signal on the matrix.
|
|
641
|
-
|
|
642
|
-
Parameters
|
|
643
|
-
----------
|
|
644
|
-
y: np.ndarray
|
|
645
|
-
- Signal y values.
|
|
646
|
-
x: np.ndarray | None
|
|
647
|
-
- Signal x values.
|
|
648
|
-
- Default: None (indices will be used)
|
|
649
|
-
c: str
|
|
650
|
-
- Color of the line.
|
|
651
|
-
- Default: None
|
|
652
|
-
ls: str
|
|
653
|
-
- Linestyle
|
|
654
|
-
- Default: "-"
|
|
655
|
-
lw: Number
|
|
656
|
-
- Linewidth
|
|
657
|
-
- Default: 1
|
|
658
|
-
m: str
|
|
659
|
-
- Marker
|
|
660
|
-
- Default: None
|
|
661
|
-
ms: number
|
|
662
|
-
- Markersize
|
|
663
|
-
- Default: 5
|
|
664
|
-
label: str
|
|
665
|
-
- Label for the plot.
|
|
666
|
-
- Legend will use this.
|
|
667
|
-
- Default: "Signal"
|
|
668
|
-
|
|
669
|
-
Returns
|
|
670
|
-
-------
|
|
671
|
-
None
|
|
672
|
-
"""
|
|
673
|
-
if x is None:
|
|
674
|
-
x = np.arange(y.size)
|
|
675
|
-
matrix_subplot = self._subplots[-1]
|
|
676
|
-
matrix_subplot.plot(x, y, color=c, linestyle=ls, linewidth=lw, marker=m, markersize=ms, label=label)
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
def add_legend(self, ypos=1.1):
|
|
681
|
-
"""
|
|
682
|
-
Add legend to the figure.
|
|
683
|
-
|
|
684
|
-
Parameters
|
|
685
|
-
----------
|
|
686
|
-
ypos: float
|
|
687
|
-
- y position from the top.
|
|
688
|
-
- > 1 to push it higher, < 1 to push it lower
|
|
689
|
-
- Default: 1.3
|
|
690
|
-
|
|
691
|
-
Returns
|
|
692
|
-
-------
|
|
693
|
-
None
|
|
694
|
-
"""
|
|
695
|
-
subplots: list = self._subplots
|
|
696
|
-
fig = self._fig
|
|
697
|
-
|
|
698
|
-
all_handles, all_labels = [], []
|
|
699
|
-
|
|
700
|
-
for subplot in subplots:
|
|
701
|
-
handles, labels = subplot.get_legend_handles_labels()
|
|
702
|
-
all_handles.extend(handles)
|
|
703
|
-
all_labels.extend(labels)
|
|
704
|
-
|
|
705
|
-
# remove duplicates if needed
|
|
706
|
-
fig.legend(all_handles, all_labels, loc='upper right', bbox_to_anchor=(0.9, ypos), ncol=2, frameon=True, bbox_transform=fig.transFigure)
|
|
707
|
-
|
|
708
|
-
def add_meta_info(self, title=None, ylabel=None, xlabel=None, ts=13, ls=11):
|
|
709
|
-
"""
|
|
710
|
-
Add meta info to the figure.
|
|
711
|
-
|
|
712
|
-
Parameters
|
|
713
|
-
----------
|
|
714
|
-
title: str
|
|
715
|
-
- Title of the figure.
|
|
716
|
-
- Default: None
|
|
717
|
-
ylabel: str
|
|
718
|
-
- y label of the signal.
|
|
719
|
-
- It will only appear in the signal subplot.
|
|
720
|
-
- Default: None
|
|
721
|
-
xlabel: str
|
|
722
|
-
- x label of the signal.
|
|
723
|
-
- It will only appear in the signal subplot.
|
|
724
|
-
- Default: None
|
|
725
|
-
ts: Number
|
|
726
|
-
- Title size
|
|
727
|
-
- Default: 10
|
|
728
|
-
ls: Number
|
|
729
|
-
- Label size.
|
|
730
|
-
- Default: 10
|
|
731
|
-
|
|
732
|
-
Returns
|
|
733
|
-
-------
|
|
734
|
-
None
|
|
735
|
-
"""
|
|
736
|
-
subplots: list = self._subplots
|
|
737
|
-
fig = self._fig
|
|
738
|
-
|
|
739
|
-
ref_subplot = subplots[0]
|
|
740
|
-
signal_subplot = subplots[-1]
|
|
741
|
-
|
|
742
|
-
if title is not None:
|
|
743
|
-
ref_subplot.set_title(title, pad=10, size=ts)
|
|
744
|
-
if ylabel is not None:
|
|
745
|
-
signal_subplot.set_ylabel(ylabel, size=ls)
|
|
746
|
-
if xlabel is not None:
|
|
747
|
-
signal_subplot.set_xlabel(xlabel, size=ls)
|
|
748
|
-
|
|
749
|
-
def save(self, path="./figure.png"):
|
|
750
|
-
"""
|
|
751
|
-
Save the figure.
|
|
752
|
-
|
|
753
|
-
Parameters
|
|
754
|
-
----------
|
|
755
|
-
path: str
|
|
756
|
-
- Path to the output file.
|
|
757
|
-
|
|
758
|
-
Returns
|
|
759
|
-
-------
|
|
760
|
-
None
|
|
761
|
-
"""
|
|
762
|
-
fig = self._fig
|
|
763
|
-
fig.savefig(path, bbox_inches="tight")
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
#======== Plot distribution ===========
|
|
768
|
-
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):
|
|
769
|
-
"""
|
|
770
|
-
Plot distribution.
|
|
771
|
-
|
|
772
|
-
.. code-block:: python
|
|
773
|
-
|
|
774
|
-
import modusa as ms
|
|
775
|
-
import numpy as np
|
|
776
|
-
np.random.seed(42)
|
|
777
|
-
data = np.random.normal(loc=1, scale=1, size=1000)
|
|
778
|
-
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")
|
|
779
|
-
|
|
780
|
-
Parameters
|
|
781
|
-
----------
|
|
782
|
-
*args: ndarray
|
|
783
|
-
- Data arrays for which distribution needs to be plotted.
|
|
784
|
-
- Arrays will be flattened.
|
|
785
|
-
ann : list[tuple[Number, Number, str] | None
|
|
786
|
-
- A list of annotations to mark specific points. Each tuple should be of the form (start, end, label).
|
|
787
|
-
- Default: None => No annotation.
|
|
788
|
-
events : list[Number] | None
|
|
789
|
-
- A list of x-values where vertical lines (event markers) will be drawn.
|
|
790
|
-
- Default: None
|
|
791
|
-
xlim : tuple[Number, Number] | None
|
|
792
|
-
- Limits for the x-axis as (xmin, xmax).
|
|
793
|
-
- Default: None
|
|
794
|
-
ylim : tuple[Number, Number] | None
|
|
795
|
-
- Limits for the y-axis as (ymin, ymax).
|
|
796
|
-
- Default: None
|
|
797
|
-
xlabel : str | None
|
|
798
|
-
- Label for the x-axis.
|
|
799
|
-
- - Default: None
|
|
800
|
-
ylabel : str | None
|
|
801
|
-
- Label for the y-axis.
|
|
802
|
-
- Default: None
|
|
803
|
-
title : str | None
|
|
804
|
-
- Title of the plot.
|
|
805
|
-
- Default: None
|
|
806
|
-
legend : list[str] | None
|
|
807
|
-
- List of legend labels corresponding to each signal if plotting multiple distributions.
|
|
808
|
-
- Default: None
|
|
809
|
-
show_hist: bool
|
|
810
|
-
- Want to show histogram as well.
|
|
811
|
-
npoints: int
|
|
812
|
-
- Number of points for which gaussian needs to be computed between min and max.
|
|
813
|
-
- Higher value means more points are evaluated with the fitted gaussian, thereby higher resolution.
|
|
814
|
-
bins: int
|
|
815
|
-
- The number of bins for histogram.
|
|
816
|
-
- This is used only to plot the histogram.
|
|
817
|
-
|
|
818
|
-
Returns
|
|
819
|
-
-------
|
|
820
|
-
plt.Figure
|
|
821
|
-
- Matplotlib figure.
|
|
822
|
-
"""
|
|
823
|
-
from scipy.stats import gaussian_kde
|
|
824
|
-
|
|
825
|
-
if isinstance(legend, str):
|
|
826
|
-
legend = (legend, )
|
|
827
|
-
|
|
828
|
-
if legend is not None:
|
|
829
|
-
if len(legend) < len(args):
|
|
830
|
-
raise ValueError(f"Legend should be provided for each signal.")
|
|
831
|
-
|
|
832
|
-
# Create figure
|
|
833
|
-
fig = plt.figure(figsize=(16, 4))
|
|
834
|
-
gs = gridspec.GridSpec(2, 1, height_ratios=[0.1, 1])
|
|
835
|
-
|
|
836
|
-
colors = plt.get_cmap('tab10').colors
|
|
837
|
-
|
|
838
|
-
dist_ax = fig.add_subplot(gs[1, 0])
|
|
839
|
-
annotation_ax = fig.add_subplot(gs[0, 0], sharex=dist_ax)
|
|
840
|
-
|
|
841
|
-
# Set limits
|
|
842
|
-
if xlim is not None:
|
|
843
|
-
dist_ax.set_xlim(xlim)
|
|
844
|
-
|
|
845
|
-
if ylim is not None:
|
|
846
|
-
dist_ax.set_ylim(ylim)
|
|
847
|
-
|
|
848
|
-
# Add plot
|
|
849
|
-
for i, data in enumerate(args):
|
|
850
|
-
# Fit gaussian to the data
|
|
851
|
-
kde = gaussian_kde(data)
|
|
852
|
-
|
|
853
|
-
# Create points to evaluate KDE
|
|
854
|
-
x = np.linspace(np.min(data), np.max(data), npoints)
|
|
855
|
-
y = kde(x)
|
|
856
|
-
|
|
857
|
-
if legend is not None:
|
|
858
|
-
dist_ax.plot(x, y, color=colors[i], label=legend[i])
|
|
859
|
-
if show_hist is True:
|
|
860
|
-
dist_ax.hist(data, bins=bins, density=True, alpha=0.3, facecolor=colors[i], edgecolor='black', label=legend[i])
|
|
861
|
-
else:
|
|
862
|
-
dist_ax.plot(x, y, color=colors[i])
|
|
863
|
-
if show_hist is True:
|
|
864
|
-
dist_ax.hist(data, bins=bins, density=True, alpha=0.3, facecolor=colors[i], edgecolor='black')
|
|
865
|
-
|
|
866
|
-
# Add annotations
|
|
867
|
-
if ann is not None:
|
|
868
|
-
annotation_ax.set_ylim(0, 1) # For consistent layout
|
|
869
|
-
# Determine visible x-range
|
|
870
|
-
x_view_min = xlim[0] if xlim is not None else np.min(x)
|
|
871
|
-
x_view_max = xlim[1] if xlim is not None else np.max(x)
|
|
872
|
-
for i, (start, end, tag) in enumerate(ann):
|
|
873
|
-
# We make sure that we only plot annotation that are within the x range of the current view
|
|
874
|
-
if start >= x_view_max or end <= x_view_min:
|
|
875
|
-
continue
|
|
876
|
-
|
|
877
|
-
# Clip boundaries to xlim
|
|
878
|
-
start = max(start, x_view_min)
|
|
879
|
-
end = min(end, x_view_max)
|
|
880
|
-
|
|
881
|
-
color = colors[i % len(colors)]
|
|
882
|
-
width = end - start
|
|
883
|
-
rect = Rectangle((start, 0), width, 1, color=color, alpha=0.7)
|
|
884
|
-
annotation_ax.add_patch(rect)
|
|
885
|
-
|
|
886
|
-
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)
|
|
887
|
-
text_obj.set_clip_path(rect)
|
|
888
|
-
|
|
889
|
-
# Add legend
|
|
890
|
-
if legend is not None:
|
|
891
|
-
handles, labels = dist_ax.get_legend_handles_labels()
|
|
892
|
-
fig.legend(handles, labels, loc='upper right', bbox_to_anchor=(0.9, 1.1), ncol=len(legend), frameon=True)
|
|
893
|
-
|
|
894
|
-
# Set title, labels
|
|
895
|
-
if title is not None:
|
|
896
|
-
annotation_ax.set_title(title, pad=10, size=11)
|
|
897
|
-
if xlabel is not None:
|
|
898
|
-
dist_ax.set_xlabel(xlabel)
|
|
899
|
-
if ylabel is not None:
|
|
900
|
-
dist_ax.set_ylabel(ylabel)
|
|
901
|
-
|
|
902
|
-
# Remove the boundaries and ticks from annotation axis
|
|
903
|
-
if ann is not None:
|
|
904
|
-
annotation_ax.tick_params(left=False, bottom=False, labelleft=False, labelbottom=False)
|
|
905
|
-
else:
|
|
906
|
-
annotation_ax.axis("off")
|
|
907
|
-
|
|
908
|
-
fig.subplots_adjust(hspace=0.01, wspace=0.05)
|
|
909
|
-
plt.close()
|
|
910
|
-
return fig
|
|
911
|
-
|
|
912
|
-
|
|
913
39
|
class Fig:
|
|
914
40
|
"""
|
|
915
|
-
|
|
41
|
+
A utility class that provides easy-to-use API for
|
|
42
|
+
plotting 1D/2D signals along with clean representations
|
|
43
|
+
of annotations, events.
|
|
916
44
|
"""
|
|
917
45
|
|
|
918
46
|
def __init__(self, arrangement="asm", xlim=None):
|
|
919
|
-
|
|
47
|
+
|
|
920
48
|
self._xlim = xlim
|
|
921
49
|
self._curr_row_idx = 1 # Starting from 1 because row 0 is reserved for reference subplot
|
|
922
50
|
self._curr_color_idx = 0 # So that we have different color across all the subplots to avoid legend confusion
|
|
@@ -924,7 +52,7 @@ class Fig:
|
|
|
924
52
|
# Subplot setup
|
|
925
53
|
self._fig, self._axs = self._generate_subplots(arrangement) # This will fill in the all the above variables
|
|
926
54
|
|
|
927
|
-
|
|
55
|
+
|
|
928
56
|
def _get_curr_row(self):
|
|
929
57
|
"""
|
|
930
58
|
Get the active row where you can add
|
|
@@ -957,7 +85,7 @@ class Fig:
|
|
|
957
85
|
|
|
958
86
|
return [x[0] - dx / 2, x[-1] + dx / 2, y[0] - dy / 2, y[-1] + dy / 2]
|
|
959
87
|
|
|
960
|
-
|
|
88
|
+
|
|
961
89
|
def _generate_subplots(self, arrangement):
|
|
962
90
|
"""
|
|
963
91
|
Generate subplots based on the configuration.
|
|
@@ -983,7 +111,7 @@ class Fig:
|
|
|
983
111
|
# Calculate height ratios list based on the arrangement
|
|
984
112
|
for char in arrangement:
|
|
985
113
|
height_ratios = [height[char] for char in arrangement]
|
|
986
|
-
|
|
114
|
+
|
|
987
115
|
# Calculate total fig height
|
|
988
116
|
fig_height = height["r"] + (n_aux_sp * height["a"]) + (n_signal_sp * height["s"]) + (n_matrix_sp * height["m"])
|
|
989
117
|
|
|
@@ -1005,17 +133,17 @@ class Fig:
|
|
|
1005
133
|
elif char == "m":
|
|
1006
134
|
axs[i, 0].grid(True, linestyle=':', linewidth=0.7, color='gray', alpha=0.7)
|
|
1007
135
|
axs[i, 0].tick_params(bottom=False, labelbottom=False)
|
|
1008
|
-
|
|
136
|
+
|
|
1009
137
|
axs[i, 0].sharex(axs[0, 0])
|
|
1010
|
-
|
|
138
|
+
|
|
1011
139
|
axs[-1, 0].tick_params(bottom=True, labelbottom=True)
|
|
1012
140
|
|
|
1013
141
|
# xlim should be applied on reference subplot, rest all subplots will automatically adjust
|
|
1014
142
|
if xlim is not None:
|
|
1015
143
|
axs[0, 0].set_xlim(xlim)
|
|
1016
|
-
|
|
144
|
+
|
|
1017
145
|
fig.subplots_adjust(hspace=0.2, wspace=0.05)
|
|
1018
|
-
|
|
146
|
+
|
|
1019
147
|
return fig, axs
|
|
1020
148
|
|
|
1021
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):
|
|
@@ -1068,7 +196,7 @@ class Fig:
|
|
|
1068
196
|
if x is None: x = np.arange(y.size)
|
|
1069
197
|
|
|
1070
198
|
if c is None: c = self._get_new_color()
|
|
1071
|
-
|
|
199
|
+
|
|
1072
200
|
curr_row[0].plot(x, y, color=c, linestyle=ls, linewidth=lw, marker=m, markersize=ms, label=label)
|
|
1073
201
|
|
|
1074
202
|
if ylabel is not None: curr_row[0].set_ylabel(ylabel)
|
|
@@ -1117,7 +245,7 @@ class Fig:
|
|
|
1117
245
|
"""
|
|
1118
246
|
if x is None: x = np.arange(M.shape[1])
|
|
1119
247
|
if y is None: y = np.arange(M.shape[0])
|
|
1120
|
-
|
|
248
|
+
|
|
1121
249
|
curr_row = self._get_curr_row() if ax is None else self._axs[ax]
|
|
1122
250
|
|
|
1123
251
|
extent = self._calculate_extent(x, y)
|
|
@@ -1131,8 +259,8 @@ class Fig:
|
|
|
1131
259
|
cbar = plt.colorbar(im, cax=curr_row[1])
|
|
1132
260
|
if label is not None:
|
|
1133
261
|
cbar.set_label(label, labelpad=5)
|
|
1134
|
-
|
|
1135
|
-
|
|
262
|
+
|
|
263
|
+
|
|
1136
264
|
def add_events(self, events, c=None, ls=None, lw=None, label=None, ax=None):
|
|
1137
265
|
"""
|
|
1138
266
|
Add events to the figure.
|
|
@@ -1164,9 +292,9 @@ class Fig:
|
|
|
1164
292
|
"""
|
|
1165
293
|
|
|
1166
294
|
curr_row = self._get_curr_row() if ax is None else self._axs[ax]
|
|
1167
|
-
|
|
295
|
+
|
|
1168
296
|
if c is None: c = self._get_new_color()
|
|
1169
|
-
|
|
297
|
+
|
|
1170
298
|
xlim = self._xlim
|
|
1171
299
|
|
|
1172
300
|
for i, event in enumerate(events):
|
|
@@ -1203,7 +331,7 @@ class Fig:
|
|
|
1203
331
|
None
|
|
1204
332
|
"""
|
|
1205
333
|
curr_row = self._get_curr_row() if ax is None else self._axs[ax]
|
|
1206
|
-
|
|
334
|
+
|
|
1207
335
|
xlim = self._xlim
|
|
1208
336
|
|
|
1209
337
|
for i, (start, end, tag) in enumerate(ann):
|
|
@@ -1250,7 +378,7 @@ class Fig:
|
|
|
1250
378
|
curr_row[0].set_ylabel(label, rotation=0, ha="center", va="center")
|
|
1251
379
|
curr_row[0].yaxis.set_label_position("right")
|
|
1252
380
|
curr_row[0].yaxis.set_label_coords(1.05, 0.75)
|
|
1253
|
-
|
|
381
|
+
|
|
1254
382
|
def add_legend(self, ypos=1.0):
|
|
1255
383
|
"""
|
|
1256
384
|
Add legend to the figure.
|
|
@@ -1297,8 +425,8 @@ class Fig:
|
|
|
1297
425
|
|
|
1298
426
|
if title is not None:
|
|
1299
427
|
ref_ax.set_title(title, pad=10, size=s)
|
|
1300
|
-
|
|
1301
|
-
|
|
428
|
+
|
|
429
|
+
|
|
1302
430
|
def add_xlabel(self, xlabel=None, s=None):
|
|
1303
431
|
"""
|
|
1304
432
|
Add shared x-label to the figure.
|
|
@@ -1316,9 +444,12 @@ class Fig:
|
|
|
1316
444
|
ref_ax = axs[-1, 0] # X-label is added to the last subplot
|
|
1317
445
|
if xlabel is not None:
|
|
1318
446
|
ref_ax.set_xlabel(xlabel, size=s)
|
|
1319
|
-
|
|
1320
|
-
def add_xticks():
|
|
1321
|
-
|
|
447
|
+
|
|
448
|
+
def add_xticks(self, xticks=None):
|
|
449
|
+
"""
|
|
450
|
+
Not implemented yet
|
|
451
|
+
"""
|
|
452
|
+
raise NotImplementedError("Please raise a github issue https://github.com/meluron-toolbox/modusa/issues")
|
|
1322
453
|
|
|
1323
454
|
def save(self, path="./figure.png"):
|
|
1324
455
|
"""
|
|
@@ -1335,6 +466,155 @@ class Fig:
|
|
|
1335
466
|
"""
|
|
1336
467
|
fig = self._fig
|
|
1337
468
|
fig.savefig(path, bbox_inches="tight")
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
#======== Plot distribution ===========
|
|
473
|
+
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):
|
|
474
|
+
"""
|
|
475
|
+
Plot distribution.
|
|
476
|
+
|
|
477
|
+
.. code-block:: python
|
|
478
|
+
|
|
479
|
+
import modusa as ms
|
|
480
|
+
import numpy as np
|
|
481
|
+
np.random.seed(42)
|
|
482
|
+
data = np.random.normal(loc=1, scale=1, size=1000)
|
|
483
|
+
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")
|
|
484
|
+
|
|
485
|
+
Parameters
|
|
486
|
+
----------
|
|
487
|
+
*args: ndarray
|
|
488
|
+
- Data arrays for which distribution needs to be plotted.
|
|
489
|
+
- Arrays will be flattened.
|
|
490
|
+
ann : list[tuple[Number, Number, str] | None
|
|
491
|
+
- A list of annotations to mark specific points. Each tuple should be of the form (start, end, label).
|
|
492
|
+
- Default: None => No annotation.
|
|
493
|
+
events : list[Number] | None
|
|
494
|
+
- A list of x-values where vertical lines (event markers) will be drawn.
|
|
495
|
+
- Default: None
|
|
496
|
+
xlim : tuple[Number, Number] | None
|
|
497
|
+
- Limits for the x-axis as (xmin, xmax).
|
|
498
|
+
- Default: None
|
|
499
|
+
ylim : tuple[Number, Number] | None
|
|
500
|
+
- Limits for the y-axis as (ymin, ymax).
|
|
501
|
+
- Default: None
|
|
502
|
+
xlabel : str | None
|
|
503
|
+
- Label for the x-axis.
|
|
504
|
+
- - Default: None
|
|
505
|
+
ylabel : str | None
|
|
506
|
+
- Label for the y-axis.
|
|
507
|
+
- Default: None
|
|
508
|
+
title : str | None
|
|
509
|
+
- Title of the plot.
|
|
510
|
+
- Default: None
|
|
511
|
+
legend : list[str] | None
|
|
512
|
+
- List of legend labels corresponding to each signal if plotting multiple distributions.
|
|
513
|
+
- Default: None
|
|
514
|
+
show_hist: bool
|
|
515
|
+
- Want to show histogram as well.
|
|
516
|
+
npoints: int
|
|
517
|
+
- Number of points for which gaussian needs to be computed between min and max.
|
|
518
|
+
- Higher value means more points are evaluated with the fitted gaussian, thereby higher resolution.
|
|
519
|
+
bins: int
|
|
520
|
+
- The number of bins for histogram.
|
|
521
|
+
- This is used only to plot the histogram.
|
|
522
|
+
|
|
523
|
+
Returns
|
|
524
|
+
-------
|
|
525
|
+
plt.Figure
|
|
526
|
+
- Matplotlib figure.
|
|
527
|
+
"""
|
|
528
|
+
from scipy.stats import gaussian_kde
|
|
529
|
+
|
|
530
|
+
if isinstance(legend, str):
|
|
531
|
+
legend = (legend, )
|
|
532
|
+
|
|
533
|
+
if legend is not None:
|
|
534
|
+
if len(legend) < len(args):
|
|
535
|
+
raise ValueError(f"Legend should be provided for each signal.")
|
|
536
|
+
|
|
537
|
+
# Create figure
|
|
538
|
+
fig = plt.figure(figsize=(16, 4))
|
|
539
|
+
gs = gridspec.GridSpec(2, 1, height_ratios=[0.1, 1])
|
|
540
|
+
|
|
541
|
+
colors = plt.get_cmap('tab10').colors
|
|
542
|
+
|
|
543
|
+
dist_ax = fig.add_subplot(gs[1, 0])
|
|
544
|
+
annotation_ax = fig.add_subplot(gs[0, 0], sharex=dist_ax)
|
|
545
|
+
|
|
546
|
+
# Set limits
|
|
547
|
+
if xlim is not None:
|
|
548
|
+
dist_ax.set_xlim(xlim)
|
|
549
|
+
|
|
550
|
+
if ylim is not None:
|
|
551
|
+
dist_ax.set_ylim(ylim)
|
|
552
|
+
|
|
553
|
+
# Add plot
|
|
554
|
+
for i, data in enumerate(args):
|
|
555
|
+
# Fit gaussian to the data
|
|
556
|
+
kde = gaussian_kde(data)
|
|
557
|
+
|
|
558
|
+
# Create points to evaluate KDE
|
|
559
|
+
x = np.linspace(np.min(data), np.max(data), npoints)
|
|
560
|
+
y = kde(x)
|
|
561
|
+
|
|
562
|
+
if legend is not None:
|
|
563
|
+
dist_ax.plot(x, y, color=colors[i], label=legend[i])
|
|
564
|
+
if show_hist is True:
|
|
565
|
+
dist_ax.hist(data, bins=bins, density=True, alpha=0.3, facecolor=colors[i], edgecolor='black', label=legend[i])
|
|
566
|
+
else:
|
|
567
|
+
dist_ax.plot(x, y, color=colors[i])
|
|
568
|
+
if show_hist is True:
|
|
569
|
+
dist_ax.hist(data, bins=bins, density=True, alpha=0.3, facecolor=colors[i], edgecolor='black')
|
|
570
|
+
|
|
571
|
+
# Add annotations
|
|
572
|
+
if ann is not None:
|
|
573
|
+
annotation_ax.set_ylim(0, 1) # For consistent layout
|
|
574
|
+
# Determine visible x-range
|
|
575
|
+
x_view_min = xlim[0] if xlim is not None else np.min(x)
|
|
576
|
+
x_view_max = xlim[1] if xlim is not None else np.max(x)
|
|
577
|
+
for i, (start, end, tag) in enumerate(ann):
|
|
578
|
+
# We make sure that we only plot annotation that are within the x range of the current view
|
|
579
|
+
if start >= x_view_max or end <= x_view_min:
|
|
580
|
+
continue
|
|
581
|
+
|
|
582
|
+
# Clip boundaries to xlim
|
|
583
|
+
start = max(start, x_view_min)
|
|
584
|
+
end = min(end, x_view_max)
|
|
585
|
+
|
|
586
|
+
color = colors[i % len(colors)]
|
|
587
|
+
width = end - start
|
|
588
|
+
rect = Rectangle((start, 0), width, 1, color=color, alpha=0.7)
|
|
589
|
+
annotation_ax.add_patch(rect)
|
|
590
|
+
|
|
591
|
+
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)
|
|
592
|
+
text_obj.set_clip_path(rect)
|
|
593
|
+
|
|
594
|
+
# Add legend
|
|
595
|
+
if legend is not None:
|
|
596
|
+
handles, labels = dist_ax.get_legend_handles_labels()
|
|
597
|
+
fig.legend(handles, labels, loc='upper right', bbox_to_anchor=(0.9, 1.1), ncol=len(legend), frameon=True)
|
|
598
|
+
|
|
599
|
+
# Set title, labels
|
|
600
|
+
if title is not None:
|
|
601
|
+
annotation_ax.set_title(title, pad=10, size=11)
|
|
602
|
+
if xlabel is not None:
|
|
603
|
+
dist_ax.set_xlabel(xlabel)
|
|
604
|
+
if ylabel is not None:
|
|
605
|
+
dist_ax.set_ylabel(ylabel)
|
|
606
|
+
|
|
607
|
+
# Remove the boundaries and ticks from annotation axis
|
|
608
|
+
if ann is not None:
|
|
609
|
+
annotation_ax.tick_params(left=False, bottom=False, labelleft=False, labelbottom=False)
|
|
610
|
+
else:
|
|
611
|
+
annotation_ax.axis("off")
|
|
612
|
+
|
|
613
|
+
fig.subplots_adjust(hspace=0.01, wspace=0.05)
|
|
614
|
+
plt.close()
|
|
615
|
+
return fig
|
|
616
|
+
|
|
617
|
+
|
|
1338
618
|
|
|
1339
619
|
|
|
1340
620
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
modusa-0.3.
|
|
2
|
-
modusa-0.3.
|
|
3
|
-
modusa-0.3.
|
|
4
|
-
modusa-0.3.
|
|
1
|
+
modusa-0.3.55.dist-info/METADATA,sha256=356OBn6deHVeRHRhwLasQ-WVKAc2AeDnL26Zm_jf1lE,1436
|
|
2
|
+
modusa-0.3.55.dist-info/WHEEL,sha256=9P2ygRxDrTJz3gsagc0Z96ukrxjr-LFBGOgv3AuKlCA,90
|
|
3
|
+
modusa-0.3.55.dist-info/entry_points.txt,sha256=fmKpleVXj6CdaBVL14WoEy6xx7JQCs85jvzwTi3lePM,73
|
|
4
|
+
modusa-0.3.55.dist-info/licenses/LICENSE.md,sha256=JTaXAjx5awk76VArKCx5dUW8vmLEWsL_ZlR7-umaHbA,1078
|
|
5
5
|
modusa/.DS_Store,sha256=_gm6qJREwfMi8dE7n5S89_RG46u5t3xHyD-smNhtNoM,6148
|
|
6
|
-
modusa/__init__.py,sha256=
|
|
6
|
+
modusa/__init__.py,sha256=RMIKAZZ27w1oh2UFMJvZQtQ-SluIQsac_3mSK_LaM30,277
|
|
7
7
|
modusa/config.py,sha256=bTqK4t00FZqERVITrxW_q284aDDJAa9aMSfFknfR-oU,280
|
|
8
8
|
modusa/decorators.py,sha256=8zeNX_wE37O6Vp0ysR4-WCZaEL8mq8dyCF_I5DHOzks,5905
|
|
9
9
|
modusa/devtools/generate_docs_source.py,sha256=UDflHsk-Yh9-3YJTVBzKL32y8hcxiRgAlFEBTMiDqwM,3301
|
|
@@ -41,7 +41,7 @@ modusa/models/t_ax.py,sha256=ZUhvZPUW1TkdZYuUd6Ucm-vsv0JqtZ9yEe3ab67Ma6w,8022
|
|
|
41
41
|
modusa/models/tds.py,sha256=FAGfibjyyE_lkEuQp-vSCuqQnopOjmy_IXqUjRlg9kc,11677
|
|
42
42
|
modusa/plugins/__init__.py,sha256=r1Bf5mnrVKRIwxboutY1iGzDy4EPQhqpk1kSW7iJj_Q,54
|
|
43
43
|
modusa/plugins/base.py,sha256=Bh_1Bja7fOymFsCgwhXDbV6ys3D8muNrPwrfDrG_G_A,2382
|
|
44
|
-
modusa/tools/__init__.py,sha256=
|
|
44
|
+
modusa/tools/__init__.py,sha256=TfIlnbSRVEk8C9YY6H5y0qiaKttFV_X3m6nI2qPYUH0,295
|
|
45
45
|
modusa/tools/_plotter_old.py,sha256=KGow7mihA2H1WNq7s5bpivhCgGo2qVIeDaO6iabpsrg,19294
|
|
46
46
|
modusa/tools/ann_loader.py,sha256=RePlwD4qG6gGrD4mOJ3RDR9q_gUscCY90_R9lgFU9lM,780
|
|
47
47
|
modusa/tools/audio_converter.py,sha256=415qBoPm2sBIuBSI7m1XBKm0AbmVmPydIPPr-uO8D3c,1778
|
|
@@ -50,7 +50,7 @@ modusa/tools/audio_player.py,sha256=GP04TWW4jBwQBjANkfR_cJtEy7cIhvbu8RTwnf9hD6E,
|
|
|
50
50
|
modusa/tools/audio_recorder.py,sha256=d2fVt0Sd2tlBdb2WlUs60K4N23zuxM3KUpQqX0ifPi8,2769
|
|
51
51
|
modusa/tools/base.py,sha256=C0ESJ0mIfjjRlAkRbSetNtMoOfS6IrHBjexRp3l_Mh4,1293
|
|
52
52
|
modusa/tools/math_ops.py,sha256=ZZ7U4DgqT7cOeE7_Lzi_Qq-48WYfwR9_osbZwTmE9eg,8690
|
|
53
|
-
modusa/tools/plotter.py,sha256=
|
|
53
|
+
modusa/tools/plotter.py,sha256=lk9uXTpyAWAITYIILXq-U6PnwezdYq2JVsxxukpYPiY,16960
|
|
54
54
|
modusa/tools/youtube_downloader.py,sha256=hB_X8-7nOHXOlxg6vv3wyhBLoAsWyomrULP6_uCQL7s,1698
|
|
55
55
|
modusa/utils/.DS_Store,sha256=nLXMwF7QJNuglLI_Gk74F7vl5Dyus2Wd74Mgowijmdo,6148
|
|
56
56
|
modusa/utils/__init__.py,sha256=1oLL20yLB1GL9IbFiZD8OReDqiCpFr-yetIR6x1cNkI,23
|
|
@@ -59,4 +59,4 @@ modusa/utils/excp.py,sha256=L9vhaGjKpv9viJYdmC9n5ndmk2GVbUBuFyZyhAQZmWY,906
|
|
|
59
59
|
modusa/utils/logger.py,sha256=K0rsnObeNKCxlNeSnVnJeRhgfmob6riB2uyU7h3dDmA,571
|
|
60
60
|
modusa/utils/np_func_cat.py,sha256=TyIFgRc6bARRMDnZxlVURO5Z0I-GWhxRONYyIv-Vwxs,1007
|
|
61
61
|
modusa/utils/plot.py,sha256=s_vNdxvKfwxEngvJPgrF1PcmxZNnNaaXPViHWjyjJ-c,5335
|
|
62
|
-
modusa-0.3.
|
|
62
|
+
modusa-0.3.55.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|