Nothing
#' @title Fit Isothermal Holding Data
#'
#' @description ##TODO
#'
#' @details ##TODO
#'
#' @param data [character] or [data.frame] (**required**): file path or data
#' frame with 5 columns named "SAMPLE", "TEMP", "TIME", "LxTx", "LxTx_ERROR".
#'
#' @param rhop [numeric] or [Luminescence::RLum.Results-class] (*with default*):
#' a vector of rho prime values (one for each sample) or an [Luminescence::RLum.Results-class]
#' object produced by [Luminescence::analyse_FadingMeasurement].
#'
#' @param ITL_model [character] (*with default*): model to be fitted, either
#' `"GOK"` or `"BTS"`.
#'
#' @param normalise_LxTx [logical] (*with default*):
#' whether the `LxTx` data should be normalised to its maximum value (`TRUE`
#' by default).
#'
#' @param plot [logical] (*with default*): enable/disable the plot output.
#'
#' @param verbose [logical] (*with default*): enable/disable output to the
#' terminal.
#'
#' @param txtProgressBar [logical] (*with default*): enable/disable the
#' progress bar. Ignored if `verbose = FALSE`.
#'
#' @param trace [logical] (*with default*): enable/disable tracing during
#' the nls fitting ([minpack.lm::nlsLM]).
#'
#' @param ... further arguments and graphical parameters that will be passed
#' to the `plot` function.
#'
#' @section Function version: 0.1.0
#'
#' @author
#' Svenja Riedesel, DTU Risø (Denmark)\cr
#' Sebastian Kreutzer, F2.1 Geophysical Parametrisation/Regionalisation, LIAG - Institute for Applied Geophysics (Germany)\cr
#' Marco Colombo, Institute of Geography, Heidelberg University (Germany)
#'
#' @keywords datagen
#'
#' @return
#' The function returns an [Luminescence::RLum.Results-class] object and an *optional* plot.
#' The object returned contains the following elements:
#'
#' \item{fit}{[list] with the fitted models}
#' \item{coefs}{[data.frame] containing the fitted coefficients for the models}
#' \item{data}{[data.frame] containing the data used in the fitting process}
#'
#' It may return `NULL` if no model could be fitted.
#'
#' @seealso [Luminescence::analyse_ThermochronometryData], [Luminescence::analyse_FadingMeasurement]
#'
#' @references
#' Li, B., Li, S.H., The effect of band-tail states on the
#' thermal stability of the infrared stimulated luminescence
#' from K-feldspar, Journal of Luminescence 136 (2013) 5–10.
#' doi: 10.1016/j.jlumin.2012.08.043
#'
#' @examples
#' # example code ##TODO
#'
#' @noRd
fit_IsothermalHolding <- function(
data,
rhop,
ITL_model = c("GOK", "BTS"),
normalise_LxTx = TRUE,
plot = TRUE,
verbose = TRUE,
txtProgressBar = TRUE,
trace = FALSE,
...
) {
.set_function_name("fit_IsothermalHolding")
on.exit(.unset_function_name(), add = TRUE)
## TODOs
## - other functions for fitting need to be implemented
## - uncertainties are not yet considered for the fitting, because they are not
## part of the input data.
## - documentation needs to be completed
## - so far non confidence intervals on the fit ... discussion needed
## - the rhop value has uncertainties, which are not yet considered
.validate_class(data, c("character", "RLum.Results", "data.frame"))
.validate_class(rhop, c("numeric", "RLum.Results"))
ITL_model <- .validate_args(ITL_model, c("GOK", "BTS"))
.validate_logical_scalar(normalise_LxTx)
.validate_logical_scalar(plot)
.validate_logical_scalar(verbose)
.validate_logical_scalar(txtProgressBar)
if (inherits(data[1], "character")) {
records_ITL <- .import_ThermochronometryData(file = data, output_type = "RLum.Results")@data$ITL
} else if (inherits(data, "RLum.Results")) {
.validate_originator(data, ".import_ThermochronometryData")
records_ITL <- data@data$ITL
} else if (inherits(data, "data.frame")) {
records_ITL <- data
}
if (!all(colnames(records_ITL) %in%
c("SAMPLE", "TEMP", "TIME", "LxTx", "LxTx_ERROR"))) {
.throw_error("'data' has the wrong column headers, please check the manual")
}
## never show the progress bar if not verbose
if (!verbose) {
txtProgressBar <- FALSE
}
###### --- Extract data from RLum.Results for ITL fitting --- #####
## get unique sample names; we will use this to filter the data
sample_id <- unique(records_ITL[["SAMPLE"]])
## data normalisation
if (normalise_LxTx) {
records_ITL$LxTx <- .normalise_curve(records_ITL$LxTx, "max")
}
## extract data.frames for each sample with all information
df_raw_list <- lapply(sample_id, function(x) records_ITL[records_ITL$SAMPLE == x, ])
## allow to control how many random values for the s parameter should be
## generated when fitting the BTS model
extraArgs <- list(...)
num_s_values_bts <- if (ITL_model == "BTS")
.validate_positive_scalar(extraArgs$num_s_values_bts,
int = TRUE, null.ok = TRUE,
name = "'num_s_values_bts'") %||% 1000
else 1
## initialise the progress bar
if (txtProgressBar) {
num.models <- sum(sapply(df_raw_list, function(x) length(unique(x$TEMP))))
pb <- txtProgressBar(min = 0, max = num.models * num_s_values_bts,
char = "=", style = 3)
}
###### --- Perform ITL fitting --- #####
# Define variables --------------------------------------------------------
kB <- .const$kB # Boltzmann constant (eV/K)
## get the rhop value from the fading measurement analysis if available
if (inherits(rhop, "RLum.Results") && .check_originator(rhop, "analyse_FadingMeasurement"))
rhop <- rhop@data$rho_prime[[1]]
## Define formulas to fit -------------------------------------------------
##
## We define each model as a function that describes the right-hand side
## of the formula. This allows us to use the `$value` term in the BTS model
## to extract the solution of the integral, which would otherwise be
## incorrectly interpreted by nlsLM().
f_GOK <- function(A, b, Et, s10, isoT, x) {
T_K <- isoT + .const$C2K
A * exp(-rhop * log(1.8 * 3e15 * (250 + x))^3) *
(1 - (1 - b) * 10^s10 * exp(-Et / (kB * T_K)) * x)^(1 / (1 - b))
}
f_BTS <- function(A, Eu, Et, s10, isoT, x) {
T_K <- isoT + .const$C2K
## call C++ calculation part; which is a lot faster than
## repeated calls to stats::integrate
## an even faster implementation would use pure C++ for everything,
## however, then we would use minpack.lm::nls.lm() instead ... perhaps
## in the future
f_BTS_cpp_part(
x, A, Eu, s10, Et, kB, T_K, Et, rhop)
}
## switch the models
start <- switch(
ITL_model,
# https://github.com/GeorginaKing/OSLThermo/blob/5f32762e2cea87aac26d65aeabe9a434b2ce19a7/Stage2a_Fitparameters.m#L311
GOK = list(A = 1, b = 4, Et = 1.4, s10 = 9),
# https://github.com/GeorginaKing/OSLThermo/blob/5f32762e2cea87aac26d65aeabe9a434b2ce19a7/Stage2a_Fitparameters.m#L297
BTS = list(A = 1, Eu = 0.1, Et = 1.4))
lower <- switch(
ITL_model,
GOK = c(0, 0, 0, 0),
BTS = c(0, 0, 1))
upper <- switch(
ITL_model,
GOK = c(20, Inf, 3, 20),
BTS = c(20, 0.5, 3))
## Fitting ----------------------------------------------------------------
## we have a double loop situation: we have a list with n samples, and
## each sample has n temperature steps
num.fits <- 0
fitted.coefs <- NULL
fit_list <- lapply(df_raw_list, function(s) {
## extract temperatures
isoT <- unique(s$TEMP)
## run the fitting at each temperature
tmp <- lapply(isoT, function(isoT) {
## extract data to fit
tmp_fitdata <- s[s$TEMP == isoT,]
df <- data.frame(x = tmp_fitdata$TIME,
y = tmp_fitdata$LxTx)
if (ITL_model == "GOK") {
fit <- try({
minpack.lm::nlsLM(
formula = y ~ f_GOK(A, b, Et, s10, isoT, x),
data = df,
start = start,
lower = lower,
upper = upper,
control = list(
maxiter = 500
),
trace = trace)
}, silent = TRUE)
coefs <- if (!inherits(fit, "try-error")) {
coef(fit)
} else {
stats::setNames(rep(NA_real_, length(start)), names(start))
}
fitted.coefs <<- rbind(fitted.coefs,
data.frame(SAMPLE = unique(s$SAMPLE),
TEMP = isoT,
t(coefs)))
## update the progress bar
num.fits <<- num.fits + 1 # uses <<- as we are in a nested lapply()
if (txtProgressBar) {
setTxtProgressBar(pb, num.fits)
}
if (inherits(fit, "try-error"))
fit <- NA
} else if (ITL_model == "BTS") {
## run fitting with different start parameters for s10
## all.s10 <- rnorm(num_s_values_bts, mean = 9, sd = 1.5)
all.s10 <- stats::qnorm(seq(0.01, 0.99, length.out = num_s_values_bts),
mean = 9, sd = 1.5)
fit <- lapply(1:length(all.s10), function(idx) {
s10 <- all.s10[idx]
t <- try(minpack.lm::nlsLM(
formula = y ~ f_BTS(A, Eu, Et, s10, isoT, x),
data = df,
start = start,
lower = lower,
upper = upper,
control = list(
maxiter = 500
), trace = FALSE),
silent = TRUE)
## update the progress bar
num.fits <<- num.fits + 1 # uses <<- as we are in a nested lapply()
if (txtProgressBar) {
setTxtProgressBar(pb, num.fits)
}
if (inherits(t, "try-error"))
return(NULL)
return(t)
})
## pick the one with the best fit after removing those that didn't fit
fit <- .rm_NULL_elements(fit)
if (length(fit) == 0)
return(NA)
fit <- fit[[which.min(vapply(fit, stats::deviance, numeric(1)))]]
s10 <- environment(fit$m$predict)$env$s10
fitted.coefs <<- rbind(fitted.coefs,
data.frame(SAMPLE = unique(s$SAMPLE),
TEMP = isoT,
t(coef(fit)), s10))
}
## return fit
return(fit)
})
## add temperature as name to list
names(tmp) <- isoT
return(tmp)
})
## add sample names
names(fit_list) <- sample_id
if (txtProgressBar) {
close(pb)
}
if (is.null(fitted.coefs)) {
.throw_warning("No ITL model could be fitted")
return(NULL)
}
## summarize the parameters of interest (Et, s10) in terms of mean, median and
## interquartile range
ITL_params <- tapply(fitted.coefs, sample_id, function(coefs) {
data.frame(t(c(Et = c(mean = mean(coefs$Et, na.rm = TRUE),
quantile(coefs$Et, c(0.5, 0.25, 0.75), na.rm = TRUE)),
s10 = c(mean = mean(coefs$s10, na.rm = TRUE),
quantile(coefs$s10, c(0.5, 0.25, 0.75), na.rm = TRUE)))))
})
## sort the ITL_params list, as tapply() may have put the results in a
## different order when converting internally the sample names to a factor
ITL_params <- ITL_params[sample_id]
## convert to a data table
ITL_params <- rbindlist(ITL_params[sample_id], idcol = "SAMPLE")
colnames(ITL_params)[-1] <- c("Et_mean", "Et_median", "Et_Q_0.25", "Et_Q_0.75",
"s10_mean", "s10_median", "s10_Q_0.25", "s10_Q_0.75")
if (verbose) {
## silence notes raised by R CMD check
Et <- Et_Q_0.25 <- Et_Q_0.75 <- Et_median <- Et_mean <- NULL
s10 <- s10_Q_0.25 <- s10_Q_0.75 <- s10_median <- s10_mean <- NULL
## report as mean; median (IQR)
format.out <- function(x, x1, x2, x3, n = 3)
sprintf("%.*f; %.*f (%.*f, %.*f)", n, x, n, x1, n, x2, n, x3)
ITL_params[, Et := format.out(Et_mean, Et_median, Et_Q_0.25, Et_Q_0.75)]
ITL_params[, s10 := format.out(s10_mean, s10_median, s10_Q_0.25, s10_Q_0.75)]
fmt <- "%20s | %30s | %32s |\n"
cat("\n---- Isothermal holding parameters [mean; median (IQR)] ----\n\n")
cat(sprintf(fmt, "SAMPLE", "Et", "log10(s)"))
for (i in seq(nrow(ITL_params))) {
row <- ITL_params[i, ]
cat(sprintf(fmt, row$SAMPLE, row$Et, row$s10))
}
cat("\n")
ITL_params[, c("Et", "s10") := NULL]
}
## Plotting ---------------------------------------------------------------
if (plot) {
## define plot settings
plot_settings <- modifyList(
x = list(
xlim = range(lapply(df_raw_list, function(x) range(x$TIME))) * c(0.5, 10),
ylim = c(0, 1.03),
log = "x",
xlab = "Isothermal holding time [s]",
ylab = expression(paste("Norm. lumin. [", L[x]/T[x], "]")),
pch = 21,
col = grDevices::palette.colors(),
col.border = "black",
main = sample_id,
mtext = paste("Fitted with the", ITL_model, "model"),
cex = 1.0,
mfrow = if (length(unique(sample_id)) > 1) c(min(c(2, ceiling(length(sample_id)/2))),2) else NULL,
legend = TRUE,
legend.pos = "bottomleft",
legend.cex = 0.6
),
val = list(...)
)
## get x vector for the prediction later; we use the same
## value for all provided data
x <- c(1:1e+03,seq(1e+03,plot_settings$xlim[2], length.out = 10000))
## par settings
## the check for mfrow not being null prevents mfrow from being restored on
## exit when this function is called from analyse_ThermochronometryData(),
## because restoring mfrow (even if to the same value) causes the graphical
## device to start a new page
if (!is.null(plot_settings$mfrow)) {
par.default <- .par_defaults()
on.exit(par(par.default), add = TRUE)
par(cex = plot_settings$cex, mfrow = plot_settings$mfrow)
}
## now we have to loop over the samples
for (i in seq_along(sample_id)) {
## open plot area
plot(NA,NA,
xlim = plot_settings$xlim,
ylim = plot_settings$ylim,
log = plot_settings$log,
xlab = plot_settings$xlab,
ylab = plot_settings$ylab,
yaxs = "i",
main = rep_len(plot_settings$main, length(sample_id))[i])
## add plot subtitle
mtext(side = 3, plot_settings$mtext, cex = 0.7 * plot_settings$cex)
## add fitted curves
isoT <- unique(df_raw_list[[i]][["TEMP"]])
for (c in seq_along(isoT)) {
## only plot the fitted lines if the fit had worked
if (inherits(fit_list[[i]][[c]], "nls")) {
y <- stats::predict(fit_list[[i]][[c]],
newdata = data.frame(x = x),
interval = "confidence",
se.fit = TRUE)
lines(
x = x,
y = y,
col = rep_len(plot_settings$col[c], length(isoT)))
}
## plot the points (don't use matplot because this would assume
## the same length for the time vector)
df_pts <- df_raw_list[[i]][df_raw_list[[i]][["TEMP"]] == isoT[c], ]
points(
x = df_pts[["TIME"]],
y = df_pts[["LxTx"]],
xpd = TRUE,
pch = plot_settings$pch,
bg = plot_settings$col[c],
col = plot_settings$col.border)
## add error bars (if NA nothing is plotted by R default)
segments(
x0 = df_pts[["TIME"]],
x1 = df_pts[["TIME"]],
y0 = df_pts[["LxTx"]] - df_pts[["LxTx_ERROR"]],
y1 = df_pts[["LxTx"]] + df_pts[["LxTx_ERROR"]],
col = plot_settings$col[c])
}
## add legend
if (plot_settings$legend[1]) {
legend(
plot_settings$legend.pos,
legend = paste0(isoT, "\u00b0C"),
bty = "n",
pch = 20,
lty = 1,
col = plot_settings$col,
cex = plot_settings$legend.cex)
}
}## plot loop
}## plot condition
# Return results ----------------------------------------------------------
set_RLum(
class = "RLum.Results",
data = list(
ITL_params = data.frame(ITL_params),
fit = fit_list,
coefs = fitted.coefs,
data = records_ITL),
info = list(
call = sys.call())
)
}
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.