## ----include = FALSE----------------------------------------------------------
knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>",
  fig.width = 7,
  fig.height = 4
)

## ----setup--------------------------------------------------------------------
library(rLifting)

if (!requireNamespace("ggplot2", quietly = TRUE)) {
  knitr::opts_chunk$set(eval = FALSE)
  message("'ggplot2' is required to render plots. Vignette code will not run.")
} else {
  library(ggplot2)
}

data("doppler_example", package = "rLifting")
data("benchmark_rlifting", package = "rLifting")
set.seed(20260522)

## ----causality-penalty--------------------------------------------------------
sub = subset(
  benchmark_rlifting,
  Wavelet == "cdf53" & Boundary == "symmetric" &
    ThresholdMethod == "universal" & !grepl("tuned", Method) &
    Shrinkage == "semisoft"
)

penalty = data.frame(
  Signal = unique(sub$Signal),
  offline_MSE = sapply(unique(sub$Signal),
  function(s)
    sub$MSE_median[sub$Signal == s & sub$Mode == "offline"]),

  causal_MSE_settled = sapply(unique(sub$Signal),
  function(s)
    sub$MSE_settled_median[sub$Signal == s & sub$Mode == "causal"]),

  stream_MSE_settled = sapply(unique(sub$Signal),
  function(s)
    sub$MSE_settled_median[sub$Signal == s & sub$Mode == "stream"])
)

penalty$causal_over_offline = round(
  penalty$causal_MSE_settled / penalty$offline_MSE, 2
)

penalty

## ----causality-plot, echo=FALSE, fig.cap="Figure 1: Causality penalty across the four Donoho–Johnstone signals. Offline (left) uses the full signal; causal and stream (right) only past samples. Penalty ranges from 1.3× on blocks to 3.3× on heavisine."----
df_pen = data.frame(
  Signal = rep(penalty$Signal, 3),
  Mode = factor(
    rep(
      c("Offline", "Causal", "Stream"), 
      each = nrow(penalty)
    ),
    levels = c("Offline", "Causal", "Stream")
  ),
  
  MSE = c(
    penalty$offline_MSE,
    penalty$causal_MSE_settled,
    penalty$stream_MSE_settled
  )
)

ggplot(df_pen, aes(x = Signal, y = MSE, fill = Mode)) +
  geom_col(position = "dodge") +
  scale_fill_manual(
    values = c(
      "Offline" = "#0072B2", "Causal" = "#D55E00", "Stream" = "#009E73"
    )
  ) +
  theme_minimal() +
  labs(
    title = "Causality penalty: MSE by mode and signal",
    subtitle = "cdf53, symmetric, universal/semisoft, default α/β",
    x = NULL, y = "MSE (settled for causal/stream)"
  )

## ----window-size-effect, eval=FALSE-------------------------------------------
# # Sketch of the trade-off (heuristic, not a chunk you should run blindly)
# window_size = 63 # short — fast, lower resolution, ~31 raw samples at start
# window_size = 127 # short-medium
# window_size = 255 # benchmark default
# window_size = 511 # long — better σ̂, more memory, longer warm-up

## ----latency-table------------------------------------------------------------
latency = aggregate(
  Per_sample_us_median ~ Mode + Wavelet, data = benchmark_rlifting,
  FUN = median
)

latency$Per_sample_us_median = round(latency$Per_sample_us_median, 2)

reshape(
  latency, idvar = "Wavelet", timevar = "Mode", direction = "wide"
)

## ----latency-plot, echo=FALSE, fig.cap="Figure 2: Median per-sample latency in microseconds across the three modes and five wavelets. Note the log scale — causal is ~80–115× slower per sample than offline, and stream adds another 1.5–2.1× over causal because of the R closure call overhead per sample."----
lat_long = aggregate(
  Per_sample_us_median ~ Mode + Wavelet, data = benchmark_rlifting,
  FUN = median
)

lat_long$Mode = factor(lat_long$Mode, levels = c("offline", "causal", "stream"))
ggplot(lat_long, aes(x = Wavelet, y = Per_sample_us_median, fill = Mode)) +
  geom_col(position = "dodge") +
  scale_y_log10() +
  scale_fill_manual(
    values = c(
      "offline" = "#0072B2", "causal" = "#D55E00", "stream" = "#009E73"
    )
  ) +
  theme_minimal() +
  labs(
    title = "Per-sample latency by mode and wavelet",
    subtitle = "Log scale on y; median across all benchmark configurations",
    x = NULL, y = "Microseconds per sample"
  )

## ----causal-best-wavelet------------------------------------------------------
best_causal = aggregate(
  MSE_settled_median ~ Signal + Wavelet,
  data = subset(benchmark_rlifting, Mode == "causal"),
  FUN = min
)

do.call(
  rbind,
  lapply(
    unique(best_causal$Signal),
    function(s) {
      ss = best_causal[best_causal$Signal == s, ]
      ss = ss[order(ss$MSE_settled_median), ]
      data.frame(
        Signal = s,
        ranking = paste(ss$Wavelet, collapse = " > ")
      )
    }
  )
)

## ----leakage-check------------------------------------------------------------
x = doppler_example$noisy
scheme = lifting_scheme("haar")
ws = 255

base_out = denoise_signal_causal(
  x, scheme,
  window_size = ws, levels = 4,
  shrinkage = "semisoft"
)

# Counterfactual: corrupt the second half of the signal severely
x_perturbed = x
half = floor(length(x) / 2)
x_perturbed[(half + 1):length(x)] = x_perturbed[(half + 1):length(x)] +
  rnorm(length(x) - half, sd = 5)

perturbed_out = denoise_signal_causal(
  x_perturbed, scheme,
  window_size = ws, levels = 4,
  shrinkage = "semisoft"
)

# Outputs at positions ≤ half must be unchanged
max_diff_before = max(abs(base_out[1:half] - perturbed_out[1:half]))
max_diff_after  = max(
  abs(
    base_out[(half + 1):length(x)] -
      perturbed_out[(half + 1):length(x)]
  )
)

data.frame(
  region = c(
    "samples 1..half (before perturbation)",
    "samples (half+1)..n (after perturbation)"
  ),
  max_abs_diff = c(max_diff_before, max_diff_after)
)

