Introduction to MatchingPursuit package

authors: Artur Gramacki and Jarosław Gramacki

1 R version and generation time

## R version:    4.5.1
## Generated on: 17-wrzesień-2026

2 Loading the package

library(MatchingPursuit)

3 The purpose and functionality of the package

The presented package enables the analysis of time-series signals using the Matching Pursuit (MP) and Orthogonal Matching Pursuit (OMP) algorithms (see Mallat and Zhang (1993), Pati, Rezaiifar, and Krishnaprasad (1993), Durka (2007), Elad (2010)). Additionally, it supports working with EEG (electroencephalogram) and ECG (electrocardiograph) signals. For multi-channel signals, each channel is decomposed independently; atoms are not selected jointly across channels.

The functionality of the package includes:

  1. Native R implementations of:
    • Matching Pursuit (MP-R)
    • Orthogonal Matching Pursuit (OMP-R)
  2. An educational and reference implementation of OMP through omp_reference()
  3. Optional high-performance Gabor-based Matching Pursuit using the external Enhanced Matching Pursuit Implementation (EMPI) backend
  4. Support for Gabor dictionaries via XML-based atom definitions
  5. Support for biomedical signal formats
    • EDF and EDF+ file support
    • WFDB record support
    • Standard EEG montages (bipolar, referential, average-reference)
  6. Visualization of time–frequency maps based on MP, OMP, and EMPI results
  7. Signal pre-processing using notch, low-pass, high-pass, band-pass, and band-stop filters
  8. Sparse decomposition using arbitrary user-defined dictionaries represented as numeric matrices
    ## Naming convention In this vignette, we use the following naming convention:

4 Installing the optional EMPI backend

The below empi_install() function downloads Enhanced Matching Pursuit Implementation external program (or EMPI for short), see Różański (2024), and stores it in the cache directory. The function downloads the EMPI program in a version compatible with the operating system used (Windows, Linux, MacOS-arm64).

First, the user can check where EMPI will be downloaded from.

empi_locate()
## $url
## [1] "https://github.com/develancer/empi/releases/download/1.0.4/empi-1.0.4-windows-x64.zip"
## 
## $fname
## [1] "empi-1.0.4-windows-x64.zip"

The code in the below chunk has been commented out because CRAN’s verification rules prohibit automatic binary downloads. Therefore, this function cannot be executed while generating this vignette.

# empi_install()

User can check whether the EMPI program is installed; if not, an error message is displayed indicating that installation is required.

empi_check()
## [1] "C:\\Users\\Artur\\AppData\\Local/R/cache/R/MatchingPursuit/empi/empi.exe"

Back to menu

5 Quick start

The following example demonstrates the native R MP/OMP workflow using mp_omp_execute(). The optional external EMPI-based workflow is presented afterwards.

Step 1 - Input signal preparation

Let us construct an example signal by combining seven non-stationary components. The resulting signal (highlighted in blue below) will be used to demonstrate the basic functionality of the package.

fs <- 1024
T <- 1
t <- seq(0, T - 1 / fs, 1 / fs)
N <- length(t)

# 7 non-stationary signals.
x1 <- sin(2 * pi * (10 + 40 * t) * t)                            # linear chirp
x2 <- sin(2 * pi * (20 * t^2) * t)                               # nonlinear chirp
x3 <- (1 + 0.5 * sin(2 * pi * 2 * t))  *  sin(2 * pi * 30 * t)   # AM
x4 <- sin(2 * pi * 50 * t + 5 * sin(2 * pi * 3 * t))             # FM
x5 <- exp(-2 * t)  *  sin(2 * pi * 60 * t)                       # decreasing amplitude
x6 <- sin(2 * pi * (5 + 20 * sin(2 * pi * t)) * t)               # frequency modulated sine wave
x7 <- t * sin(2 * pi * 40 * t)                                   # increasing amplitude

signal <- data.frame(x = x1 + x2 + x3 + x4 + x5 + x6 + x7)

Data must be stored in a data frame: rows represent samples for all channels, and columns represent channels. Our first demo dataset consists of only one channel (one column). The read_csv_signals() function checks whether the data has the correct structure. The first line of the file must contain two numbers: the sampling rate in Hz (freq) and the signal length in seconds (sec). This allows verification that the file actually contains freq*sec samples.

# The sample1.csv file contains exactly the same data as shown in Step 1.
file <- system.file("extdata", "sample1.csv", package = "MatchingPursuit")

# The first line of the file contains two values:
# the sampling rate in Hz (1024 Hz here) and the signal duration
# in seconds (1 s here).
out <- read.csv(file, header = FALSE)
head(out)
##           V1
## 1     1024 1
## 2 0.00000000
## 3 1.02492083
## 4 1.93756420
## 5 2.63875099
## 6 3.05949332

signal <- read_csv_signals(file)
signal
## Signal object (class 'sig')
## --------------------------------------------------
## Samples:            1024
## Channels:           1
## Sampling frequency: 1024 Hz
## Duration:           1 s

Step 2 - Native R MP decomposition

The input data (signal) is passed as an argument to the mp_omp_execute() function, which generates the object of class mp containing all atom parameters.

For Gabor-based decomposition, mp_omp_execute() provides the high-level native R interface. It prepares the Gabor dictionary, selects candidate atoms, performs MP or OMP decomposition, and returns an object of class "mp". In this example, classical Matching Pursuit is used; setting mode = "omp" runs Orthogonal Matching Pursuit instead.

fit_mp <- mp_omp_execute(
  mode = "mp",
  signal = signal,
  n_nonzero_coefs = 25
)
## mp_omp_execute(): method = "mp", channel = 1 Successfully processed.

summary(fit_mp)
## Summary of Matching Pursuit object (class 'mp')
## --------------------------------------------------------
## Samples:            1024
## Channels:           1
## Sampling frequency: 1024 Hz
## Duration:           1 s
## 
## Per-channel decomposition:
## 
## Explained energy = 1 - residual energy / signal energy
## For MP, this measure is preferred because the reconstruction 
## and residualare not generally orthogonal, so reconstruction 
## energy / signal energy is not equivalent to explained energy.
## 
## Channel 1 (Channel 1): 25 atoms
## Signal energy:         2746.16
## Reconstruction energy: 2594.91
## Residual energy:       231.225
## Explained energy:      91.58%

Step 3 - Time-frequency map

It is now time to generate the final time-frequency (T-F) map for the selected channel. To display the T–F map, you can also use plot.mp(), the S3 method for the generic plot() function. This function requires an object of class mp, such as an object returned by mp_omp_execute() or empi_execute().

plot(fit_mp)

