Module tests for plot_MultiExponential()

Author

Dirk Mittelstraß

Published

August 31, 2025

This document collects scripts for developing, bug-fixing and pre-release validating a specific function.

Sourcing

Is the function successfully sourced?

Show code
library(OSLdecomposition)
source("../R/plot_MultiExponential.R")

Define signal curves

Show code
 # ToDo: Create a couple of test curves with wild varity of signal shapes

cases <- list()
add_component_case <- 
  function(cases, lambda, n, 
           channel_width = 0.1, 
           channel_number = 400, 
           noise = 0, 
           background = 0,
           reiterate = FALSE,
           title = "Unspecified test case"){
    
    components <- data.frame(name = paste("Component", 1:length(n)),
                             lambda = lambda, 
                             n = n)
    curve <- simulate_OSLcomponents(components, 
                                    channel.width = channel_width,
                                    channel.number = channel_number,
                                    simulate.curve = TRUE, 
                                    add.poisson.noise = TRUE,
                                    add.gaussian.noise = noise,
                                    add.background = background)
    
    # Re-iterate components to add some uncertainty
    if (reiterate) {
          components <- decompose_OSLcurve(curve, 
                                     components, 
                                     verbose = FALSE)
      
    }

  
    case <- list(components = components,
                 curve = curve,
                 title = title)
    
    cases <- c(cases, list(case))
    return(cases)
  }

cases <- add_component_case(cases,
                   lambda = c(1, 0.2, 0.02),
                   n = c(1000, 1000, 10000),
                   reiterate = TRUE,
                   title = "Typical quartz OSL curve")

cases <- add_component_case(cases,
                   lambda = c(2, 0.5, 0.2, 0.001),
                   n = c(200, -500, 2000, 100000),
                   channel_width = 0.875,
                   channel_number = 42,
                   title = "Atypical low-res measurement with stretched medium component")

cases <- add_component_case(cases,
                   lambda = c(0.02, 0.005, 0.002, 0.0005, 0.00001),
                   n = c(-10e5, 10e5, 10e6, 10e7, 5e9),
                   channel_width = 2,
                   channel_number = 5000,
                   noise = 2000,
                   title = "Typical IR-RF curve")

cases <- add_component_case(cases,
                   lambda = c(0.1, 0.02),
                   n = c(-10e4, 10e5),
                   channel_width = 0.2,
                   channel_number = 200,
                   title = "Increasing signal")

cases <- add_component_case(cases,
                   lambda = c(1, 0.01),
                   n = c(10, 100),
                   noise = 10,
                   reiterate = TRUE,
                   title = "Background-corrected noise")

cases <- add_component_case(cases,
                   lambda = c(2, 0.03, 0.0001),
                   n = c(20e5, -10e6, 10e9),
                   channel_number = 500,
                   title = "Sharp drop then slowly increasing")


cases <- add_component_case(cases,
                   lambda = c(0.3),
                   background = 20,
                   n = c(3000),
                   reiterate = TRUE,
                   title = "Single component with some background")

Plotting modes

Components as stacked areas

Show code
for (case in cases) {
  plot_MultiExponential(case$curve, case$components, 
                        fill.components = TRUE, main = case$title)
}

Line graphs

Show code
for (case in cases) {
  plot_MultiExponential(case$curve, case$components, 
                        fill.components = FALSE, main = case$title)
}

Axis transformation

Linear modulation

After Bulur (2000)

Show code
for (case in cases) {
  
  cat("CASE:", case$title, "\n")
  
    # Randomizer
  fill.components <- sample(c(FALSE,TRUE), size = 1)
  cat("fill.components =", fill.components, "\n")

  plot_MultiExponential(case$curve, case$components, 
                        fill.components = fill.components, 
                        linear.modulation = TRUE,
                        main = case$title)
  cat("\n\n")
}
CASE: Typical quartz OSL curve 
fill.components = TRUE 



CASE: Atypical low-res measurement with stretched medium component 
fill.components = FALSE 



CASE: Typical IR-RF curve 
fill.components = TRUE 



CASE: Increasing signal 
fill.components = FALSE 



CASE: Background-corrected noise 
fill.components = TRUE 



CASE: Sharp drop then slowly increasing 
fill.components = FALSE 



CASE: Single component with some background 
fill.components = FALSE 

Logarithmic axes

