---
title: "5. Extensions and advanced usage"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{5. Extensions and advanced usage}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r, include = FALSE}
knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>"
)
```

This vignette explores the extensibility of `rLifting` beyond the built-in
wavelets and standard denoising pipelines. The package is designed around
the Lifting Scheme framework (Sweldens, 1996), which represents wavelet
transforms as compositions of simple predict (P) and update (U) steps.
This modularity makes it straightforward to define custom wavelets, tune
thresholding parameters, and build low-level processing pipelines.

## 1. Built-in wavelet families

`rLifting` ships with six built-in lifting schemes:

| Name      | Alias       | Steps | Support | Properties                       |
|:----------|:------------|:-----:|:-------:|:---------------------------------|
| `haar`    |             | 2     | 2       | Orthogonal, 1 vanishing moment   |
| `db2`     |             | 3     | 4       | Orthogonal, 2 vanishing moments  |
| `cdf53`   | `bior2.2`   | 2     | 6       | Biorthogonal, linear prediction  |
| `cdf97`   | `bior4.4`   | 4     | 10      | Biorthogonal, used in JPEG 2000  |
| `dd4`     | `interp4`   | 2     | 8       | Interpolating, 4th-order         |
| `lazy`    |             | 0     | 1       | Identity (polyphase split only)  |

Each is constructed via `lifting_scheme()`:

```{r builtin}
library(rLifting)

if (!requireNamespace("ggplot2", quietly = TRUE)) {
  knitr::opts_chunk$set(eval = FALSE)
  message("Required package 'ggplot2' is missing. Vignette code will not run.")
} else {
  library(ggplot2)
}

haar   = lifting_scheme("haar")
cdf97  = lifting_scheme("cdf97")
print(haar)
print(cdf97)
```

The CDF 9/7 wavelet has four lifting steps (two predict, two update),
making it significantly smoother than Haar but with wider support. This
trade-off between smoothness and locality is fundamental in wavelet
analysis.

## 2. Defining custom wavelets

One of the strengths of the Lifting Scheme is its modularity. Unlike
convolution-based wavelet implementations, lifting allows you to construct
wavelets by composing simple predict (P) and update (U) steps. Each step
is a local operation defined by:

- **type**: `"predict"` or `"update"`
- **coeffs**: a numeric vector of filter coefficients
- **position**: `"center"` (symmetric), `"left"` (causal), or `"right"`
  (anti-causal); or an explicit `start_idx`

### Example: recreating CDF 5/3 manually

The CDF 5/3 (linear) wavelet uses linear interpolation for prediction
and averaging for update:

$$d[i] \mathbin{-}= 0.5 \cdot s[i] + 0.5 \cdot s[i+1]$$

$$s[i] \mathbin{+}= 0.25 \cdot d[i-1] + 0.25 \cdot d[i]$$

The coefficients and index offsets must match the package's internal
convention (Matlab-style factorization from Daubechies and Sweldens, 1998):

```{r custom_define}
# Step 1: Predict (P) — coeffs = c(0.5, 0.5), starting at index 0
p_step = lift_step(
  type = "predict",
  coeffs = c(0.5, 0.5),
  start_idx = 0
)

# Step 2: Update (U) — coeffs = c(0.25, 0.25), starting at index -1
u_step = lift_step(
  type = "update",
  coeffs = c(0.25, 0.25),
  start_idx = -1
)

# Combine into a scheme
my_linear = custom_wavelet(
  name = "LinearManual",
  steps = list(p_step, u_step),
  norm = c(sqrt(2), 1/sqrt(2))  # biorthogonal normalization
)

print(my_linear)
```

### Using the custom wavelet

Once defined, a custom wavelet works seamlessly with every function in
the package — `lwt`, `ilwt`, `denoise_signal_offline`,
`denoise_signal_causal`, and `new_wavelet_stream`:

```{r custom_use}
# A linear ramp: detail coefficients should be zero
x = 1:16
fwd = lwt(x, my_linear, levels = floor(log2(length(x))))

