Nothing
#' @title Analyse portable CW-OSL measurements
#'
#' @description The function analyses CW-OSL curve data produced by a SUERC portable OSL reader and
#' produces a combined plot of OSL/IRSL signal intensities, OSL/IRSL depletion ratios
#' and the IRSL/OSL ratio.
#'
#' @details This function only works with [RLum.Analysis-class] objects produced by [read_PSL2R].
#' It further assumes (or rather requires) an equal amount of OSL and IRSL curves that
#' are pairwise combined for calculating the IRSL/OSL ratio. For calculating the depletion ratios
#' the cumulative signal of the last n channels (same number of channels as specified
#' by `signal.integral`) is divided by cumulative signal of the first n channels (`signal.integral`).
#'
#' **Note: The function assumes the following sequence pattern: `DARK COUNT`, `IRSL`, `DARK COUNT`, `BSL`, `DARK COUNT`. If you have written a different sequence, the analysis function will (likely) not work!**.
#'
#' **Signal processing**
#' The function processes the signals as follows: `BSL` and `IRSL` signals are extracted using the
#' chosen signal integral, dark counts are taken in full.
#'
#' **Working with coordinates**
#' Usually samples are taken from a profile with a certain stratigraphy. In the past the function
#' calculated an index. With this newer version, you have two option of passing on xy-coordinates
#' to the function:
#'
#' * (1) Add coordinates to the sample name during measurement. The form is rather
#' strict and has to follow the scheme `_x:<number>|y:<number>`. Example:
#' `sample_x:0.2|y:0.4`.
#'
#' * (2) Alternatively, you can provide a [list] or [matrix] with the sample coordinates.
#' Example: `coord = list(c(0.2, 1), c(0.3,1.2))`
#'
#' Please note that the unit is meter (m) and the function expects always xy-coordinates.
#' The latter one is useful for surface interpolations. If you have measured a profile where
#' the x-coordinates to not measure, x-coordinates should be 0.
#'
#' @param object [RLum.Analysis-class] (**required**): [RLum.Analysis-class] object produced by [read_PSL2R].
#' The input can be a [list] of such objects, in such case each input is treated as a separate sample
#' and the results are merged.
#'
#' @param signal.integral [numeric] (**required**): A vector of two values specifying the lower and upper channel used to calculate the OSL/IRSL signal. Can be provided in form of `c(1, 5)` or `1:5`.
#'
#' @param invert [logical] (*with default*): `TRUE` flip the plot the data in reverse order.
#'
#' @param normalise [logical] (*with default*): `TRUE` to normalise the OSL/IRSL signals
#' to the *mean* of all corresponding data curves.
#'
#' @param mode [character] (*with default*): defines the analysis mode, allowed
#' are `"profile"` (the default) and `"surface"` for surface interpolation. If you select
#' something else, nothing will be plotted (similar to `plot = FALSE`).
#'
#' @param coord [list] [matrix] (*optional*): a list or matrix of the same length as
#' number of samples measured with coordinates for the sampling positions. Coordinates
#' are expected to be provided in meter (unit: m).
#' Expected are x and y coordinates, e.g.,
#' `coord = list(samp1 = c(0.1, 0.2)`. If you have not measured x coordinates, please x should be 0.
#'
#' @param plot [logical] (*with default*): enable/disable plot output
#'
#' @param ... other parameters to be passed to modify the plot output.
#' Supported are `run` to provide the run name ,
#' if the input is a `list`, this is set automatically. Further plot parameters are
#' `surface_values` ([character] with value to plot), `legend` (`TRUE`/`FALSE`), `col_ramp` (for
#' surface mode), `contour` (contour lines `TRUE`/`FALSE` in surface mode), `grid` (`TRUE`/`FALSE`), `col`, `pch` (for profile mode), `xlim` (a name [list] for profile mode), `ylim`,
#' `zlim` (surface mode only), `ylab`, `xlab`, `zlab` (here x-axis labelling), `main`, `bg_img` (for
#' profile mode background image, usually a profile photo; should be a raster object),
#' `bg_img_positions` (a vector with the four corner positions, cf. [graphics::rasterImage])
#'
#' @return
#' Returns an S4 [RLum.Results-class] object with the following elements:
#'
#' `$data`\cr
#' `.. $summary`: [data.frame] with the results\cr
#' `.. $data`: [list] with the [RLum.Analysis-class] objects\cr
#' `.. $args`: [list] the input arguments
#'
#' @seealso [RLum.Analysis-class], [RLum.Data.Curve-class], [read_PSL2R]
#'
#' @author Christoph Burow, University of Cologne (Germany), Sebastian Kreutzer,
#' Institute of Geography, Ruprecht-Karl University of Heidelberg, Germany
#'
#' @section Function version: 0.1.0
#'
#' @keywords datagen plot
#'
#' @examples
#'
#' ## example profile plot
#' # (1) load example data set
#' data("ExampleData.portableOSL", envir = environment())
#'
#' # (2) merge and plot all RLum.Analysis objects
#' merged <- merge_RLum(ExampleData.portableOSL)
#' plot_RLum(
#' object = merged,
#' combine = TRUE,
#' records_max = 5,
#' legend.pos = "outside")
#' merged
#'
#' # (3) analyse and plot
#' results <- analyse_portableOSL(
#' merged,
#' signal.integral = 1:5,
#' invert = FALSE,
#' normalise = TRUE)
#' get_RLum(results)
#'
#' @md
#' @export
analyse_portableOSL <- function(
object,
signal.integral = NULL,
invert = FALSE,
normalise = FALSE,
mode = "profile",
coord = NULL,
plot = TRUE,
...)
{
## TODO
## - add tests for background image option
## - clear docu
# Self-call ---------------------------------------------------------------
if (inherits(object, "list")) {
temp <- .warningCatcher(lapply(1:length(object), function(x) {
analyse_portableOSL(
object = object[[x]],
signal.integral = signal.integral,
invert = invert,
normalise = normalise,
plot = plot,
run = paste0("RUN #", x))
}))
return(merge_RLum(temp))
}
# Start function ----------------------------------------------------------
## INPUT VERIFICATION ----
## only RLum.Analysis objects
if (!inherits(object, "RLum.Analysis"))
stop("[analyse_portableOSL()] Only objects of class 'RLum.Analysis' are allowed.",
call. = FALSE)
## only curve objects
if (!all(sapply(object, class) == "RLum.Data.Curve"))
stop("[analyse_portableOSL()] The 'RLum.Analysis' object must contain only objects of class 'RLum.Data.Curve'.",
call. = FALSE)
## check originator
if (!all(sapply(object, function(x) x@originator) == "read_PSL2R"))
stop("[analyse_portableOSL()] Only objects originating from 'read_PSL2R()' are allowed.",
call. = FALSE)
## check sequence pattern
if(!all(names(object)[1:5] == c("USER", "IRSL", "USER", "OSL", "USER")))
stop("[analyse_portableOSL()] Sequence pattern not supported, please read manual for details!",
call. = FALSE)
if (is.null(signal.integral)) {
signal.integral <- c(1, 1)
warning("No value for 'signal.integral' provided. Only the first data point of each curve was used!",
call. = FALSE)
}
## set SAMPLE --------
if("run" %in% names(list(...)))
run <- list(...)$run
else if (!is.null(object@info$Run_Name))
run <- object@info$Run_Name
else
run <- "Run #1"
## CALCULATIONS ----
## Note: the list ... unlist construction is used make sure that get_RLum() always
## returns a list
### get OSL -------
OSL <- .unlist_RLum(list(get_RLum(object, recordType = "OSL")))
OSL <- do.call(rbind, lapply(OSL, function(x) {
.posl_get_signal(x, signal.integral)
}))
### get IRSL -------
IRSL <- .unlist_RLum(list(get_RLum(object, recordType = "IRSL")))
IRSL <- do.call(rbind, lapply(IRSL, function(x) {
.posl_get_signal(x, signal.integral)
}))
### get DARK counts ----------
### we assume that USER contains the dark count measurements
DARK_COUNT <- .unlist_RLum(list(get_RLum(object, recordType = "USER")))
DARK_COUNT <- lapply(seq(1,length(DARK_COUNT),3), function(x) DARK_COUNT[x:(x+2)])
DARK_COUNT <- do.call(rbind, lapply(DARK_COUNT, function(x) {
.posl_get_dark_count(x)
}))
### NORMALISE ----
if (normalise) {
OSL <- .posl_normalise(OSL)
IRSL <- .posl_normalise(IRSL)
}
### OSL/IRSL Ratio -------
RATIO <- IRSL$sum_signal / OSL$sum_signal
### extract coordinates -------
if(is.null(coord)) {
coord <- .extract_PSL_coord(object)
} else {
if(!inherits(coord, "matrix") && !inherits(coord, "list"))
stop("[analyse_portableOSL()] Argument 'coord' needs to be a matrix or list!",
call. = FALSE)
if(inherits(coord, "list"))
coord <- do.call(rbind, coord)
## check length
if(nrow(coord) != length(OSL$sum_signal))
stop("[analyse_portableOSL()] Number of coordinates differ from the number of samples!",
call. = FALSE)
}
### GENERATE SUMMARY data.frame -----
summary <- data.frame(
ID = seq_along(OSL$sum_signal),
RUN = run,
BSL = OSL$sum_signal,
BSL_error = OSL$sum_signal_err,
IRSL = IRSL$sum_signal,
IRSL_error = IRSL$sum_signal_err,
BSL_depletion = OSL$sum_signal_depletion,
IRSL_depletion = IRSL$sum_signal_depletion,
IRSL_BSL_RATIO = RATIO,
DARK = DARK_COUNT$mean_dark_count,
DARK_error = DARK_COUNT$sd_dark_count,
COORD_X = coord[,1],
COORD_Y = coord[,2]
)
## if coordinates exist, sort by depth
if(!any(is.na(coord[,2])))
summary <- summary[order(coord[,2]),]
### INVERT ----------
if(invert)
summary <- summary[nrow(summary):1,]
# PLOTTING -------------------------------------------------------------------
## generate list of plot matrices
## this done to have consistent settings for all plot types
parm <- c("BSL", "BSL_error", "IRSL", "IRSL_error",
"BSL_depletion", "IRSL_depletion", "IRSL_BSL_RATIO", "DARK", "DARK_error")
m_list <- lapply(parm, function(x){
cbind(x = summary[["COORD_X"]], y = summary[["COORD_Y"]], value = summary[[x]])
})
## correct names of the list
names(m_list) <- parm
## add a few attributes to be used later
attr(m_list, "xlim") <- lapply(m_list, function(x) range(x[,1]))
attr(m_list, "ylim") <- if(invert) rev(range(m_list[[1]][,2])) else range(m_list[[1]][,2])
attr(m_list, "zlim") <- lapply(m_list, function(x) range(x[,3]))
## account for surface case
if (!is.null(mode) && mode == "surface") {
attr(m_list, "ylim") <- if (invert) rev(range(summary$COORD_Y)) else range(summary$COORD_Y)
attr(m_list, "xlim") <- range(summary$COORD_X)
}
if (!is.null(mode) && plot[1]) {
## account for surface case
## preset plot settings
## plot settings -------
plot_settings <- modifyList(
x = list(
col_ramp = grDevices::heat.colors(30, rev = TRUE, alpha = 0.5),
bg_img = NULL,
bg_img_positions = NULL,
surface_value = c("BSL", "IRSL", "IRSL_BSL_RATIO"),
legend = TRUE,
col = c("blue", "red", "blue", "red", "black", "grey"),
pch = rep(16, length(m_list)),
xlim = attr(m_list, "xlim"),
ylim = attr(m_list, "ylim"),
zlim = if(mode == "surface") NA else attr(m_list, "zlim"),
ylab = if(!any(is.na(summary$COORD_Y))) "Depth [m]" else "Index",
xlab = "x [m]",
grid = TRUE,
contour = FALSE,
zlab = c("BSL", "IRSL", "BSL depl.", "IRSL depl.", "IRSL/BSL", "mean DARK"),
main = summary$RUN[1]
),
val = list(...), keep.null = TRUE)
## mode == "surface" ---------
if(mode[1] == "surface") {
### check for validity of surface value -------
if(!all(plot_settings$surface_value %in% names(m_list)))
stop(paste0("[analyse_portableOSL()] Unknown value to plot: Valid are: ",
paste(names(m_list), collapse = ", ")),
call. = FALSE)
## set par -------
if(length(plot_settings$surface_value) > 1) {
par.default <- par(mfrow = c(2,2))
on.exit(par(par.default))
}
## loop over surface values -------
for(i in plot_settings$surface_value) {
## set matrix for the plot
m <- m_list[[i]]
## respect xlim and ylim range
m <- m[m[,2] >= min(plot_settings$ylim) & m[,2] <= max(plot_settings$ylim), ]
m <- m[m[,1] >= min(plot_settings$xlim) & m[,1] <= max(plot_settings$xlim), ]
## respect z_values
if(!all(is.na(plot_settings$zlim)))
m <- m[m[,3] >= min(plot_settings$zlim) & m[,3] <= max(plot_settings$zlim), ]
## interpolate ------
s <-
try(interp::interp(
x = m[, 1],
y = m[, 2],
z = m[, 3],
nx = 200,
ny = 200,
), silent = TRUE)
## show only warning
if(inherits(s, "try-error"))
warning("[analyse_portableOSL()] Surface interpolation failed, this happens when all points are arranged in one line. Nothing plotted!", call. = FALSE)
## show error
if(!inherits(s, "try-error")) {
par.default <- c(
if(exists("par.default")) par.default else NULL,
par(mar = c(4.5,4.5,4,2), xpd = FALSE))
on.exit(par(par.default))
## open empty plot
plot(
x = NA,
y = NA,
ylim = plot_settings$ylim,
xlim = plot_settings$xlim,
xlab = plot_settings$xlab,
ylab = plot_settings$ylab,
main = plot_settings$main)
## add background image if available -------
if (!is.null(plot_settings$bg_img)) {
## get corner positions
if(!is.null(plot_settings$bg_img_positions))
positions <- plot_settings$bg_img_positions[1:4]
else
positions <- par()$usr
graphics::rasterImage(
image = plot_settings$bg_img,
xleft = positions[1],
ybottom = positions[4],
xright = positions[2],
ytop = positions[3],
interpolate = TRUE)
}
## plot image -------
graphics::image(
s,
col = plot_settings$col_ramp,
add = TRUE
)
## add contour
if (plot_settings$contour)
graphics::contour(m, add = TRUE, col = "grey")
## add points
points(m[,1:2], pch = 20)
## add what is shown in the plot
mtext(side = 3, text = i, cex = 0.7)
## add legend
if(plot_settings$legend) {
par.default <- c(par.default, par(xpd = TRUE))
on.exit(par(par.default))
col_grad <- plot_settings$col_ramp[
seq(1, length(plot_settings$col_ramp), length.out = 14)]
slices <- seq(par()$usr[3],par()$usr[4],length.out = 15)
for(s in 1:(length(slices) - 1)){
graphics::rect(
xleft = par()$usr[2] * 1.01,
xright = par()$usr[2] * 1.03,
ybottom = slices[s],
ytop = slices[s + 1],
col = col_grad[s],
border = TRUE)
}
## add legend text
text(
x = par()$usr[2] * 1.04,
y = par()$usr[4],
labels = if(is.null(plot_settings$zlim_image)) {
format(max(m[,3]), digits = 1, scientific = TRUE)
} else {
format(plot_settings$zlim_image[2], digits = 1, scientific = TRUE)
},
cex = 0.6,
srt = 270,
pos = 3)
text(
x = par()$usr[2] * 1.04,
y = par()$usr[3],
labels = if(is.null(plot_settings$zlim_image)) {
format(min(m[,3]), digits = 1, scientific = TRUE)
} else {
format(plot_settings$zlim_image[1], digits = 1, scientific = TRUE)
},
cex = 0.6,
pos = 3,
srt = 270)
## add legend labelling (central)
text(
x = par()$usr[2] * 1.05,
y = (par()$usr[4] - par()$usr[3])/2 + par()$usr[3],
labels = "Intensity [a.u.]",
cex = 0.7,
pos = 3,
srt = 270)
}
}
}# end for loop
}
## mode == "profile" ---------
if (!is.null(mode[1]) && mode == "profile") {
par.old.full <- par(no.readonly = TRUE)
on.exit(par(par.old.full))
# default: par(mar = c(5, 4, 4, 2) + 0.1) // bottom, left, top, right
par(mfrow = c(1, 7))
par(mar = c(5, 4, 4, 1) + 0.1)
frame()
mtext(side= 3, run, cex = 0.7, line = 2)
par(mar = c(5, 0, 4, 1) + 0.1)
## make sure that wrong zlim settings do not screw up the function
if(!inherits(plot_settings$zlim, "list")) {
warning("[analyse_portableOSL()] In profile mode, zlim needs to be provided
as named list. Example: list(BSL = c(0,1)). Reset to default.",
call. = FALSE)
plot_settings$zlim <- attr(m_list, "zlim")
}
#### BSL -------
plot(
NA,
NA,
ylim = plot_settings$ylim,
xlim = plot_settings$zlim[["BSL"]],
xlab = plot_settings$zlab[1],
bty = "n",
yaxt = "n"
)
if(plot_settings$grid) grid()
lines(
x = m_list[["BSL"]][,"value"],
y = m_list[["BSL"]][,"y"],
type = "b",
pch = plot_settings$pch[1],
col = plot_settings$col[1]
)
## add error bars
segments(
x0 = m_list[["BSL"]][,"value"] - m_list[["BSL_error"]][,"value"],
x1 = m_list[["BSL"]][,"value"] + m_list[["BSL_error"]][,"value"],
y0 = m_list[["BSL"]][,"y"],
y1 = m_list[["BSL"]][,"y"],
col = plot_settings$col[1])
axis(2, line = 3, at = m_list[["BSL"]][,"y"], labels = m_list[["BSL"]][,"y"])
axis(3)
## add general y-axis label
mtext(plot_settings$ylab[1], side = 2, line = 6)
### IRSL --------------
plot(
NA, NA,
ylim = plot_settings$ylim,
xlim = plot_settings$zlim[["IRSL"]],
xlab = plot_settings$zlab[2],
bty = "n",
yaxt = "n"
)
if(plot_settings$grid) grid()
lines(
x = m_list[["IRSL"]][,"value"],
y = m_list[["IRSL"]][,"y"],
type = "b",
pch = plot_settings$pch[2],
col = plot_settings$col[2])
## add error bars
segments(
x0 = m_list[["IRSL"]][,"value"] - m_list[["IRSL_error"]][,"value"],
x1 = m_list[["IRSL"]][,"value"] + m_list[["IRSL_error"]][,"value"],
y0 = m_list[["IRSL"]][,"y"],
y1 = m_list[["IRSL"]][,"y"],
col = plot_settings$col[2])
axis(3)
### OSL DEPLETATION -------
plot(
NA, NA,
ylim = plot_settings$ylim,
xlim = plot_settings$zlim[["BSL_depletion"]],
xlab = plot_settings$zlab[3],
bty = "n",
yaxt = "n"
)
if(plot_settings$grid) grid()
lines(
x = m_list[["BSL_depletion"]][,"value"],
y = m_list[["BSL_depletion"]][,"y"],
type = "b",
lty = 2,
pch = plot_settings$pch[3],
col = plot_settings$col[3]
)
axis(3)
### IRSL DEPLETION ---------------
plot(
NA, NA,
ylim = plot_settings$ylim,
xlim = plot_settings$zlim[["IRSL_depletion"]],
xlab = plot_settings$zlab[4],
bty = "n",
yaxt = "n"
)
if(plot_settings$grid) grid()
lines(
x = m_list[["IRSL_depletion"]][,"value"],
y = m_list[["IRSL_depletion"]][,"y"],
type = "b",
lty = 2,
pch = plot_settings$pch[4],
col = plot_settings$col[4])
axis(3)
### RATIO -----------------------------
plot(
NA, NA,
ylim = plot_settings$ylim,
xlim = plot_settings$zlim[["IRSL_BSL_RATIO"]],
xlab = plot_settings$zlab[5],
ylab = "",
bty = "n",
yaxt = "n"
)
if(plot_settings$grid) grid()
lines(
x = m_list[["IRSL_BSL_RATIO"]][,"value"],
y = m_list[["IRSL_BSL_RATIO"]][,"y"],
type = "b",
pch = plot_settings$pch[5],
col = plot_settings$col[5])
axis(3)
### DARK -----------------------------
plot(
x = m_list[["DARK"]][,"value"],
y = m_list[["DARK_error"]][,"y"],
type = "b",
pch = plot_settings$pch,
col = plot_settings$col[6],
ylim = plot_settings$ylim,
xlim = range(c(
plot_settings$zlim[["DARK"]] - plot_settings$zlim[["DARK_error"]],
plot_settings$zlim[["DARK"]] + plot_settings$zlim[["DARK_error"]])),
xlab = plot_settings$zlab[6],
ylab = "",
bty = "n",
yaxt = "n"
)
## add error bars
segments(
x0 = m_list[["DARK"]][,"value"] - m_list[["DARK_error"]][,"value"],
x1 = m_list[["DARK"]][,"value"] + m_list[["DARK_error"]][,"value"],
y0 = m_list[["DARK"]][,"y"],
y1 = m_list[["DARK"]][,"y"],
col = plot_settings$col[6])
axis(3)
} ## end mode == "profile"
}
## RETURN VALUE ----
call<- sys.call()
args <- as.list(call)[2:length(call)]
newRLumResults <- set_RLum(
class = "RLum.Results",
data = list(
summary=summary,
data = object,
args=args
),
info = list(call = call))
return(newRLumResults)
}
# HELPER FUNCTIONS ----------
## This extracts the relevant curve data information of the RLum.Data.Curve
## objects
.posl_get_signal <- function(x, signal.integral) {
raw_signal <- get_RLum(x)[,2]
sigint <- range(signal.integral)
if (sigint[2] > length(raw_signal)) {
sigint[2] <- length(raw_signal)
warning("'signal.integral' (", paste(range(signal.integral), collapse = ", "),") ",
"exceeded the number of available data points (n = ", length(raw_signal),") and ",
"has been automatically reduced to the maximum number.", call. = FALSE)
}
sum_signal <- sum(raw_signal[sigint[1]:sigint[2]])
sum_signal_err <- sqrt(sum(x@info$raw_data$counts_per_cycle_error[sigint[1]:sigint[2]]^2))
sum_signal_depletion <- sum(raw_signal[(length(raw_signal)-length(sigint[1]:sigint[2])):length(raw_signal)]) / sum_signal
return(data.frame(sum_signal, sum_signal_err, sum_signal_depletion))
}
.posl_get_dark_count <- function(x) {
## we do assume a fixed sequence pattern, hence, we know what to
## expect that anything that comes in here, can be merged
counts <- unlist(lapply(x, function(x) as.matrix(x)[,2]))
return(data.frame(mean_dark_count = mean(counts), sd_dark_count = sd(counts)))
}
## This function normalises the data curve by the mean signal
.posl_normalise <- function(x) {
rel.error <- x$sum_signal_err / x$sum_signal
x$sum_signal <- x$sum_signal / mean(x$sum_signal)
x$sum_signal_err <- x$sum_signal * rel.error
x$sum_signal_depletion <- x$sum_signal_depletion / mean(x$sum_signal_depletion)
return(x)
}
## This function extracts the coordinates from the file name
##
.extract_PSL_coord <- function(object){
## get settings
settings_sample <- vapply(object, function(x) x@info$settings$Sample, character(1)) |>
unique()
## set character vector
tmp_coord <- character(length(settings_sample))
## search for pattern match ... why?
## because otherwise the dataset becomes inconsistent
pattern_match <- grepl(
pattern = "\\_x\\:[0-9].+\\|y\\:[0-9].+",
x = settings_sample, perl = TRUE)
## extract coordinates
tmp_coord[pattern_match] <- regexpr(
pattern = "\\_x\\:[0-9].+\\|y\\:[0-9].+",
text = settings_sample[pattern_match ], perl = TRUE) |>
regmatches(x = settings_sample[pattern_match], m = _)
## extract x and y
coord_split <- strsplit(tmp_coord, split = "|y:", fixed = TRUE)
## assign values
coord <- vapply(coord_split, function(x) {
if(length(x) == 0)
return(c(x = NA_real_, y = NA_real_))
c(x = as.numeric(strsplit(x, "_x:", fixed = TRUE)[[1]][[2]]),
y = as.numeric(x[2]))},
numeric(2)) |> t()
## if NA, assign index
if(any(is.na(coord[,1]))) coord[,1] <- 0
if(any(is.na(coord[,2]))) coord[,2] <- 1:nrow(coord)
return(coord)
}
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.