By comparing the two signal waveforms below the T-F map, it can be seen that the original signal and the reconstructed signal differ only minimally. Also, basic signal parameters are displayed, along with information about the number of atoms into which the input signal was decomposed. Additionally, the energy of the input signal and the reconstructed signal (from the atoms) is calculated. The results show that 91.58% of the original signal’s energy is “explained” by the generated atoms. Naturally, increasing the number of generated atoms will likely bring the energy of the reconstructed signal closer to 100%.

Alternatively, you can use the tf_map() function to display a time-frequency map. The function provides greater control over the visualization process and offers additional functionality, such as saving time-frequency maps to a file. See the function documentation for details.

In the below plot, the centers of the atoms (in terms of time and frequency coordinates) are marked with the numbers of successive atoms, sorted from highest to lowest energy.

out <- tf_map(
  x = fit_mp,
  channel = 1,
  freq_divide = 4,
  atom_centers = "numbers"
)

Step 4 - Optional EMPI-based decomposition

The same signal can also be decomposed using the optional external Enhanced Matching Pursuit Implementation (EMPI) backend. Note that EMPI must be installed separately before empi_execute() can be used.

Important note: The code in the chunk below has been commented out because CRAN’s verification rules prohibit automatic binary downloads. empi_execute() function requires that EMPI is installed, otherwise it terminates with an error message. Therefore, this function cannot be executed here. This is why the output object from the empi_execute() has been generated in advance and included in the package as rds file. In the next chunk, the plot() and tf_map() functions use this file as input.

Notice the empi_options parameter in the empi_execute() function. You can omit this parameter, and the EMPI program will run with the default values set in the function ("-o local --gabor -i 50"). It is also worth noting that the EMPI program offers a wide range of configuration options. Details can be found in the README.md file located in the directory where the EMPI program was installed. In our example, parameters were set to instruct the program to find 25 atoms.

# sample1_empi_out <- empi_execute(
#   signal = signal,
#   empi_options = "-o local --gabor -i 25",
# )

# saveRDS(sample1_empi_out, file = "sample1.rds")

Now, let’s load a previously generated result (stored in the sample1.rds file).

file <- system.file("extdata", "sample1.rds", package = "MatchingPursuit")
sample1_empi_out <- readRDS(file = file)

plot(sample1_empi_out)

The T-F map is similar to the previous one, although some differences in atom selection can be observed. The reconstructed signal explains a slightly larger proportion of the original signal energy, reaching 92.69%.

Back to menu

6 Electroencephalogram (EEG) analysis

In this section, we demonstrate how to analyze electroencephalography (EEG) signals using the Matching Pursuit algorithm. The package provides a dedicated function for reading files in EDF and EDF+ (European Data Format). It also supports three types of EEG montages and allows for signal filtering.

Step 1 - Reading EEG data

Reading an example EEG signal (EDF file). The signal is 10 seconds long and consists of 20 channels (19 plus a special channel called the annotation channel that does not contain EEG signal data). The sampling rate is 256 Hz for each channel with EEG data. The channels have standard names.

file <- system.file("extdata", "EEG.edf", package = "MatchingPursuit")

# Read signal parameters and display them in a tabular form.
eeg <- read_edf_signals(file)
signal_eeg <- eeg$signal
sampling_frequency <- eeg$sampling_frequency
time <- seq(0, nrow(signal_eeg) - 1) / sampling_frequency

Step 2 - Filtering

EEG signals are rarely analyzed without prior filtering. Using the design_filters() function, you can define the filter parameters and then apply the filter to the signal. The filter parameters listed below use typical values recommended in the literature for EEG signal analysis.

# Filter parameters that will be used (quite typical in filtering EEG signals).
fc <- design_filters(
   sampling_frequency = sampling_frequency,
   notch = c(49, 51),
   lowpass = 40,
   highpass = 1,
)

# Filtering input signals.
signal_eeg_f <- signal_eeg

for (m in 1:ncol(signal_eeg_f)) {
  signal_eeg_f[, m] = signal::filtfilt(fc$notch, signal_eeg[, m])      # 50Hz notch filter
  signal_eeg_f[, m] = signal::filtfilt(fc$lowpass, signal_eeg_f[, m])  # Low pass IIR Butterworth
  signal_eeg_f[, m] = signal::filtfilt(fc$highpass, signal_eeg_f[, m]) # High pass IIR Butterwoth
}

Step 3 - Resampling

Sometimes it is necessary to change the sampling frequency (increase — upsampling or decrease — downsampling). In the example below, the original sampling frequency is reduced from 256 Hz to 128 Hz.

signal_eeg_f_r <- resample_signal(signal = signal_eeg_f, p = 1, q = 2)
time_128 <- seq(0, nrow(signal_eeg_f_r) - 1) / (sampling_frequency / 2)
sampling_frequency_r <- 128

The effect of the preprocessing (filtering and resampling) is illustrated below. Although filtering removes baseline drift and high-frequency noise and downsampling reduces the sampling frequency, the overall morphology of the EEG waveform is well preserved.

Step 4 - Montage construction

A bipolar montage is created (the classical double banana montage), where each channel compares two adjacent electrodes. In the first step, you define the pairs of electrodes to be connected using the pairs list. In the second step, the eeg_montage() function generates the required montage.

# Pairs of signals for bipolar montage (so called "double banana").
pairs <- list(
  c("Fp2", "F4"), c("F4", "C4"), c("C4", "P4"), c("P4", "O2"), c("Fp1", "F3"), c("F3", "C3"),  
  c("C3", "P3"), c("P3", "O1"), c("Fp2", "F8"), c("F8", "T4"), c("T4", "T6"), c("T6", "O2"),
  c("Fp1", "F7"), c("F7", "T3"), c("T3", "T5"), c("T5", "O1"), c("Fz", "Cz"), c("Cz", "Pz")
)

# Make the bipolar montage.
signal_eeg_f_r_m <- eeg_montage(
  signal_eeg_f_r,
  montage_type = c("bipolar"),
  bipolar_pairs = pairs
)

# Original signal (first 6 rows, first 6 channels).
signal_eeg_f_r[1:6, 1:6]
##            Fp1         Fp2           F3           F4           F7           F8
## 1 -0.009169519 -0.00948931 -0.007902186 -0.009404775 -0.004150085 -0.006489879
## 2 -0.007879380 -0.02545971 -0.017611303 -0.019963222 -0.007967950 -0.027958014
## 3  0.078689947  0.17633851  0.135141209  0.153953511  0.073793375  0.189075962
## 4 -4.405347474 -5.25777306 -4.292400132 -5.062491209 -2.274244020 -4.099115184
## 5 -7.123860061 -9.75573535 -7.216335597 -8.494470758 -3.112619764 -7.582372940
## 6 -4.904350386 -7.37213153 -4.761021228 -5.880097268 -1.128596436 -5.172483701

