Site icon R-bloggers

Variational Mode Decomposition (VMD) using R

[This article was first published on mlampros, and kindly contributed to R-bloggers]. (You can report issue about the content on this page here)
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.

This blog post is a slight modification of the R package Vignette. The VMDecomp R package is an RcppArmadillo implementation of the Matlab Code for Variational Mode Decomposition (1- and 2-dimensional) based on the papers,

The Matlab code is available to download in the Author’s website (https://math.montana.edu/dzosso/code/).


Example Use Cases

VMD has been used in many scientific areas with true or synthetic data. I’ll explain the functionality of the VMDecomp R package using two simple use cases.

Noise Removal (1-Dimensional VMD)

In the Authors’ paper “Variational Mode Decomposition” (page 13 / 16) is mentioned, “A ‘clean’ electrocardiogram (ECG) signal can be reconstructed by summing all but the first and last VMD modes, thus discarding the low-frequency baseline oscillation and most of the high-frequency noise.” I’ll use the ‘arrhythmia’ data (especially the first column which corresponds to the modified limb lead II (MLII)) for this purpose. This same example appears also online (Matlab Mathworks).

require(VMDecomp)
require(glue)

data(arrhythmia)

alpha = 2000       # moderate bandwidth constraint
tau = 0            # noise-tolerance (no strict fidelity enforcement)
K = 9              # 9 modes
DC = FALSE         # no DC part imposed
init = 1           # initialize omegas uniformly
tol = 1e-6

vec_arrhythmia = arrhythmia[['MLII']]

set.seed(1)
arr_vmd = vmd(data = vec_arrhythmia, 
              alpha = alpha,
              tau = tau, 
              K = K, 
              DC = DC,
              init = init, 
              tol = tol,
              verbose = TRUE)

# 1-dimensional VMD starts ...
# --------------------------------
# The 1-dimensional VMD starts ...
# --------------------------------
# Iteration: 10   uDiff: 2.28024
# Iteration: 20   uDiff: 1.05777
# Iteration: 30   uDiff: 0.515047
# Iteration: 40   uDiff: 0.346114
# Iteration: 50   uDiff: 0.277273
# Iteration: 60   uDiff: 0.240478
# Iteration: 70   uDiff: 0.212126
# Iteration: 80   uDiff: 0.20821
# Iteration: 90   uDiff: 0.196179
# ...............
# Iteration: 470   uDiff: 0.00728391
# Iteration: 480   uDiff: 0.00698528
# Iteration: 490   uDiff: 0.00663912
# Iteration: 500   uDiff: 0.00629761
# -----------------------------------------
# The algorithm converged in iteration: 500
# -----------------------------------------
# Elapsed time: 0 hours and 0 minutes and 19 seconds. 


str(arr_vmd)
# List of 3
#  $ u    : num [1:10000, 1:9] -0.0695 -0.0697 -0.0702 -0.0707 -0.0711 ...
#  $ u_hat: cplx [1:10000, 1:9] 6.54e-04+0.0i -3.83e-03+5.1e-04i -1.99e-03+8.5e-04i ...
#  $ omega: num [1:500, 1:9] 0 0.00561 0.00595 0.00596 0.00586 ...


op <- par(mfrow = c(3, K/3))

for (item in K:1) {
  item_mode = glue::glue("IMF {K-item+1}")
  plot(x = arr_vmd$u[, item], type = 'l', main = item_mode, xlab = 'Time', ylab = '')
}


The previous plot shows the intrinsic mode functions (IMF’s). Based on the Authors’ statement if we exclude the first and last Modes (IMF’s) we’ll receive a noise-free ECG,

imfs_noise_free = rowSums(arr_vmd$u[, -c(1,K)])

op <- par(mfrow = c(1,1))

plot(x = vec_arrhythmia, type = 'l', col = "blue", xlab = 'Time', ylab = 'Signal')
lines(x = imfs_noise_free, col = "orange")
legend("topright", 
       legend = c("Original ECG", "Clean ECG"),
       col = c("blue", "orange"), 
       lty = 1:2, 
       cex = 0.8)

Synthetic image segmentation (2-Dimensional VMD)

To explain the functionality of the “vmd()” R function for 2-dimensional data I’ll load the synthetic image used in section 3.1 of the Authors’ second paper where it’s mentioned,

“The synthetic image is a composition of spatially overlapping basic shapes, more precisely six ellipses and a rectangle, with frequency patterns varying in both periodicity and direction… The spectrum is ideal for segmentation due to modes being deliberately both well isolated and narrow-banded. The resolution of the synthetic image is 256 x 256 and the experiment was run with parameters Alpha = 1000 and K = 5. This experiment converged in 520 iterations which took 45 seconds on a standard PC. The algorithm has no problems in accuracy nor timeliness in segmenting the image into its five constituent sub-images. The first is the DC component of the image – a solid ellipse and rectangle, while the four remaining decompositions show clear separation of the patterned ellipses. Due to the solid pieces having sharp edges, their spectra are not band limited, and only smoothed versions are recovered.”

require(R.matlab)
require(OpenImageR)

pth_texture = system.file('Matlab', 'VMD_2D', 'texture.mat', package = 'VMDecomp')
data = R.matlab::readMat(pth_texture)
data = data$f
dim(data)
# [1] 256 256

alpha = 1000       # bandwidth constraint
tau = 0.25         # Lagrangian multipliers dual ascent time step
K = 5              # number of modes
DC = TRUE          # includes DC part (first mode at DC)
init = 1           # initialize omegas randomly, may need multiple runs!
tol = 1e-7         # tolerance (for convergence)

set.seed(2)
res_2d = vmd(data = data,
             alpha = alpha,
             tau = tau,
             K = K,
             DC = DC,
             init = init,
             tol = tol,
             verbose = TRUE)

# --------------------------------
# The 2-dimensional VMD starts ...
# --------------------------------
# Iteration: 10  uDiff: 42.7451  omegaDiff: 0.00012395
# Iteration: 20  uDiff: 1.05264  omegaDiff: 1.15039e-09
# Iteration: 30  uDiff: 0.31892  omegaDiff: 4.14469e-10
# Iteration: 40  uDiff: 0.117648  omegaDiff: 1.45234e-10
# Iteration: 50  uDiff: 0.0491776  omegaDiff: 6.399e-11
# Iteration: 60  uDiff: 0.0222704  omegaDiff: 3.33701e-11
# Iteration: 70  uDiff: 0.0106591  omegaDiff: 1.87647e-11
# Iteration: 80  uDiff: 0.0053165  omegaDiff: 1.08343e-11
# Iteration: 90  uDiff: 0.00273909  omegaDiff: 6.30121e-12
# ............
# Iteration: 250  uDiff: 6.31694e-07  omegaDiff: 1.22605e-15
# Iteration: 260  uDiff: 4.33964e-07  omegaDiff: 8.05652e-16
# Iteration: 270  uDiff: 3.08055e-07  omegaDiff: 5.66624e-16
# Iteration: 280  uDiff: 2.26559e-07  omegaDiff: 4.31241e-16
# Iteration: 290  uDiff: 1.728e-07  omegaDiff: 3.54727e-16
# Iteration: 300  uDiff: 1.36554e-07  omegaDiff: 3.11406e-16
# Iteration: 310  uDiff: 1.11508e-07  omegaDiff: 2.86634e-16
# -----------------------------------------
# The algorithm converged in iteration: 317
# -----------------------------------------
# Elapsed time: 0 hours and 0 minutes and 41 seconds.

end_dims = dim(res_2d$u)[3]
str(res_2d)
# List of 3
#  $ u    : num [1:256, 1:256, 1:5] 3.16e-05 1.80e-05 -3.32e-06 -3.17e-05 -6.32e-05 ...
#  $ u_hat: cplx [1:256, 1:256, 1:5] 7.65e-04+0.00i -6.63e-04-3.28e-04i 7.74e-04-3.15e-04i ...
#  $ omega: num [1:317, 1:2, 1:5] 0 0 0 0 0 0 0 0 0 0 ...


To the output IMF’s I’ll also include the input image and then I’ll visualize all images in a multi-plot to verify that each IMF represents a different shape of the input image,

res_2d_lst = lapply(1:end_dims, function(x) res_2d$u[,, x])
res_2d_lst = append(res_2d_lst, list(data), after = 0)
str(res_2d_lst)
# List of 6
#  $ : num [1:256, 1:256] 0 0 0 0 0 0 0 0 0 0 ...
#  $ : num [1:256, 1:256] 3.16e-05 1.80e-05 -3.32e-06 -3.17e-05 -6.32e-05 ...
#  $ : num [1:256, 1:256] -4.36e-05 -3.08e-05 -1.19e-05 2.28e-05 6.01e-05 ...
#  $ : num [1:256, 1:256] -2.67e-07 2.53e-06 1.03e-06 1.94e-06 2.44e-07 ...
#  $ : num [1:256, 1:256] 3.31e-06 1.21e-06 2.15e-06 1.66e-07 4.97e-07 ...
#  $ : num [1:256, 1:256] 6.72e-06 9.27e-06 1.14e-05 8.79e-06 1.45e-06 ...


init_plt = OpenImageR::GaborFeatureExtract$new()
mlt_plt = init_plt$plot_multi_images(list_images = res_2d_lst, 
                                     par_ROWS = 3, 
                                     par_COLS = 2, 
                                     axes = TRUE,
                                     titles = c("Input Image", glue::glue("Imf_{1:end_dims}")))


Package Installation & Citation:


To install the package from CRAN use,

install.packages("VMDecomp")


and to download the latest version of the package from Github,

remotes::install_github('mlampros/VMDecomp')


If you use the VMDecomp R package in your paper or research please cite both VMDecomp and the original articles / software https://cran.r-project.org/web/packages/VMDecomp/citation.html:


@Manual{,
  title = {VMDecomp: Variational Mode Decomposition using R},
  author = {Lampros Mouselimis},
  year = {2022},
  note = {R package version 1.0.0},
  url = {https://CRAN.R-project.org/package=VMDecomp},
}


To leave a comment for the author, please follow the link and comment on their blog: mlampros.

R-bloggers.com offers daily e-mail updates about R news and tutorials about learning R and many other topics. Click here if you're looking to post or find an R/data-science job.
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.