# Detail at level 1: all zeros for a linear signal
print(round(fwd$coeffs$d1, 10))

# Perfect reconstruction
rec = ilwt(fwd)
all.equal(x, rec)
```

The detail coefficients at level 1 are exactly zero because the CDF 5/3
wavelet has two vanishing moments — it annihilates polynomials up to
degree 1 (i.e., ramps). This property is directly related to the
predict step using linear interpolation.

## 3. Diagnosing a wavelet

`rLifting` includes a full diagnostic suite that verifies the mathematical
properties of any lifting scheme. This is particularly useful when defining
custom wavelets, to catch errors in the coefficient specification.

```{r diagnose}
diag = diagnose_wavelet(
  my_linear,
  config = list(
    is_ortho  = FALSE,   # CDF 5/3 is biorthogonal, not orthogonal
    vm_degrees = c(0, 1), # test vanishing moments for degree 0 and 1
    max_taps  = 5         # expected filter support width
  ),
  plot = FALSE  # suppress basis plot in vignette
)
```

The individual diagnostic tests can also be called directly:

| Function                          | What it tests                              |
|:----------------------------------|:-------------------------------------------|
| `validate_perfect_reconstruction` | $\text{ilwt}(\text{lwt}(x)) = x$           |
| `validate_vanishing_moments`      | Detail coefficients vanish for polynomials  |
| `validate_orthogonality`          | Energy preservation (Parseval's theorem)    |
| `validate_compact_support`        | Impulse response has finite support         |
| `validate_shift_sensitivity`      | Quantifies shift-variance                  |

```{r individual_tests}
# Test perfect reconstruction separately
pr = validate_perfect_reconstruction(my_linear)
cat("Perfect reconstruction:", pr$passed, "\n")
cat("Max error:", format(pr$max_error, scientific = TRUE), "\n")
```

## 4. Thresholding methods

The package provides three thresholding operators, all accelerated in C++:

- `threshold_hard(x, lambda)`:  keeps coefficients above $\lambda$,
  zeroes the rest ("keep or kill"). Best for preserving peak amplitudes,
  but can leave noise artifacts near the threshold.
- `threshold_soft(x, lambda)`: shrinks all coefficients towards zero
  by $\lambda$. Smoother output, but introduces systematic amplitude bias.
- `threshold_semisoft(x, lambda)`: hyperbolic shrinkage, a compromise.
  For $|x| > \lambda$: $\text{sign}(x)\sqrt{x^2 - \lambda^2}$.

A unified wrapper `threshold(x, lambda, method)` dispatches to the
appropriate function.

```{r threshold_demo, fig.width=6, fig.height=4}
x = seq(-3, 3, length.out = 201)
lambda = 1.0

df_th = data.frame(
  x = rep(x, 3),
  y = c(threshold(x, lambda, "hard"),
        threshold(x, lambda, "soft"),
        threshold(x, lambda, "semisoft")),
  Method = rep(c("Hard", "Soft", "Semisoft"), each = length(x))
)

library(ggplot2)
ggplot(df_th, aes(x = x, y = y, colour = Method)) +
  geom_line(linewidth = 0.8) +
  geom_abline(slope = 1, intercept = 0, linetype = "dotted", colour = "grey60") +
  geom_vline(xintercept = c(-lambda, lambda), linetype = "dashed", colour = "grey40") +
  labs(title = "Thresholding functions",
       subtitle = expression(paste(lambda, " = 1")),
       x = "Input coefficient", y = "Output coefficient") +
  theme_minimal() +
  scale_colour_manual(values = c("Hard" = "#E69F00", "Soft" = "#56B4E9",
                                  "Semisoft" = "#009E73"))