# Signal after banana montage (first 6 rows, first 6 channels).
signal_eeg_f_r_m[1:6, 1:6]
##             Fp2_F4        F4_C4        C4_P4         P4_O2       Fp1_F3         F3_C3
## [1,] -8.453465e-05 -0.009038717 -0.003565737  0.0001613474 -0.001267334 -0.0003177297
## [2,] -5.496491e-03  0.030559501  0.005492579  0.0012699229  0.009731924 -0.0059268445
## [3,]  2.238500e-02 -0.150521483 -0.025526483 -0.0073991820 -0.056451262  0.0364474510
## [4,] -1.952819e-01 -2.676997475 -1.303608531  0.1215588493 -0.112947342 -0.4015900412
## [5,] -1.261265e+00 -2.573360624 -2.040162295  0.2925304240  0.092475536 -0.8818594888
## [6,] -1.492034e+00 -2.397153899 -1.985208885  0.2483389685 -0.143329158 -0.6516086820

Step 5 - MP decomposition

Important note: The code below has been commented out. See the explanation given here.

# The empi_options parameter is NULL, so the EMPI program is 
# run with the parameters "-o local --gabor -i 50"

# # To make the RDS file smaller (CRAN requirement), we select only one channel.
# sig <- as_sig(signal_eeg_f_r_m[, 2], sampling_frequency_r)

# eeg_empi_out <- empi_execute (
#   signal = sig,
#   empi_options = NULL
# )
# 
# saveRDS(eeg_empi_out, file = "eeg_empi_out.rds")

Step 6 - Time-frequency map

Generating the final time-frequency map for the selected channel (Fp2_F4). The centers of atoms (in the sense of time and frequency coordinates) are now marked with white crosses.

Comparing the two signal waveforms under the T-F map, it can be seen that the original and reconstructed signals differ only minimally.

Below the plot, basic signal parameters are displayed, along with information about the number of atoms into which the input signal was decomposed.

Additionally, the energy of the input signal and the reconstructed signal (from the generated atoms) is calculated. The results show that nearly all of the energy of the original signal (96.18%) is “explained” by the generated atoms.

file <- system.file("extdata", "eeg_empi_out.rds", package = "MatchingPursuit")
eeg_empi_out <- readRDS(file = file)

plot(eeg_empi_out)

Step 7 - Displaying EEG signals using plot() (optional)

The package also provides the plot.edf() function, an S3 method for plot() function, for visualization of multichannel EEG recordings. The function accepts an object of class edf returned by read_edf_signals() and displays the selected time interval using a conventional stacked EEG layout.

Since the preprocessing functions are designed to operate on generic multichannel signal matrices, they are independent of the edf class and can be applied to signals imported from different file formats. Therefore, before using plot.edf(), the processed signals are combined with the corresponding metadata into an object of class edf.

The panel_height argument controls the vertical spacing between adjacent channels. If it is NULL, an appropriate value is determined automatically to prevent overlap between signals. The selected value is reported in the R console.

edf_processed <- structure(
  list(
    signal = as.data.frame(signal_eeg_f_r_m),
    sampling_frequency = sampling_frequency_r,
    time = time_128,
    signal_names = colnames(signal_eeg_f_r_m),
    record_name = basename(file)
  ),
  class = "edf"
)

plot(
  x = edf_processed,
  begin = 0,
  end = 10,
  panel_height = NULL,
  rainbow = FALSE,
  bg_colour = "white",
  txt_col = "blue",
  zero_line = TRUE,
  main = "EEG after filtering, resampling, and double banana montage"
)

## Actual value of 'panel_height' parameter is: 67.5

The same approach can be used to visualize the original EEG recording before filtering, resampling, and montage construction.


plot(
  x = eeg,
  begin = 0,
  end = 10,
  panel_height = NULL,
  rainbow = FALSE,
  bg_colour = "white",
  txt_col = "blue",
  zero_line = TRUE,
  main = "Original EEG before preprocessing"
)

## Actual value of 'panel_height' parameter is: 75.5

Back to menu

7 Electrocardiography (ECG) analysis

In this section, we demonstrate how to analyze electrocardiography (ECG) signals using the Matching Pursuit algorithm. The package provides a dedicated function for reading files in WFDB (WaveForm DataBase) format. Once the ECG data has been loaded, further analysis is essentially the same as demonstrated in previous chapters.

Step 1 - Reading ECG data

Reading an example ECG signal (.dat and .hea files). The signal is 10 seconds long and consists of 12 channels. The sampling rate is 100 Hz. The channels have standard names. The data comes from the repository available at PhysioNet.

file <- system.file("extdata", "00001_lr.hea", package = "MatchingPursuit")

out_ecg <- read_wfdb_signals(file)

head(out_ecg$signal)
##           I     II   III   AVR    AVL   AVF     V1     V2     V3     V4     V5     V6
## [1,] -0.119 -0.055 0.064 0.086 -0.091 0.004 -0.069 -0.031  0.000 -0.026 -0.039 -0.079
## [2,] -0.116 -0.051 0.065 0.083 -0.090 0.006 -0.064 -0.036 -0.003 -0.031 -0.034 -0.074
## [3,] -0.120 -0.044 0.076 0.082 -0.098 0.016 -0.058 -0.034 -0.010 -0.028 -0.029 -0.069
## [4,] -0.117 -0.038 0.080 0.077 -0.098 0.021 -0.050 -0.030 -0.015 -0.023 -0.022 -0.064
## [5,] -0.103 -0.031 0.072 0.066 -0.087 0.021 -0.045 -0.027 -0.020 -0.019 -0.018 -0.058
## [6,] -0.097 -0.025 0.071 0.061 -0.084 0.023 -0.036 -0.025 -0.009 -0.014 -0.012 -0.052
out_ecg$sampling_frequency
## [1] 100
out_ecg$lead_names
##  [1] "I"   "II"  "III" "AVR" "AVL" "AVF" "V1"  "V2"  "V3"  "V4"  "V5"  "V6"
out_ecg$record_name
## [1] "00001_lr"

Step 2 - MP decomposition

The input data (signal) is passed as an argument to the empi_execute() function, which generates the object of class mp containing all atom parameters.

Important note: The code below has been commented out. See the explanation given here.

# ecg_empi_out <- empi_execute(
#   signal = out_ecg
# )
# 
# saveRDS(ecg_empi_out, file = "00001_lr.rds")