Show code
for (case in cases) {
  
  cat("CASE:", case$title, "\n")
  
  # Randomizer
  fill.components <- sample(c(FALSE,TRUE), size = 1)
  linear.modulation <- sample(c(FALSE,FALSE,TRUE), size = 1)
  xlog <- sample(c(FALSE,TRUE), size = 1)
  cat("fill.components =", fill.components, "|",
      "linear.modulation =", linear.modulation, "|",
      "xlog =", xlog, "\n")
  
  plot_MultiExponential(case$curve, case$components, 
                        fill.components = fill.components, 
                        linear.modulation = linear.modulation,
                        xlog = xlog, 
                        ylog = TRUE,
                        main = case$title)
}
CASE: Typical quartz OSL curve 
fill.components = TRUE | linear.modulation = FALSE | xlog = TRUE 

CASE: Atypical low-res measurement with stretched medium component 
fill.components = FALSE | linear.modulation = TRUE | xlog = TRUE 
Input data contains signal components with negative intensity (increasing signals). These can not be displayed with logarithmic y-axis and are therefore omitted.
`geom_line()`: Each group consists of only one observation.
ℹ Do you need to adjust the group aesthetic?

CASE: Typical IR-RF curve 
fill.components = TRUE | linear.modulation = TRUE | xlog = TRUE 
Input data contains signal components with negative intensity (increasing signals). These can not be displayed with logarithmic y-axis and are therefore omitted.

CASE: Increasing signal 
fill.components = FALSE | linear.modulation = TRUE | xlog = TRUE 
Input data contains signal components with negative intensity (increasing signals). These can not be displayed with logarithmic y-axis and are therefore omitted.

CASE: Background-corrected noise 
fill.components = TRUE | linear.modulation = FALSE | xlog = TRUE 
Input data contains signal components with negative intensity (increasing signals). These can not be displayed with logarithmic y-axis and are therefore omitted.
Signal curve contains value equal/lower zero. Values below 0.1 will not be displayed.

CASE: Sharp drop then slowly increasing 
fill.components = FALSE | linear.modulation = TRUE | xlog = FALSE 
Input data contains signal components with negative intensity (increasing signals). These can not be displayed with logarithmic y-axis and are therefore omitted.

CASE: Single component with some background 
fill.components = TRUE | linear.modulation = FALSE | xlog = FALSE 

Manual plot design

Show code
# Rescale y-axis, put legend inside plot and add units
case <- cases[[5]]
plot_MultiExponential(case$curve, case$components,
                      fill.components = FALSE,
                      xlab = "Stimulation time [sec]",
                      ylab = "PMT signal [cts]",
                      font.size = 12,
                      legend.position = c(1, 1),
                      legend.justification = c(1, 1),
                      ylim = c(-20, 50))
Warning: A numeric `legend.position` argument in `theme()` was deprecated in ggplot2
3.5.0.
ℹ Please use the `legend.position.inside` argument of `theme()` instead.

Show code
# IR-RF example with some individual colors
case <- cases[[3]]
plot_MultiExponential(case$curve, case$components,
                      main = "Infrared radiofluorescence of abritrary mineral",
                      xlab = "Accumulated dose [Gy]",
                      ylab = "IR-RF signal [cts]",
                      graph.colors = c("black", "red", rev(heat.colors(5))),
                      xlim = c(0, 3000),
                      theme.set = ggplot2::theme_bw())

Show code
# User-set graph names
case <- cases[[1]]
plot_MultiExponential(case$curve, case$components,
                      main = "Quartz CW-OSL measurement",
                      xlab = "Time (s)",
                      ylab = "Signal (counts)",
                      graph.colors = c("black", "blue", grey.colors(3)),
                      graph.names = c("OSL decay", "Component sum", "Res",
                                      "Fast comp.", "Medium comp.", "Slow comp."),
                      legend.position = "bottom",
                      font.size = 8,
                      theme.set = ggplot2::theme_minimal())

Not-standard inputs

User-set decay rates

Show code
# Use common quartz CW-OSL as example
case <- cases[[1]]

# Define test cases
component_inputs <- list("Decay rates only" = case$components$lambda,
                        "Named vector as input" = c(Schnell = 1,
                                         Mittel = 0.1,
                                         Langsam = 0.01),
                        "Only a single decay rate" = 0.3,
                        "Too many decay rates" = c(2,1,0.5,0.2,0.1,0.05, 0.01))



for (i in 1:length(component_inputs)) {
  
  plot_MultiExponential(case$curve,
                        component_inputs[[i]],
                        main = names(component_inputs)[i])
}

Components without curve

Show code
  plot_MultiExponential(components = case$components,
                        main = "No curve, just component table given")

Curve without components

Show code
source("../R/plot_MultiExponential.R")  

plot_MultiExponential(curve = case$curve,
                        main = "No components, just curve given")
Input object 'curve' has more than two columns. Just the first two are used to define time and signal

Show code
plot_MultiExponential(curve = cbind(case$curve$time, case$curve$signal),
                      main = "Just curve as matrix with time and signal column")

ToDo:

  • When components$lambda = NA (decomposition or fit failed)