Skip to content

Commit 2845d83

Browse files
committed
whenxuan: update the readme for mvmd
1 parent 6a35f64 commit 2845d83

3 files changed

Lines changed: 109 additions & 19 deletions

File tree

‎README.md‎

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
# PySDKit: signal decomposition in Python
2-
31
<div align="center">
42

3+
# PySDKit: signal decomposition in Python
4+
55
[![PyPI version](https://badge.fury.io/py/PySDKit.svg)](https://pypi.org/project/PySDKit/)
66
![License](https://img.shields.io/github/license/wwhenxuan/PySDKit)
77
[![Python](https://img.shields.io/badge/python-3.8+-blue?logo=python)](https://www.python.org/)
@@ -68,16 +68,27 @@ plot_IMFs(signal, IMFs, view="2d_freq", fs=fs, freq_max=fs / 2)
6868

6969
![vmd_example](https://raw.githubusercontent.com/wwhenxuan/PySDKit/main/images/vmd_example.jpg)
7070

71-
Better observe the characteristics of the decomposed intrinsic mode function in the frequency domain.
71+
For multichannel recordings, algorithms such as multivariate VMD ([`MVMD`](https://doi.org/10.1109/TSP.2019.2951223)) keep shared oscillations **mode-aligned** across channels:
7272

7373
~~~python
74-
from pysdkit.plot import plot_IMFs_amplitude_spectra
74+
import numpy as np
75+
from pysdkit import MVMD
76+
from pysdkit.plot import plot_IMFs
7577

76-
# frequency domain visualization
77-
plot_IMFs_amplitude_spectra(IMFs, smooth="exp") # use exp smooth
78+
t = np.arange(0, 1, 0.001)
79+
# ch1: 2+36 Hz, ch2: 24+36 Hz, ch3: 80+36 Hz (36 Hz shared)
80+
signal = np.vstack([
81+
np.cos(2*np.pi*2*t) + np.cos(2*np.pi*36*t),
82+
np.cos(2*np.pi*24*t) + np.cos(2*np.pi*36*t),
83+
np.cos(2*np.pi*80*t) + np.cos(2*np.pi*36*t),
84+
])
85+
86+
mvmd = MVMD(alpha=2000, K=4, tau=0.0, init="uniform")
87+
IMFs = mvmd.fit_transform(signal) # shape: (K, T, C)
88+
plot_IMFs(signal, IMFs) # per-channel panels
7889
~~~
7990

80-
![frequency_example](https://raw.githubusercontent.com/wwhenxuan/PySDKit/main/images/frequency_example.jpg)
91+
![mvmd_example](https://raw.githubusercontent.com/wwhenxuan/PySDKit/main/images/mvmd_example.jpg)
8192

8293
## Target 🎯 <a id="Target"></a>
8394

‎images/mvmd_example.jpg‎

351 KB
Loading

‎pysdkit/plot/_plot_imfs.py‎

Lines changed: 91 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ def plot_IMFs(
3636
save_name: Optional[str] = None,
3737
fs: Optional[float] = None,
3838
freq_max: Optional[float] = None,
39+
show_alignment: bool = False,
3940
) -> Optional[plt.Figure]:
4041
"""
4142
Visualizes the numpy array of intrinsic mode functions derived from the decomposition of a signal.
@@ -60,6 +61,7 @@ def plot_IMFs(
6061
:param save_name: The name of the saved image file
6162
:param fs: sampling frequency for ``view="2d_freq"``
6263
:param freq_max: spectrum x-axis upper limit for ``view="2d_freq"``
64+
:param show_alignment: for multivariate 2D plots, append an Align column
6365
:return: The figure object for the plot
6466
"""
6567
# Get the selected visualization
@@ -97,6 +99,7 @@ def plot_IMFs(
9799
dpi=dpi,
98100
spine_width=spine_width,
99101
save_name=save_name,
102+
show_alignment=show_alignment,
100103
)
101104
else:
102105
# The output data is in the wrong format
@@ -516,58 +519,105 @@ def plot_multi_IMFs(
516519
IMFs: np.ndarray,
517520
max_imfs: Optional[int] = -1,
518521
colors: Optional[List] = None,
522+
channel_colors: Optional[List] = None,
519523
save_figure: Optional[bool] = False,
520524
return_figure: Optional[bool] = False,
521525
dpi: Optional[int] = 64,
522526
fontsize: float = 8,
523527
spine_width: float = 2,
524528
save_name: Optional[str] = None,
529+
show_alignment: bool = False,
525530
) -> Optional[plt.Figure]:
526531
"""
527-
Plotting a multivariate signal and its decomposed intrinsic mode functions
528-
:param signal: The input original signal
529-
:param IMFs: The intrinsic mode functions obtained after signal decomposition
532+
Plotting a multivariate signal and its decomposed intrinsic mode functions.
533+
534+
Layout (when ``show_alignment=True``)::
535+
536+
| Var-0 | Var-1 | ... | Align |
537+
|-------|-------|-----|-------|
538+
| Signal channel panels ... | all channels overlaid |
539+
| IMF-k channel panels ... | all channels of IMF-k |
540+
541+
The rightmost **Align** column overlays every channel of the same mode
542+
(paper-style mode-alignment check used in MVMD demos).
543+
544+
:param signal: The input original signal, shape ``(n_vars, seq_len)``
545+
:param IMFs: Modes of shape ``(n_imfs, seq_len, n_vars)``
530546
:param max_imfs: The number of decomposition modes to be plotted
531-
:param colors: List of color strings for plotting
547+
:param colors: Row colors for per-channel panels (signal / IMF rows)
548+
:param channel_colors: Colors used in the Align column (one per channel)
532549
:param save_figure: Whether to save the figure as an image
533550
:param return_figure: Whether to return the figure object
534551
:param dpi: The resolution of the saved image
535552
:param fontsize: The font size of the axis labels
536553
:param save_name: The name of the saved image file
537554
:param spine_width: The width of the visible axes spines
555+
:param show_alignment: If True, append a rightmost alignment column
538556
:return: The figure object for the plot
539557
"""
540558

541559
# Set the matplotlib configs
542560
set_themes(choice="plot_imfs")
543561

562+
signal = np.asarray(signal, dtype=float)
563+
IMFs = np.asarray(IMFs, dtype=float)
564+
if signal.ndim != 2:
565+
raise ValueError("signal must have shape (n_vars, seq_len)")
566+
if IMFs.ndim != 3:
567+
raise ValueError("IMFs must have shape (n_imfs, seq_len, n_vars)")
568+
544569
# Get the number of elements and signal length of a multi-element signal
545570
n_vars, seq_len = signal.shape
571+
if IMFs.shape[1] != seq_len or IMFs.shape[2] != n_vars:
572+
raise ValueError(
573+
"IMFs shape must be (n_imfs, seq_len, n_vars) matching the signal"
574+
)
546575

547576
# Edge padding
548577
padding = int(seq_len / 50)
549578

550579
# Determine the number of rows
551580
if max_imfs == -1:
552-
n_rows = IMFs.shape[0] + 1
581+
n_imfs = IMFs.shape[0]
553582
else:
554-
n_rows = min(max_imfs, IMFs.shape[0]) + 1
583+
n_imfs = min(int(max_imfs), IMFs.shape[0])
584+
n_rows = n_imfs + 1
555585

556-
# Reconstruct the input signal
586+
# Reconstruct the input signal stack: (n_rows, seq_len, n_vars)
557587
signals = np.zeros(shape=(n_rows, seq_len, n_vars))
558588
signals[0, :, :] = signal.transpose(1, 0)
559-
signals[1:, :, :] = IMFs
589+
signals[1:, :, :] = IMFs[:n_imfs]
560590

561591
# Set the colors for plotting
562592
if colors is None:
563-
colors = COLORS
593+
colors = list(COLORS)
594+
else:
595+
colors = list(colors)
564596
# Add random colors if there are not enough colors in the list
565597
while len(colors) <= n_rows:
566598
colors.append(generate_random_hex_color())
567599

568-
# Create the figure and axes for multi-plotting
600+
if channel_colors is None:
601+
channel_colors = [
602+
"#1f4e79",
603+
"#c45c26",
604+
"#2a9d8f",
605+
"#6c5ce7",
606+
"#d63031",
607+
"#00b894",
608+
]
609+
else:
610+
channel_colors = list(channel_colors)
611+
while len(channel_colors) < n_vars:
612+
channel_colors.append(generate_random_hex_color())
613+
614+
n_cols = n_vars + (1 if show_alignment else 0)
569615
fig, ax = plt.subplots(
570-
nrows=n_rows, ncols=n_vars, figsize=(3 * n_vars, 1.5 * n_rows - 1), dpi=256
616+
nrows=n_rows,
617+
ncols=n_cols,
618+
figsize=(3 * n_vars + (3.2 if show_alignment else 0), 1.5 * n_rows - 1),
619+
dpi=256,
620+
squeeze=False,
571621
)
572622
fig.tight_layout()
573623

@@ -603,7 +653,33 @@ def plot_multi_IMFs(
603653
if row != n_rows - 1:
604654
ax[row, col].set_xticks([])
605655

606-
for col in range(n_vars):
656+
if show_alignment:
657+
ax_a = ax[row, n_vars]
658+
ax_a.axhline(
659+
y=0, color="gray", linestyle="-", alpha=0.6, linewidth=spine_width
660+
)
661+
for c in range(n_vars):
662+
ax_a.plot(
663+
signals[row, :, c],
664+
color=channel_colors[c],
665+
lw=0.9,
666+
alpha=0.9,
667+
label=f"ch{c + 1}",
668+
)
669+
ax_a.set_xlim(-padding, seq_len + padding)
670+
ax_a.tick_params(axis="both", which="major", labelsize=8)
671+
for spine_name, spine in ax_a.spines.items():
672+
if spine_name != "left":
673+
spine.set_visible(False)
674+
else:
675+
spine.set_visible(True)
676+
spine.set_linewidth(spine_width)
677+
if row != n_rows - 1:
678+
ax_a.set_xticks([])
679+
if row == 0:
680+
ax_a.legend(fontsize=7, loc="upper right", frameon=False)
681+
682+
for col in range(n_cols):
607683
# Open the bottom spine of the last axes and set its position
608684
ax[-1, col].spines["bottom"].set_position(("axes", -0.1))
609685
ax[-1, col].spines["bottom"].set_visible(True)
@@ -619,6 +695,8 @@ def plot_multi_IMFs(
619695
# Setting the number of variations
620696
for col in range(n_vars):
621697
ax[0, col].set_title(f"Var-{col}", fontsize=fontsize + 1)
698+
if show_alignment:
699+
ax[0, n_vars].set_title("Align", fontsize=fontsize + 1)
622700

623701
# Save the figure if requested
624702
saved = False
@@ -637,6 +715,7 @@ def plot_multi_IMFs(
637715
# Return the figure if requested
638716
if return_figure is True:
639717
return fig
718+
return None
640719

641720

642721
def plot_multi_3D_IMFs(

0 commit comments

Comments
 (0)