Step 3 - Time-frequency map

It is now time to generate the final time-frequency (T-F) map for the selected channel.

# Read the previously generated decomposition result.
file <- system.file("extdata", "00001_lr.rds", package = "MatchingPursuit")
ecg_empi_out <- readRDS(file = file)

# Create time-frequency map based on atoms.
out <- tf_map(
  x = ecg_empi_out,
  channel = 1,
  verbose = TRUE
)
## Channel number:             1
## Total channels:             12
## Number of atoms:            50
## Sampling frequency:         100 Hz
## Epoch size (in points):     1000
## Signal length (in seconds): 10
## 
## Signal energy:              11.89
## Reconstruction energy:      11.95
## Residual energy:            0.55
## Explained_energy:           95.41%

Step 4 - ECG printout (optional)

The package also includes a function for displaying ECG signals in a layout corresponding to standard paper ECG printouts. A typical ECG paper layout was used, with a small grid of 0.04 s × 0.1 mV and a large grid of 0.20 s × 0.5 mV. To do this, you can use plot.wfdb(), the S3 method for the generic plot() function. This function requires an object of class ecg, created with read_wfdb_signals().

plot(
  x = out_ecg,
  begin = 0,
  end = 10,
  panel_height = 1,
  zero_line = FALSE,
  small_squares = TRUE
)

Back to menu

8 Sparse decomposition of a synthetic multicomponent signal

In this chapter, we present a specific data example adapted from the work of Durka (2007). The signal consists of a mixture of seven components: (a) four Gabor functions with different parameters, (b) a unit impulse, (c) a sinusoidal waveform, and (d) a chirp signal where frequency varies continuously over time.

In the T–F map, all signal components—except for the chirp—are represented clearly and accurately (i.e., blobs for Gabor functions, a horizontal line for the sine wave, and a vertical line for the unit impulse). However, the chirp signal is decomposed into several separate blobs. This behavior arises from the discrete nature of the atom dictionary used in the Matching Pursuit algorithm, which prevents a continuous representation of a signal with smoothly varying frequency. This limitation (and, in some respects, a drawback) of the Matching Pursuit algorithm should be taken into account.

Important note: The code below has been commented out. See the explanation given here.

# sig_file <- system.file("extdata", "sample2.csv", package = "MatchingPursuit")
# signal <- read_csv_signals(sig_file, col_names_in_csv = FALSE)

# sample2_empi_out <- empi_execute (
#   signal = signal
# )

# saveRDS(sample2_empi_out, file = "sample2.rds")
file <- system.file("extdata", "sample2.rds", package = "MatchingPursuit")
sample2_empi_out <- readRDS(file = file)

out <- tf_map(
  x = sample2_empi_out,
  channel = 1,
  freq_divide = 1
)

Back to menu

9 The Matching Pursuit algorithm in a nutshell

The Matching Pursuit algorithm is well-known and described in the literature. Its purpose is to approximate the analyzed signal using so-called atoms. (the text below is adapted from Kunik and Gramacki (2025)).

Given a discrete signal \(f \in \mathbb{R}^n\), and a possibly overcomplete dictionary \(D =\{g_{\gamma}\}_{\gamma \in \Gamma}\) of normalized atoms \(\|g_{\gamma}\|=1\) Matching Pursuit finds a sparse signal representation

\[ f \approx \sum_{n = 0}^{N-1} a_n g_{\gamma_n}, \tag{1} \] where \(a_n \in \mathbb{R}\) are the expansion coefficients, \(g_{\gamma_n} \in D\) are the selected atoms, and \(N\) denotes the number of iterations performed by the algorithm. In practice, the decomposition may terminate earlier if a predefined stopping criterion is satisfied. In most practical cases \(N \ll |D|\), where \(|D|\) denotes the number of atoms in the dictionary. Also, \(g_{\gamma}\) is the dictionary atom indexed by \(\gamma\) and \(\Gamma\) is the corresponding index set.

In the ideal case, the linear expansion (1) should include all atoms \(g_{\gamma_n}\) that represent the relevant structures of the signal \(f\). For real signals, such an ideal scenario is rarely possible, and some form of approximation is required. This task can be accomplished elegantly using the Matching Pursuit algorithm, which was first proposed by Mallat and Zhang (1993) in the context of signal processing.

Each atom \(g_{\gamma}\) is typically a time-frequency shifted, scaled version of a prototype function, such as the Gabor function (often called a Gaussian-windowed sinusoid). The dictionary is constructed to cover a wide range of time and frequency characteristics. A continuous-time real-valued Gabor function can be written as

\[ g_{\gamma}(t) = K(\gamma) e^{- \pi \left( \frac{t-\mu}{\sigma} \right) ^2} \cos(\omega (t - \mu) + \phi), \tag{2} \]

where \(\gamma = (\mu, \omega, \sigma, \phi)\) denotes a point in a four-dimensional parameter space and \(K(\gamma)\) is such that \(||g_{\gamma}|| = 1\). It is easy to see that Gabor functions are constructed by multiplying Gaussian envelopes with cosine oscillations of different frequencies \(\omega\) and phases offset \(\phi\). By multiplying these two functions, we can obtain a wide variety of shapes depending on their parameters. A few examples of Gabor function are presented in figure below (in blue). The sinusoidal plane wave (in gray) is modulated by a Gaussian envelope (in red).

The Matching Pursuit (MP) algorithm is an iterative greedy algorithm for decomposing a signal into a sparse linear combination of atoms selected from a dictionary. In each iteration, MP selects an atom \(g_{\gamma_n}\) from the dictionary \(D\) that best matches the current residual signal \(R^n\). Formally, at the beginning of the decomposition (\(n=0\)), the initial residual and signal approximation are given by

\[ R^0 = f \tag{3} \] and

\[ f^0 = 0. \tag{4} \]

For each iteration \(n = \{0,1,\ldots, N-1\}\), the atom \(g_{\gamma_n} \in D\) maximizing the absolute inner product with the current residual is selected:

\[ g_{\gamma_n} = \operatorname*{arg\,max}_{\gamma \in \Gamma} | \langle R^{n}, g_{\gamma} \rangle |. \tag{5} \] The coefficients \(a_n\) in (1) are

\[ a_n = \langle R^{n}, g_{\gamma_n} \rangle \tag{6} \]

and updated function approximation is defined as

\[ f^{n+1} = f^{n} + a_n g_{\gamma_n}. \tag{7} \]

Similarly, updated residual is defined as

\[ R^{n+1} = R^{n} - a_n g_{\gamma_n}. \tag{8} \]