```

## 5. Adaptive threshold estimation

Instead of requiring the user to specify a threshold manually, `rLifting`
can estimate it adaptively from the wavelet coefficients. The
`compute_adaptive_threshold` function implements the recursive formula
from Liu et al. (2014), where the threshold at each decomposition level
is derived from the noise variance estimated via the median absolute
deviation (MAD) of the finest-level detail coefficients.

```{r adaptive}
set.seed(42)
n = 256
signal = sin(seq(0, 4*pi, length.out = n)) + rnorm(n, sd = 0.3)

sch = lifting_scheme("cdf97")
decomp = lwt(signal, sch, levels = 4)

# Compute adaptive thresholds
lambdas = compute_adaptive_threshold(decomp, alpha = 0.3, beta = 1.2)
print(lambdas)
```

These thresholds are used internally by `denoise_signal_offline`,
`denoise_signal_causal`, and `new_wavelet_stream`, controlled by the
`alpha` and `beta` parameters.

## 6. Low-level transform pipeline

For users who need fine-grained control, the forward and inverse
transforms can be called directly, enabling custom processing between
decomposition and reconstruction:

```{r pipeline, fig.width=7, fig.height=4}
set.seed(42)
n = 512
t_vec = seq(0, 1, length.out = n)
pure = sin(2 * pi * 5 * t_vec) + 0.5 * sin(2 * pi * 20 * t_vec)
noisy = pure + rnorm(n, sd = 0.4)

sch = lifting_scheme("cdf97")

# 1. Forward transform
decomp = lwt(noisy, sch, levels = 5)

# 2. Compute adaptive thresholds
lambdas = compute_adaptive_threshold(decomp, alpha = 0.3, beta = 1.2)

# 3. Custom thresholding: apply semisoft to detail levels,
#    but leave the coarsest level (d5) untouched
for (lev in 1:4) {
  dname = paste0("d", lev)
  decomp$coeffs[[dname]] = threshold(
    decomp$coeffs[[dname]], lambdas[[dname]], method = "semisoft"
  )
}
# d5 is not thresholded — preserving low-frequency structure

# 4. Inverse transform
reconstructed = ilwt(decomp)

# 5. Compare
df_pipe = data.frame(
  t = rep(t_vec, 3),
  value = c(noisy, pure, reconstructed),
  Signal = rep(c("Noisy", "Truth", "Filtered"), each = n)
)

ggplot(df_pipe, aes(x = t, y = value, colour = Signal)) +
  geom_line(alpha = 0.7, linewidth = 0.4) +
  labs(title = "Custom denoising pipeline (CDF 9/7, 5 levels)",
       x = "Time", y = "Amplitude") +
  scale_colour_manual(values = c("Noisy" = "grey70", "Truth" = "black",
                                  "Filtered" = "#D55E00")) +
  theme_minimal()
```

This level of control is useful when the standard denoising functions
are not flexible enough — for example, when different decomposition
levels require different thresholding strategies, or when certain
frequency bands should be preserved entirely.

## 7. Visualizing wavelet basis functions

The `visualize_wavelet_basis` function (or `plot(scheme)`) generates the
scaling function ($\phi$) and wavelet function ($\psi$) by cascade
iteration of the lifting steps:

```{r basis, fig.width=6, fig.height=4}
plot(lifting_scheme("cdf97"))
```

This is helpful for understanding the shape and support of a wavelet,
and for verifying that custom wavelets produce reasonable basis functions.

## Summary

`rLifting` is designed to be both high-performance and extensible:

- **Built-in wavelets** cover the most common use cases (Haar through
  CDF 9/7).
- **Custom wavelets** can be defined via `lift_step` + `custom_wavelet`
  and used in every function.
- **Diagnostic tools** verify mathematical properties automatically.
- **Three thresholding methods** and adaptive threshold estimation
  provide flexible noise removal.
- **Low-level API** (`lwt`/`ilwt` + `compute_adaptive_threshold` +
  `threshold`) allows fully custom pipelines.
- The **C++ backend** ensures that all operations run at native speed.