Repeating this procedure yields a sequence of progressively improved signal approximations. After \(N\) iterations, the signal \(f\) is approximated as

\[ f \approx \sum_{n = 0}^{N-1} \langle R^n, g_{\gamma_n} \rangle g_{\gamma_n} = \sum_{n = 0}^{N-1} a_n g_{\gamma_n} \tag{9} \]

or equivalently

\[ f = \sum_{n = 0}^{N-1} \langle R^n, g_{\gamma_n} \rangle g_{\gamma_n} + R^{N}. \tag{10} \]

The procedure stops when \(\|R^{n+1}\|_2\) falls below a predefined threshold or when a fixed number of iterations has been reached

Finding an optimal \(N\)-term approximation over a redundant dictionary is, in general, NP-hard. MP therefore employs a greedy strategy that provides an efficient approximate solution with substantially lower computational cost. Its main advantage is the relatively simple iterative structure and comparatively low computational cost.

However, once an atom has been selected according to (5), its coefficient is not re-estimated in subsequent iterations. Consequently, the coefficients of previously selected atoms may become suboptimal when additional correlated atoms are added to the approximation.

Because the selected atoms are normalized, each MP iteration removes an amount of squared \(\ell_2\) energy equal to \(|\langle R^n,g_{\gamma_n}\rangle|^2\) from the current residual. Consequently, the signal energy can be decomposed as

\[ ||f||^2_2 = \sum_{n = 0}^{N-1} |\langle R^n, g_{\gamma_n} \rangle |^2 + ||R^{N}||^2_2. \tag{11} \]

This identity is sometimes referred to as the energy conservation property of Matching Pursuit.

Back to menu

10 The Orthogonal Matching Pursuit algorithm in a nutshell

The package also provides an implementation of the Orthogonal Matching Pursuit (OMP) algorithm. OMP algorithm is closely related to MP, but differs in how approximation coefficients are estimated.

In the classical MP algorithm, after an atom has been selected, only the residual signal is updated (8). The previously selected atoms and their coefficients remain unchanged and are not re-estimated in subsequent iterations.

OMP addresses this limitation by recomputing the coefficients of all selected atoms after each atom selection. As a result, the new residual is orthogonal to the subspace spanned by all selected atoms.

At each iteration, OMP selects the next atom \(g_{\gamma_n}\) by maximizing its correlation with the current residual, as in (5). The crucial difference from MP lies in the subsequent estimation of the coefficients. After the new atom has been added to the selected set, the coefficients are obtained by solving a least-squares problem

\[ \mathbf{a}^{(n)} = \operatorname*{arg\,min}_{\mathbf{c}} \left\| f - D_n \mathbf{c} \right\|_2^2, \tag{12} \]

where \(\mathbf{c}\) is the vector of coefficients associated with the selected atoms, and \(D_n\) denotes the matrix whose columns are the atoms selected up to and including iteration \(n\):

\[ D_n = [g_{\gamma_0},g_{\gamma_1},\ldots,g_{\gamma_n}]. \tag{13} \]

Assuming that \(D_n\) has full column rank, the least-squares solution is

\[ \mathbf{a}^{(n)} = (D_n^T D_n)^{-1}D_n^T f. \tag{14} \] In the OMP implementation, this least-squares problem is solved efficiently using an incremental Cholesky factorization. Rather than recomputing the solution from scratch after each newly selected atom, the Cholesky factor is updated as the active dictionary grows, and the coefficients are obtained by forward and backward substitution.

This coefficient re-estimation is the core difference between OMP and MP. Instead of simply adding the contribution of the newly selected atom, as in (5), OMP projects the original signal \(f\) orthogonally onto the subspace spanned by all currently selected atoms.

The new residual is then calculated as

\[ R^{n+1} = f - D_n \mathbf{a}^{(n)}. \tag{15} \]

Because the residual is orthogonal to the span of the selected atoms, its correlation with every selected atom is zero. Consequently, a previously selected atom cannot be selected again.

11 Comparison of MP and OMP

Both MP and OMP use a greedy correlation-based strategy to select atoms from a dictionary. The main difference between the two algorithms lies in the way the coefficients associated with the selected atoms are estimated and the residual is updated. In MP, only the coefficient of the newly selected atom is estimated at each iteration, whereas OMP jointly re-estimates the coefficients of all atoms selected so far.

The main advantage of OMP is that, after solving the least-squares problem, the residual is orthogonal to the subspace spanned by all selected atoms. Consequently, the coefficients of previously selected atoms can be re-estimated when new atoms are added. In contrast, MP does not revisit previously estimated coefficients, which makes it computationally simpler but may result in a less accurate approximation for a given number of selected atoms.

The choice between MP and OMP therefore represents a trade-off between computational efficiency and approximation accuracy. MP may be preferable when computational simplicity and speed are important, whereas OMP generally provides a more accurate reconstruction for a given number of selected atoms and may require fewer atoms to achieve a prescribed reconstruction accuracy. This improvement comes at the cost of higher computational complexity, since the coefficients of all selected atoms must be jointly updated after each iteration.

Back to menu

12 Pure R implementation of the MP and OMP algorithms

Core MP and OMP functions

The native R implementation is built around two core functions, mp_core() and omp_core(), which implement the Matching Pursuit and Orthogonal Matching Pursuit algorithms, respectively. These functions form the general-purpose sparse decomposition layer of the package and are independent of any particular atom family or dictionary construction method.

Both functions operate on user-defined dictionaries represented as numeric matrices, with candidate atoms stored in columns. This design allows the core algorithms to be used not only within the Gabor-based workflow provided by the package, but also with arbitrary dictionaries constructed independently by the user.

To demonstrate that the core solvers are independent of the Gabor-specific workflow, the following examples use a simple user-defined matrix dictionary.

dictionary <- matrix(
  c(
     0.098, -0.308, -0.342, -0.894,
     0.928,  0.674, -0.270,  0.283,
     0.145, -0.326,  0.810, -0.114,
    -0.170, -0.466,  0.377, -0.036,
     0.281, -0.357,  0.105, -0.327
  ),
  nrow = 5,
  byrow = TRUE
)

# Although mp_core() and omp_core() normalize dictionary atoms internally, 
# the atoms are normalized explicitly here so that the coefficients used to 
# construct the signal have a direct interpretation.
dictionary <- sweep(dictionary, 2, sqrt(colSums(dictionary^2)), "/")

# Signal constructed from two correlated dictionary atoms
signal <- 1.5 * dictionary[, 1] - 0.8 * dictionary[, 2]

After defining the dictionary and signal, the MP and OMP decompositions can be performed.

fit_mp <- mp_core(
  dictionary = dictionary,
  signal = signal,
  n_nonzero_coefs = 2
)

fit_omp <- omp_core(
  dictionary = dictionary,
  signal = signal,
  n_nonzero_coefs = 2
)

The decomposition results can then be compared in terms of the selected atoms, estimated coefficients, and residual error.

fit_mp$support
## [1] 1 2
fit_omp$support
## [1] 1 2

fit_mp$coefs
## [1]  1.0783989 -0.5778156
fit_omp$coefs
## [1]  1.5 -0.8

sum(fit_mp$residual^2)
## [1] 0.1283816
sum(fit_omp$residual^2)
## [1] 4.622232e-31

The two examples illustrate the different behavior of MP and OMP when dictionary atoms are correlated. Both algorithms identify the same relevant atoms, but OMP re-estimates all active coefficients after each new atom is added. As a result, OMP recovers the original coefficients more accurately and produces a smaller residual error. In contrast, MP updates the approximation sequentially and does not revise previously estimated coefficients, which can lead to a less accurate reconstruction for the same number of selected atoms.

High-level decomposition interface

The mp_omp_execute() function provides a convenient high-level interface for Gabor-based decomposition. After loading the signal, the user only needs to select the decomposition mode and specify the main analysis parameters.

sig_file <- system.file("extdata", "sample1.csv", package = "MatchingPursuit")
signal <- read_csv_signals(sig_file, col_names_in_csv = FALSE)

The decomposition can then be performed using either OMP or classical MP. The returned object is ready for direct visualization with plot().

fit <- mp_omp_execute(
   mode = 'omp',          # or "mp" for classical Matching Pursuit
   signal = signal,
   n_nonzero_coefs = 50,
   topk = 10000,
   verbose = FALSE
 )
## mp_omp_execute(): method = "omp", channel = 1 Successfully processed.

plot(fit)

Gabor dictionary preparation

For educational and experimental purposes, the individual steps performed internally by the mp_omp_execute() function can also be carried out explicitly. These steps include generating an XML file containing the dictionary parameters using generate_xml_dict(), reading this file using read_gabor_dict(), and preselecting candidate atoms using topk_gabor_atoms().

sampling_frequency <- signal$sampling_frequency
signal_length <- nrow(signal$signal)
duration <- nrow(signal$signal) / sampling_frequency

xml_file <- tempfile(fileext = ".xml")

dict <- generate_xml_dict(
   N = signal_length,
   file = xml_file
)

Reading the Gabor dictionary

The read_gabor_dict() function reads the XML-based dictionary specification and returns the corresponding candidate Gabor atom definitions. Depending on the specification, the resulting set may contain tens or hundreds of thousands of candidate atoms. For format details, see the section XML-based atom dictionary specification.

atoms_dict <- read_gabor_dict(
  xml_file = xml_file, 
  sampling_frequency = sampling_frequency, 
  duration = duration, 
  full_atoms_in_signal = FALSE,
  verbose = FALSE
)

dim(atoms_dict)
## [1] 322154      7
head(atoms_dict)
##      block time_sample   time_sec freq_bin freq_hz window_len fft_size
## [1,]     1          -8 -0.0078125        0       0         17       64
## [2,]     1          -8 -0.0078125        1      16         17       64
## [3,]     1          -8 -0.0078125        2      32         17       64
## [4,]     1          -8 -0.0078125        3      48         17       64
## [5,]     1          -8 -0.0078125        4      64         17       64
## [6,]     1          -8 -0.0078125        5      80         17       64

Signal-dependent Gabor atom selection

The topk_gabor_atoms() function evaluates all dictionary atoms using phase-invariant complex projections and selects the atoms with the highest similarities to the analysed signal.

This step substantially reduces the size of the optimization problem. Only the selected atoms are retained and converted into real-valued Gabor atoms with optimal phase estimates.

dict_topk <- topk_gabor_atoms(
  atoms_dict = atoms_dict,
  signal = signal,
  topk = 10000,
  verbose = TRUE
)
## topk_gabor_atoms(), step 1, calculating 322154 inner products...
## topk_gabor_atoms(), step 1 finished.
## topk_gabor_atoms(), step 2 finished.
## 10000 out of 322154 atoms selected successfully.

Validation using a synthetic sparse signal

To validate the behavior of the general sparse decomposition solvers, we use a synthetic signal with a known sparse representation. The procedure applies to both MP and OMP; here, OMP is used as a representative example.

First we construct the dictionary. The dictionary contains sinusoidal atoms at several frequencies, while only three atoms are used to generate the signal. This allows the selected atoms, estimated coefficients, and reconstruction error to be compared directly with the known ground truth.

Each column of the matrix represents one dictionary atom. The atoms are normalized to unit L2 norm so that the coefficients used to generate the signal can be compared directly with the coefficients estimated by OMP.

set.seed(1)

N <- 256
t <- (0:(N - 1)) / N

dictionary <- cbind(
  sin(2 * pi * 3  * t),  cos(2 * pi * 3  * t),  sin(2 * pi * 7  * t),
  cos(2 * pi * 7  * t),  sin(2 * pi * 12 * t),  cos(2 * pi * 12 * t),
  sin(2 * pi * 20 * t),  cos(2 * pi * 20 * t)
)

# Unit-L2 normalization of dictionary atoms.
dictionary <- sweep(dictionary, 2, sqrt(colSums(dictionary^2)), "/")

colnames(dictionary) <- c(
  "sin_3", "cos_3",  "sin_7", "cos_7",  
  "sin_12", "cos_12",  "sin_20", "cos_20"
)

Second, we generate a signal with known sparse representation. The signal is generated using only three non-zero coefficients. Therefore, the true support consists of the atoms cos_3, sin_12, and cos_20.

true_coef <- c(0, 1.5, 0, 0, -0.8, 0, 0, 0.5)
true_atoms <- c("cos_3", "sin_12", "cos_20")

signal <- as.vector(dictionary %*% true_coef)

Now, we can do the OMP decomposition. Because the signal was generated exactly from three dictionary atoms, OMP should recover the same support and the corresponding coefficients.

out <- omp_core(
  dictionary = dictionary,
  signal = signal,
  n_nonzero_coefs = 3
)

colnames(out$selected_atoms)
## [1] "cos_3"  "sin_12" "cos_20"

out$coefs
## [1]  1.5 -0.8  0.5

For the noise-free signal, the selected support should match the known support and the relative reconstruction error should be close to zero. The reconstruction error is at the level of numerical precision, indicating exact recovery of the noise-free signal.

setequal(colnames(out$selected_atoms), true_atoms)
## [1] TRUE

relative_error <- sqrt(sum((signal - out$reconstruction)^2)) / sqrt(sum(signal^2))
relative_error
## [1] 7.733811e-16

Now, we can examine the robustness to additive noise. The noisy example demonstrates whether the same sparse support can still be identified when the observed signal is perturbed. Since the clean signal is known in this synthetic example, the reconstructed signal can also be compared directly with the noise-free ground truth.

signal_noisy <- signal + rnorm(N, sd = 0.1)

out_noisy <- omp_core(
  dictionary = dictionary,
  signal = signal_noisy,
  n_nonzero_coefs = 3
)

colnames(out_noisy$selected_atoms)
## [1] "cos_3"  "sin_12" "cos_20"

out_noisy$coefs
## [1]  1.6412326 -0.7505156  0.3159247

setequal(colnames(out_noisy$selected_atoms), true_atoms)
## [1] TRUE

Despite additive Gaussian noise, OMP recovered the same three-atom support, while the reconstruction remained close to the known clean signal.

relative_residual_error <- sqrt(
  sum((signal_noisy - out_noisy$reconstruction)^2)
  ) / sqrt(sum(signal_noisy^2))

relative_residual_error
## [1] 0.6353605

relative_clean_error <- sqrt(
  sum((signal - out_noisy$reconstruction)^2)
  ) / sqrt(sum(signal^2))

relative_clean_error
## [1] 0.1338779

Back to menu

Relationship between MP-R, OMP-R, and EMPI backends

MP-R and OMP-R implement Matching Pursuit and Orthogonal Matching Pursuit, respectively. EMPI is a high-performance C++ backend specialized in Gabor-based Matching Pursuit and is designed for substantially faster decomposition of large dictionaries.

The diagram below summarizes the three available decomposition workflows and their relationship within the package. Blue boxes denote R functions, whereas peach boxes represent workflow components, inputs, or returned objects. Panel (b) shows the main steps performed internally by mp_omp_execute().

The general matrix-based MP/OMP workflow operates directly on a user-defined numeric dictionary and input signal through mp_core() or omp_core(). It is independent of the Gabor-specific dictionary construction utilities and can therefore be used with arbitrary matrix dictionaries. These functions return low-level decomposition results, giving the user direct control over the dictionary and the decomposition procedure.

The native R Gabor-based MP/OMP workflow, implemented by mp_omp_execute(), provides a higher-level interface for Gabor-based MP and OMP decomposition. It integrates dictionary specification or generation, dictionary reading, channel-specific atom preselection, decomposition with mp_core() or omp_core(), and aggregation of the results into an object of class "mp". If no XML dictionary specification is supplied, generate_xml_dict() creates one internally; otherwise, the user-provided XML file is used. The specification is processed by read_gabor_dict(), after which topk_gabor_atoms() selects candidate Gabor atoms for each signal channel.

The external EMPI-based MP workflow provides an alternative MP implementation through empi_execute(). In this case, decomposition is performed by the external high-performance C++ EMPI backend rather than by the native R MP/OMP core functions. The wrapper integrates the external decomposition results into the same package-level "mp" representation, allowing them to be handled using the same downstream visualization functions, including plot() and tf_map().

Thus, the three workflows differ mainly in the dictionary representation and decomposition backend: direct matrix-based MP/OMP for arbitrary user-defined dictionaries, the integrated native Gabor MP/OMP workflow, and the external EMPI-based Gabor MP workflow. The latter two return a common "mp" object, facilitating consistent visualization and interpretation within the package.

Back to menu

13 XML-based atom dictionary specification

The XML file encodes the structure of a dictionary of basis functions (Gabor atoms), including window lengths, window shifts, and frequency grids used during signal analysis. A simple example illustrates how atom parameters are encoded in the XML file. Consider the following block:

<block>
<param name="windowLen" value="17"/>
<param name="windowShift" value="1"/>
<param name="fftSize" value="32"/>
</block>

Assume that the analyzed signal is sampled at \(1\;024\) Hz and has a duration of \(1\) second. We define:

Number of window positions

With a shift of one sample (\(S = 1\)), the number of possible window positions is

\[ N_{\text{windows}} = N - L + 1 = 1\;024 - 17 + 1 = 1\;008 \]

Number of frequency bins

For an FFT size of \(32\), and because the analyzed signal is real-valued, it is sufficient to consider the non-negative frequencies from 0 to the Nyquist frequency. Thus, the number of frequency bins is

\[ N_{\text{freq}} = \frac{32}{2} + 1 = 17 \] For a sampling frequency of \(1024\) Hz, the frequency resolution is

\[ \Delta f = \frac{1024}{32} = 32\ \text{Hz}. \]

Therefore, the frequencies are:

\[ 0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448, 480, 512\ \text{Hz} \]

The total number of atoms

\[ N_{\text{atoms}} = N_{\text{windows}} \times N_{\text{freq}} = 1\;008 \times 17 = 17\;136 \]

In other words, for each of the \(1\;008\) possible positions of the \(17\)-sample window, \(17\) atoms corresponding to different FFT frequencies are generated.

Multiple-block dictionaries

A practical dictionary usually consists of multiple blocks with different window lengths, shifts, and FFT sizes. The calculations for each block are analogous to those shown above. The results for the example dictionary are summarized in the table below. As above, the sampling frequency is (\(f_s = 1\;024\ \text{Hz}\)) and the signal duration is (\(T = 1\ \text{sec.}\)), giving the number of samples (\(N = 1\;024\)). In total, the XML file defines parameters for \(185\;365\) atoms.

<?xml version="1.0" encoding="ISO-8859-1"?>
<dict>
<block>
<param name="windowLen" value="17"/>
<param name="windowShift" value="1"/>
<param name="fftSize" value="32"/>
</block>
<block>
<param name="windowLen" value="27"/>
<param name="windowShift" value="1"/>
<param name="fftSize" value="64"/>
</block>
<block>
<param name="windowLen" value="41"/>
<param name="windowShift" value="2"/>
<param name="fftSize" value="128"/>
</block>
<block>
<param name="windowLen" value="65"/>
<param name="windowShift" value="3"/>
<param name="fftSize" value="128"/>
</block>
<block>
<param name="windowLen" value="103"/>
<param name="windowShift" value="6"/>
<param name="fftSize" value="256"/>
</block>
<block>
<param name="windowLen" value="163"/>
<param name="windowShift" value="9"/>
<param name="fftSize" value="512"/>
</block>
<block>
<param name="windowLen" value="259"/>
<param name="windowShift" value="15"/>
<param name="fftSize" value="512"/>
</block>
<block>
<param name="windowLen" value="409"/>
<param name="windowShift" value="24"/>
<param name="fftSize" value="1024"/>
</block>
<block>
<param name="windowLen" value="647"/>
<param name="windowShift" value="39"/>
<param name="fftSize" value="2048"/>
</block>
<block>
<param name="windowLen" value="1023"/>
<param name="windowShift" value="61"/>
<param name="fftSize" value="2048"/>
</block>
<block>
<param name="windowLen" value="1619"/>
<param name="windowShift" value="97"/>
<param name="fftSize" value="4096"/>
</block>
</dict>
windowLen windowShift fftSize number of windows number of frequencies number of atoms
17 1 32 1008 17 17 136
27 1 64 998 33 32 934
41 2 128 492 65 31 980
65 3 128 320 65 20 800
103 6 256 154 129 19 866
163 9 512 96 257 24 672
259 15 512 52 257 13 364
409 24 1024 26 513 13 338
647 39 2048 10 1025 10 250
1023 61 2048 1 1025 1 025
1619 97 4096 0 2049 0

An interesting special case are the two last blocks:

<block>
<param name="windowLen" value="1023"/>
<param name="windowShift" value="61"/>
<param name="fftSize" value="2048"/>
</block>
<block>
<param name="windowLen" value="1619"/>
<param name="windowShift" value="97"/>
<param name="fftSize" value="4096"/>
</block>

The last two blocks illustrate a boundary case resulting from the finite signal length. For windowLen = \(1\;023\), the window can be placed only once within the 1024-sample signal, resulting in \(1\;025\) atoms corresponding to the available frequency bins. For the final block (windowLen = \(1\;619\)), the window length exceeds the signal length, so no fully contained atom can be generated. This behavior follows directly from the full_atoms_in_signal = TRUE convention, under which only atoms whose complete support lies within the signal boundaries are included.

A regular pattern can be observed in the dictionary construction: the ratio of windowShift to windowLen is nearly constant across all blocks (approximately 0.06). This means that consecutive windows overlap by about 94%.

Furthermore, as the window length increases, the frequency resolution also increases. This is a direct consequence of the time–frequency uncertainty principle - the longer the window, the better the ability to distinguish closely spaced frequencies, but at the expense of time localization.

Therefore, it is beneficial to analyze a larger number of frequency components for long windows, as they provide meaningful frequency information. In contrast, for short windows, a very dense frequency grid would not contribute much additional information because the frequency resolution is fundamentally limited by the window length itself.

In summary, the dictionary provides an approximately uniform coverage of the time–frequency plane - short windows are associated with many temporal positions and relatively few frequency bins, whereas long windows have fewer time positions but a much denser frequency sampling.

Back to menu

14 XML-based atom dictionary generator

The package includes a utility for generating XML-based atom dictionaries for MPTK-like sparse decomposition algorithms. The generator creates multiscale Gabor dictionaries with logarithmically distributed window lengths, automatically selecting window shifts and FFT sizes for each atom scale.

The generated dictionaries support multiresolution signal analysis by combining short atoms for transient components and long atoms for slowly varying structures. The XML specification may include atom scales longer than the analyzed signal. Whether such atoms are retained during dictionary construction depends on the full_atoms_in_signal setting. When full_atoms_in_signal = TRUE, only atoms fully contained within the signal are included; otherwise, longer atoms may be handled using zero-padding.

# Generate a dictionary for a 512-sample signal
xml_file <- tempfile(fileext = ".xml")

dict <- generate_xml_dict (
   N = 512,
   file = xml_file,
   max_window_length = "3N"
)

dict
##    windowLen windowShift fftSize
## 1         17           1      64
## 2         27           2      64
## 3         39           2     128
## 4         59           4     128
## 5         89           5     256
## 6        133           8     512
## 7        199          12     512
## 8        301          18    1024
## 9        449          27    1024
## 10       677          41    2048
## 11      1021          61    2048
## 12      1535          92    4096

The generated XML specification may contain atom scales longer than the analyzed signal. Whether such atoms are retained depends on the full_atoms_in_signal argument used when the dictionary specification is read.

# Read the dictionary specification and retain only atoms
# fully contained within the analyzed signal
dict_full <- read_gabor_dict(
   xml_file = xml_file,
   sampling_frequency = 256,
   duration = 2,
   full_atoms_in_signal = TRUE,
   verbose = FALSE
)

dict_padded <- read_gabor_dict(
   xml_file = xml_file,
   sampling_frequency = 256,
   duration = 2,
   full_atoms_in_signal = FALSE,
   verbose = FALSE
)

dim(dict_full)
## [1] 85137     7
dim(dict_padded)
## [1] 150558      7

With full_atoms_in_signal = TRUE, only atoms whose complete support lies within the analyzed signal are retained, resulting in \(85\;137\) candidate atoms. When full_atoms_in_signal = FALSE, atoms extending beyond the signal boundaries are also allowed through zero-padding, increasing the dictionary size to \(150\;558\) atoms. The difference is particularly pronounced for short signals, for which long atom scales have relatively few positions that are fully contained within the signal boundaries.

Back to menu

Bibliography

Durka, P. 2007. Matching Pursuit and Unification in EEG Analysis. Engineering in Medicine and Biology. Boston: Artech House.
Elad, M. 2010. Sparse and Redundant Representations: From Theory to Applications in Signal and Image Processing. Springer. https://doi.org/10.1007/978-1-4419-7011-4.
Kunik, M., and A. Gramacki. 2025. Deep Learning Epileptic Seizure Detection based on the Matching Pursuit Algorithm and its Time–Frequency Graphical Representation.” International Journal of Applied Mathematics and Computer Science 35 (4): 617–30. https://doi.org/10.61822/amcs-2025-0044.
Mallat, S., and Z. Zhang. 1993. Matching Pursuits With Time-Frequency Dictionaries.” IEEE Transactions on Signal Processing 41 (12): 3397–3415. https://doi.org/10.1109/78.258082.
Pati, Y. C., R. Rezaiifar, and P. S. Krishnaprasad. 1993. Orthogonal Matching Pursuit: Recursive Function Approximation with Applications to Wavelet Decomposition.” In Proceedings of the 27th Asilomar Conference on Signals, Systems and Computers, 40–44. https://doi.org/10.1109/ACSSC.1993.342465.
Różański, P. T. 2024. empi: GPU-Accelerated Matching Pursuit with Continuous Dictionaries.” ACM Transactions on Mathematical Software 50 (3): 1–17. https://doi.org/10.1145/3674832.