R/class.R

Defines functions set_names get_peak get_default_names get_names get_name new_rdecon as_v12_collection as_v12_singlet as_collection as_rdecons as_idecons as_ispecs as_rdecon as_idecon as_ispec as_decon2.rdecon as_decon2.idecon as_decon2.decon1 as_decon1.idecon as_decon1.decon2 as_decon1.decon0 as_decons2 as_decons1 as_decons0 as_spectra as_decon2 as_decon1 as_decon0 as_spectrum is_rdecons is_idecons is_ispecs is_rdecon is_idecon is_ispec is_spectrum_or_spectra is_aligns is_decons2 is_decons1 is_decons0 is_spectra is_align is_decon2 is_decon1 is_decon0 is_spectrum `[.collection` print.rdecons print.idecons print.ispecs print.rdecon print.idecon print.ispec print.aligns print.decons2 print.decons1 print.spectra print.align print.decon2 print.decon1 print.spectrum

Documented in as_decon0 as_decon1 as_decon2 as_decons0 as_decons1 as_decons2 as_spectra as_spectrum is_align is_aligns is_decon0 is_decon1 is_decon2 is_decons0 is_decons1 is_decons2 is_spectra is_spectrum print.align print.aligns print.decon1 print.decon2 print.decons1 print.decons2 print.spectra print.spectrum

# Print (Public) #####

#' @name print_methods
#' @rdname print_methods
#'
#' @title S3 Methods for Printing Metabodecon Objects
#'
#' @description
#' S3 Methods for printing metabodecon objects as described in the [Metabodecon
#' Classes](https://spang-lab.github.io/metabodecon/articles/).
#'
#' @param x
#' The object to print.
#'
#' @param name
#' Logical. If TRUE, the name of the object is printed before the object.
#'
#' @param ...
#' Not used. Only accepted to comply with generic [base::print()].
#'
#' @return
#' NULL, called for side effect of printing to the standard output device.
#'
#' @author 2024-2025 Tobias Schmidt: initial version.
#'
#' @examples
#' print(sim[[1]])
#' print(sim[[1]], name = TRUE)
#' print(sim)
#' decon <- deconvolute(sim[[1]], sfr = c(3.55, 3.35))
#' print(decon)
NULL

#' @export
#' @rdname print_methods
print.spectrum <- function(x, name = FALSE, ...) {
    namestr <- if (name) paste0(x$meta$name %||% "NULL", ": ") else ""
    fmt <- "%sspectrum object (%d dp, %.1f to %.1f ppm)\n"
    catf(fmt, namestr, length(x$cs), max(x$cs), min(x$cs))
}

#' @export
#' @rdname print_methods
print.decon1 <- function(x, name = FALSE, ...) {
    ppm <- x$x_values_ppm
    n <- length(ppm)
    name <- if (name) paste0(x$filename %||% "NULL", ": ") else ""
    fmt <- "%sdecon1 object (%d dp, %.1f to %.1f ppm, %d peaks)\n"
    catf(fmt, name, n, max(ppm), min(ppm), length(x$A))
}

#' @export
#' @rdname print_methods
print.decon2 <- function(x, name = FALSE, ...) {
    name <- if (name) paste0(x$meta$name %||% "NULL", ": ") else ""
    fmt <- "%sdecon2 object (%d dp, %.1f to %.1f ppm, %d peaks)\n"
    catf(fmt, name, length(x$cs), max(x$cs), min(x$cs), length(x$lcpar$A))
}

#' @export
#' @rdname print_methods
print.align <- function(x, name = FALSE, ...) {
    name <- if (name) paste0(x$meta$name %||% "NULL", ": ") else ""
    fmt <- "%salign object (%d dp, %.1f to %.1f ppm, %d peaks)\n"
    catf(fmt, name, length(x$cs), max(x$cs), min(x$cs), length(x$lcpar$A))
}

#' @export
#' @rdname print_methods
print.spectra <- function(x, ...) {
    msg <- "spectra object consisting of %d spectrum objects:\n"
    catf(msg, length(x, ...))
    nams <- get_names(x, ...)
    msg <- "%s (%d datapoints from %.2f - %.2f ppm)\n"
    mapply(x, ..., nams, FUN = function(x, nam) {
        catf(msg, nam, length(x$si), min(x$cs), max(x$cs))
    })
    invisible(NULL)
}

#' @export
#' @rdname print_methods
print.decons1 <- function(x, ...) {
    catf("decons1 object with %s decon1 elements\n", length(x))
    invisible(sapply(x, print, name = TRUE))
}

#' @export
#' @rdname print_methods
print.decons2 <- function(x, ...) {
    catf("decons2 object with %s decon2 elements\n", length(x))
    invisible(sapply(x, print, name = TRUE))
}

#' @export
#' @rdname print_methods
print.aligns <- function(x, ...) {
    catf("aligns object with %s align elements\n", length(x))
    invisible(sapply(x, print, name = TRUE))
}

# Print (Private) #####

#' @export
print.ispec <- function(x, name = FALSE, ...) {
    name <- {
        if (isTRUE(name)) paste0(get_name(x) %||% "NULL", ": ")
        else if (is.character(name)) paste0(name, ": ")
        else ""
    }
    fmt <- "%sispec object (%d dp, %.1f to %.1f ppm)\n"
    catf(fmt, name, length(x$ppm), max(x$ppm), min(x$ppm))
}

#' @export
print.idecon <- function(x, name = FALSE, ...) {
    name <- {
        if (isTRUE(name)) paste0(get_name(x) %||% "NULL", ": ")
        else if (is.character(name)) paste0(name, ": ")
        else ""
    }
    fmt <- "%sidecon object (%d dp, %.1f to %.1f ppm, %d peaks)\n"
    catf(fmt, name, length(x$ppm), max(x$ppm), min(x$ppm), length(x$lcr$A))
}

#' @export
print.rdecon <- function(x, name = FALSE, ...) {
    name <- {
        if (isTRUE(name)) paste0(get_name(x) %||% "NULL", ": ")
        else if (is.character(name)) paste0(name, ": ")
        else ""
    }
    catf("%srdecon object\n", name)
}

#' @export
print.ispecs <- function(x, ...) {
    catf("ispecs object with %s ispec elements\n", length(x))
    nams <- get_names(x)
    invisible(mapply(print, x, nams))
}

#' @export
print.idecons <- function(x, ...) {
    catf("idecons object with %s idecon elements\n", length(x))
    nams <- get_names(x)
    invisible(mapply(print, x, nams))
}

#' @export
print.rdecons <- function(x, ...) {
    catf("rdecons object with %s rdecon elements\n", length(x))
    nams <- get_names(x)
    invisible(mapply(print, x, nams))
}

# Subset (Private) #####

#' @noRd
#' @author 2024-2025 Tobias Schmidt: initial version.
`[.collection` <- function(x, i, ...) {
    result <- NextMethod("[")
    class(result) <- class(x)
    result
}

#' @export
`[.spectra` <- `[.collection`

#' @export
`[.decons0` <- `[.collection`

#' @export
`[.decons1` <- `[.collection`

#' @export
`[.decons2` <- `[.collection`

#' @export
`[.aligns` <- `[.collection`

#' @export
`[.ispecs` <- `[.collection`

#' @export
`[.idecons` <- `[.collection`

#' @export
`[.rdecons` <- `[.collection`

# Checks (Public) #####

#' @export
#'
#' @name is_metabodecon_class
#'
#' @title Is an Object from a Metabodecon Class?
#'
#' @description
#' Check if an object is an instance of a specific 'Metabodecon Class'. See
#' [Metabodecon
#' Classes](https://spang-lab.github.io/metabodecon/articles/Classes.html) for a
#' list of classes.
#'
#' @param x
#' The object to check.
#'
#' @param check_class
#' Logical indicating whether to check the class of the object.
#'
#' @param check_contents
#' Logical indicating whether to check the contents of the object.
#'
#' @param check_child_classes
#' Logical indicating whether to check the class of each element of the object.
#'
#' @return
#' TRUE if the object is an instance of the specified class, otherwise FALSE.
#'
#' @author 2024-2025 Tobias Schmidt: initial version.
#'
#' @examples
#' ss <- sim[1:2]
#' s1 <- sim[[1]]
#' is_spectra(ss) # TRUE
#' is_spectrum(s1) # TRUE
#' is_spectrum(s1, check_contents = TRUE) # TRUE
#'
#' dd <- deconvolute(ss, sfr = c(3.55, 3.35))
#' d1 <- dd[[1]]
#' is_decons0(dd) # FALSE
#' is_decons1(dd) # FALSE
#' is_decons2(dd) # TRUE
#' is_decon0(d1) # FALSE
#' is_decon1(d1) # FALSE
#' is_decon2(d1) # TRUE
#'
#' if (interactive()) {
#'     # Example requires an interactive R session, because in case of missing
#'     # dependencies the user will be asked for confirmation to install them.
#'     aa <- align(dd)
#'     a1 <- aa[[1]]
#'     is_align(a1) # TRUE
#'     is_aligns(aa) # TRUE
#' }
#'
is_spectrum <- function(x,
                        check_class = TRUE,
                        check_contents = FALSE) {
    # styler: off
    if (check_class && !inherits(x, "spectrum")) return(FALSE)
    if (!check_contents) return(TRUE)
    if (!is.list(x)) return(FALSE)
    mandatory <- c("si", "cs")
    if (!all(mandatory %in% names(x))) return(FALSE)
    # styler: on
    return(TRUE)
}

#' @export
#' @rdname is_metabodecon_class
is_decon0 <- function(x) {
    is.list(x) && all(decon0_members_mandatory %in% names(x)) && !is_decon1(x)
}

#' @export
#' @rdname is_metabodecon_class
is_decon1 <- function(x) inherits(x, "decon1")

#' @export
#' @rdname is_metabodecon_class
is_decon2 <- function(x) inherits(x, "decon2")

#' @export
#' @rdname is_metabodecon_class
is_align <- function(x) inherits(x, "align")

#' @export
#' @rdname is_metabodecon_class
is_spectra <- function(x,
                       check_class = TRUE,
                       check_contents = FALSE,
                       check_child_classes = FALSE) {
    # styler: off
    if (check_class && !inherits(x, "spectra")) return(FALSE)
    if (check_child_classes && !all(sapply(x, is_spectrum))) return(FALSE)
    if (!check_contents) return(TRUE)
    if (!is.list(x)) return(FALSE)
    if (!all(sapply(x, is_spectrum, check_contents = TRUE))) return(FALSE)
    # styler: on
    return(TRUE)
}

#' @export
#' @rdname is_metabodecon_class
is_decons0 <- function(x) all(sapply(x, is_decon0))

#' @export
#' @rdname is_metabodecon_class
is_decons1 <- function(x) inherits(x, "decons1")

#' @export
#' @rdname is_metabodecon_class
is_decons2 <- function(x) inherits(x, "decons2")

#' @export
#' @rdname is_metabodecon_class
is_aligns <- function(x) inherits(x, "aligns")

# Checks (Private) #####

is_spectrum_or_spectra <- function(x) is_spectrum(x) || is_spectra(x)
is_ispec <- function(x) inherits(x, "ispec")
is_idecon <- function(x) inherits(x, "idecon")
is_rdecon <- function(x) inherits(x, "rdecon")
is_ispecs <- function(x) inherits(x, "ispecs")
is_idecons <- function(x) inherits(x, "idecons")
is_rdecons <- function(x) inherits(x, "rdecons")

# Convert (Public) #####

#' @export
#'
#' @name as_metabodecon_class
#' @rdname as_metabodecon_class
#'
#' @title Convert to a Metabodecon Object
#'
#' @description Convert a object to a Metabodecon object.
#'
#' @param x
#' The object to convert.
#'
#' @param sf
#' Scale factor used during Only required if `x` is a decon0 object.
#'
#' @param sfs
#' List of scale factors. Only required if `x` is a list of decon0 objects.
#'
#' @param spectrum,spectra
#' The `spectrum`/`spectra` object corresponding to `x` as returned by
#' [read_spectrum()] / [read_spectra]. Only required if `x` is a decon0 object.
#'
#' @param sfr,sfrs
#' `sfr` should be a vector specifying the borders of the signal free region.
#' `sfrs` should be a list of such vectors. Only required if `x` is a `decon0`
#' object where element `signal_free_region` is missing (or a `decons0` objected
#' containing such `decon0` objects).
#'
#' @param wshw,wshws
#' `wshw` should specify the half width of the water signal region. `wshws`
#' should be a list of such values. Only required if `x` is a `decon0` object
#' where element `range_water_signal_ppm` is missing (or a `decons0` objected
#' containing such `decon0` objects).
#'
#' @param bwc
#' Level of backwards compatibility. If `bwc == 0`, bug fixes introduced after
#' version 0.2.2 of Metabodecon are not used. If `bwc == 1`, new features
#' introduced after version 0.2.2 of Metabodecon (e.g. faster algorithms) are
#' not used. If `bwc == 2`, all bug fixes and features introduced after version
#' 0.2.2 are used. Support for `bwc == 0` will be removed in 'metabodecon v2.0'.
#'
#' @param optional
#' Logical. If `TRUE`, the two optional elements `signal_free_region` and
#' `range_water_signal_ppm` are included in the returned `decon0` object.
#'
#' @param nworkers
#' Number of workers for parallel processing.
#'
#' @return An object of the specified class.
#'
#' @author 2024-2025 Tobias Schmidt: initial version.
#'
#' @examples
#' dirpath <- metabodecon_file("sim_subset")
#' spectra <- read_spectra(dirpath)
#' spectrum <- spectra[[1]]
#' decons1 <- generate_lorentz_curves_sim(spectra)
#' decon1 <- generate_lorentz_curves_sim(spectrum)
#' decon2 <- as_decon2(decon1)
as_spectrum <- function(x, sf = c(1e3, 1e6)) {
    if (is_spectrum(x)) {
        return(x)
    } else if (is_decon1(x)) {
        cs <- x$x_values_ppm
        si <- x$y_values_raw %||% (x$y_values * sf[2])
        name <- x$filename
        fq <- x$x_values_hz
        meta <- named(name, fq)
        obj <- named(cs, si, meta)
        return(structure(obj, class = "spectrum"))
    } else if (is_ispec(x)) {
        cs <- x$ppm
        si <- x$y_raw
        meta <- x$meta
        obj <- named(cs, si, meta)
        return(structure(obj, class = "spectrum"))
    } else {
        msg <- "Converting %s to spectrum is not suppoorted"
        msg <- sprintf(msg, class(x)[1])
        stop(msg)
    }
}

#' @export
#' @rdname as_metabodecon_class
as_decon0 <- function(x,
                      sf = NULL,
                      spectrum = NULL,
                      optional = TRUE) {
    if (is_decon0(x)) return(x)
    y <- as_decon1(x)
    y <- unclass(y)
    y[if (optional) decon0_members else decon0_members_mandatory]
}

#' @export
#' @rdname as_metabodecon_class
as_decon1 <- function(x,
                      sf = c(1e3, 1e6),
                      spectrum = NULL,
                      sfr = NULL,
                      wshw = NULL,
                      bwc = 2) {
    if (is_decon0(x)) as_decon1.decon0(x, sf, spectrum, sfr, wshw, bwc)
    else if (is_decon1(x)) x
    else if (is_decon2(x)) as_decon1.decon2(x, sf, spectrum, sfr, wshw, bwc)
    else if (is_idecon(x)) as_decon1.idecon(x, sf, spectrum, sfr, wshw, bwc)
    else stop(sprintf("Converting %s to decon1 is not supported", class(x)[1]))
}

#' @export
#' @rdname as_metabodecon_class
as_decon2 <- function(x, sf = c(1e3, 1e6), spectrum = NULL, sfr = NULL, wshw = NULL, bwc = 2) {
    if (is_decon0(x)) as_decon2.decon1(x = as_decon1(x, sf, spectrum, sfr, wshw, bwc))
    else if (is_decon1(x)) as_decon2.decon1(x)
    else if (is_decon2(x)) x
    else if (is_idecon(x)) as_decon2.idecon(x)
    else if (is_rdecon(x)) as_decon2.rdecon(x)
    else stop(sprintf("Converting %s to decon2 is not supported", class(x)[1]))
}

#' @export
#' @rdname as_metabodecon_class
#' @inheritParams read_spectra
as_spectra <- function(x,
                       file_format = "bruker",
                       expno = 10,
                       procno = 10,
                       raw = FALSE,
                       silent = TRUE,
                       force = FALSE) {
    if (is_spectrum(x)) {
        xx <- structure(list(x), class = "spectra")
        xx <- set_names(xx, get_names(xx))
    } else if (all(sapply(x, is_spectrum))) {
        xx <- structure(x, class = "spectra")
        xx <- set_names(xx, get_names(xx))
    } else if (is.character(x) && file.exists(x)) {
        xx <- read_spectra(x, file_format, expno, procno, raw, silent, force)
    } else {
        stop("Input must be a path, spectrum or list of spectrum objects")
    }
    xx
}

#' @export
#' @rdname as_metabodecon_class
as_decons0 <- function(x,
                       sfs = list(c(1e3, 1e6)),
                       spectra = list(NULL),
                       nworkers = 1) {
    if (is_decons0(x)) {
        return(x)
    } else if (is_decons1(x) || is_decons2(x) || is_idecons(x)) {
        decons0 <- mcmapply(as_decon0, x, sfs, spectra, nw = nworkers)
    } else if (is.list(x) && all(sapply(x, is_decon0))) {
        decons0 <- x
    } else {
        stop(paste(
            "Input must be a list of decon0 objects or a single object",
            "of type decons0, decons1, decons2 or idecons."
        ))
    }
    # Don't set names or class for decons0, as the original MetaboDecon1D
    # objects didn't have names or classes as well and we want to stay backwards
    # compatible. If someone wants to have names, they can use `decons1` or
    # `decons2` instead.
    n <- length(decons0)
    for (i in seq_len(n)) decons0[[i]]$number_of_files <- n
    decons0
}

#' @export
#' @rdname as_metabodecon_class
as_decons1 <- function(x,
                       sfs = list(c(1e3, 1e6)),
                       spectra = list(NULL),
                       sfrs = list(NULL),
                       wshws = list(NULL),
                       bwc = 2,
                       nworkers = 1) {
    if (is_decons1(x)) {
        return(x)
    } else if (is_decons0(x) || is_decons2(x) || is_idecons(x)) {
        decons1 <- mcmapply(as_decon1, x, sfs, spectra, sfrs, wshws, bwc, nw = nworkers)
    } else if (is.list(x) && all(sapply(x, is_decon1))) {
        decons1 <- x
    } else {
        stop(paste(
            "Input must be a list of decon1 objects or a single object",
            "of type decons0, decons1, decons2 or idecons."
        ))
    }
    names(decons1) <- get_names(x)
    class(decons1) <- "decons1"
    n <- length(decons1)
    for (i in seq_len(n)) decons1[[i]]$number_of_files <- n
    decons1
}

#' @export
#' @rdname as_metabodecon_class
as_decons2 <- function(x,
                       sfs = list(c(1e3, 1e6)),
                       spectra = list(NULL),
                       sfrs = list(NULL),
                       wshws = list(NULL),
                       bwc = 2,
                       nworkers = 1) {
    if (is_decons2(x)) {
        return(x)
    } else if (is_decons0(x) || is_decons1(x) || is_idecons(x)) {
        decons2 <- mcmapply(as_decon2, x, sfs, spectra, sfrs, wshws, bwc, nw = nworkers)
    } else if (is.list(x) && all(sapply(x, is_decon2))) {
        decons2 <- x
    } else {
        stop(paste(
            "Input must be a list of decon2 objects or a single object",
            "of type decons0, decons1, decons2 or idecons."
        ))
    }
    names(decons2) <- get_names(x)
    class(decons2) <- "decons2"
    decons2
}

# Convert (Private) #####

as_decon1.decon0 <- function(x,
                            sf = c(1e3, 1e6),
                            spectrum = NULL,
                            sfr = NULL,
                            wshw = NULL,
                            bwc = 2) {
    if (is.null(sf)) stop("Please provide `sf`")
    if (is.null(spectrum)) stop("Please provide `spectrum`")
    # Define some shorthands
    fq <- spectrum$meta$fq
    si <- spectrum$si
    ssp <- as.numeric(x$spectrum_superposition)
    ppm <- x$x_values_ppm
    sdp <- x$x_values
    dp <- round(x$x_values * sf[1])
    y <- x
    # Append optional elements if missing
    if (is.null(x[["signal_free_region"]])) {
        if (is.null(sfr)) stop("Please provide `sfr`")
        y[["signal_free_region"]] <- sfr_in_sdp_bwc(sfr, ppm, sf)
    }
    if (is.null(x[["range_water_signal_ppm"]])) {
        if (is.null(wshw)) stop("Please provide `wshw`")
        y[["range_water_signal_ppm"]] <- wshw
    }
    # Make sure elements are in correct order
    y <- y[decon0_members]
    # Calculate decon1 elements
    y$y_values_raw <- si
    y$x_values_hz <- fq
    y$mse_normed_raw <- mse(si, ssp, normed = TRUE)
    y$signal_free_region_ppm <- sfr %||% sfr_in_ppm_bwc(x[["signal_free_region"]], sdp, ppm)
    y$x_0_hz <- convert_pos(x$x_0, sdp, fq)
    y$x_0_dp <- convert_pos(x$x_0, sdp, dp)
    y$x_0_ppm <- convert_pos(x$x_0, sdp, ppm)
    y$A_hz <- convert_width(x$A, sdp, fq)
    y$A_dp <- convert_width(x$A, sdp, dp)
    y$A_ppm <- convert_width(x$A, sdp, ppm)
    y$lambda_hz <- convert_width(x$lambda, sdp, fq)
    y$lambda_dp <- convert_width(x$lambda, sdp, dp)
    y$lambda_ppm <- convert_width(x$lambda, sdp, ppm)
    class(y) <- "decon1"
    y
}

as_decon1.decon2 <- function(x, sf, spectrum, sfr, wshw, bwc) {
    # Helper vars
    cs <- x$cs
    si <- x$si
    n <- length(si)
    dpn <- (n - 1):0
    sdp <- dpn / 1e3
    fq <- x$meta$fq
    cs_step <- width(cs) / (n - 1)
    dpn_step <- 1
    fq_step <- if (!is.null(fq)) width(fq) / (n - 1)
    sdp_step <- dpn_step / 1e3
    x0_ppm <- x$lcpar$x0
    A_raw_ppm <- x$lcpar$A
    lambda_ppm <- x$lcpar$lambda
    x0_dp <- convert_pos(x0_ppm, cs, dpn)
    x0_sdp <- convert_pos(x0_ppm, cs, sdp)
    x0_hz <- if (!is.null(fq)) convert_pos(x0_ppm, cs, fq)
    A_raw_dp <- A_raw_ppm * (dpn_step / cs_step)
    A_raw_sdp <- A_raw_ppm * (sdp_step / cs_step)
    A_raw_hz <- if (!is.null(fq)) A_raw_ppm * (fq_step / cs_step)
    A_sc_ppm <- A_raw_ppm / 1e6
    A_sc_dp <- A_raw_dp / 1e6
    A_sc_sdp <- A_raw_sdp / 1e6
    A_sc_hz <- if (!is.null(fq)) A_raw_hz / 1e6
    lambda_dp <- convert_width(lambda_ppm, cs, dpn)
    lambda_sdp <- convert_width(lambda_ppm, cs, sdp)
    lambda_hz <- if (!is.null(fq)) abs(convert_width(lambda_ppm, cs, fq))
    limits_sdp <- if (bwc < 1) c(0, max(sdp) + sdp_step) else NULL
    integrals <- t(lorentz_int(x0_sdp, A_sc_sdp, lambda_sdp, limits = limits_sdp))
    # Outputs
    y <- structure(class = "decon1", .Data = list())
    y$number_of_files <- 1
    y$filename <- x$meta$name
    y$x_values <- seq.int(length(x$cs) - 1, 0, -1) / sf[1]
    y$x_values_ppm <- x$cs
    y$y_values <- x$sit$sm / 1e6
    y$spectrum_superposition <- t(x$sit$sup / 1e6)
    y$mse_normed <- x$mse$smnorm
    y$index_peak_triplets_middle <- x$peak$center
    y$index_peak_triplets_left <- x$peak$right # decon[01] has left and right inverted
    y$index_peak_triplets_right <- x$peak$left # decon[01] has left and right inverted
    y$peak_triplets_middle <- x$cs[x$peak$center]
    y$peak_triplets_left <- x$cs[x$peak$right] # decon[01] has left and right inverted
    y$peak_triplets_right <- x$cs[x$peak$left] # decon[01] has left and right inverted
    sdp <- ((length(x$cs) - 1):0) / sf[1]
    y$integrals <- integrals
    y$signal_free_region <- sfr_in_sdp_bwc(x$args$sfr, x$cs, sf)
    y$range_water_signal_ppm <- x$args$wshw
    y$A <- -A_sc_sdp
    y$lambda <- -lambda_sdp
    y$x_0 <- x0_sdp
    y$y_values_raw <- x$si
    y$x_values_hz <- if (!is.null(fq)) x$meta$fq
    y$mse_normed_raw <- x$mse$norm
    y$signal_free_region_ppm <- x$args$sfr
    y$x_0_hz <- if (!is.null(fq)) x0_hz
    y$x_0_dp <- x0_dp
    y$x_0_ppm <- x0_ppm
    y$A_hz <- if (!is.null(fq)) (A_sc_hz)
    y$A_dp <- -A_sc_dp
    y$A_ppm <- -A_sc_ppm
    y$lambda_hz <- if (!is.null(fq)) (lambda_hz)
    y$lambda_dp <- -lambda_dp
    y$lambda_ppm <- -lambda_ppm
    y
}

as_decon1.idecon <- function(x, sf, spectrum, sfr, wshw, bwc) {
    y <- structure(class = "decon1", .Data = list())
    y$number_of_files <- 1
    y$filename <- x$name
    y$x_values <- x$sdp
    y$x_values_ppm <- x$ppm
    y$y_values <- x$y_smooth
    y$spectrum_superposition <- t(s <- lorentz_sup(x = x$sdp, lcpar = x$lcr))
    y$mse_normed <- mse(x$y_smooth, s, normed = TRUE)
    y$index_peak_triplets_middle <- as.numeric(x$peak$center[x$peak$high])
    y$index_peak_triplets_left <- as.numeric(x$peak$right[x$peak$high]) # decon[01] has left and right inverted
    y$index_peak_triplets_right <- as.numeric(x$peak$left[x$peak$high]) # decon[01] has left and right inverted
    y$peak_triplets_middle <- x$ppm[x$peak$center[x$peak$high]]
    y$peak_triplets_left <- x$ppm[x$peak$right[x$peak$high]] # decon[01] has left and right inverted
    y$peak_triplets_right <- x$ppm[x$peak$left[x$peak$high]] # decon[01] has left and right inverted
    y$integrals <- t(x$lcr$integrals)
    y$signal_free_region <- sfr_in_sdp_bwc(x$args$sfr, x$ppm, sf)
    y$range_water_signal_ppm <- x$args$wshw
    y$A <- x$lcr$A
    y$lambda <- x$lcr$lambda
    y$x_0 <- x$lcr$w
    y$y_values_raw <- x$y_raw
    y$x_values_hz <- x$hz
    y$mse_normed_raw <- mse(x$y_raw, s, normed = TRUE)
    y$signal_free_region_ppm <- x$args$sfr
    y$x_0_hz <- convert_pos(x$lcr$w, x$sdp, x$hz)
    y$x_0_dp <- convert_pos(x$lcr$w, x$sdp, x$dp)
    y$x_0_ppm <- convert_pos(x$lcr$w, x$sdp, x$ppm)
    y$A_hz <- convert_width(x$lcr$A, x$sdp, x$hz)
    y$A_dp <- convert_width(x$lcr$A, x$sdp, x$dp)
    y$A_ppm <- convert_width(x$lcr$A, x$sdp, x$ppm)
    y$lambda_hz <- convert_width(x$lcr$lambda, x$sdp, x$hz)
    y$lambda_dp <- convert_width(x$lcr$lambda, x$sdp, x$dp)
    y$lambda_ppm <- convert_width(x$lcr$lambda, x$sdp, x$ppm)
    y
}

#' @noRd
#' @author 2024-2025 Tobias Schmidt: initial version.
as_decon2.decon1 <- function(x, ...) {
    cs <- x$x_values_ppm
    si <- x$y_values_raw
    meta <- list(
        name = x$filename,
        fq = x$x_values_hz
    )
    args <- list(
        nfit = NA, smopts = NA, delta = NA,
        sfr = sfr_in_ppm_bwc(x$signal_free_region, x$x_values, x$x_values_ppm),
        wshw = x$range_water_signal_ppm,
        ask = NA, force = NA, verbose = NA, bwc = NA, nworkers = NA
    )
    sit <- data.frame(
        wsrm = NA, nvrm = NA,
        sm = x$y_values * 1e6,
        sup = x$spectrum_superposition[1, ] * 1e6
    )
    peak <- data.frame(
        left = x$index_peak_triplets_right, # decon[01] has left and right inverted
        center = x$index_peak_triplets_middle,
        right = x$index_peak_triplets_left
    )
    lcpar <- data.frame(
        x0 = x$x_0_ppm,
        A = -(x$A_ppm * 1e6),
        lambda = -(x$lambda_ppm)
    )
    mse <- list(
        raw  = mse(si, sit$sup, normed = FALSE),
        norm = x$mse_normed_raw,
        sm = mse(sit$sm, sit$sup, normed = FALSE),
        smnorm = x$mse_normed
    )
    obj <- named(cs, si, meta, args, sit, peak, lcpar, mse)
    class(obj) <- "decon2"
    obj
}

#' @noRd
#' @author 2024-2025 Tobias Schmidt: initial version.
as_decon2.idecon <- function(x, ...) {
    cs <- x$ppm
    si <- x$y_raw
    meta <- x$meta
    args <- x$args
    lcpar <- data.frame(
        x0 = convert_pos(x$lcr$w, x$sdp, x$ppm),
        A = -convert_width(x$lcr$A, x$sdp, x$ppm) * 1e6,
        lambda = -convert_width(x$lcr$lambda, x$sdp, x$ppm)
    )
    sit <- data.frame(
        wsrm = x$y_nows * 1e6,
        nvrm = x$y_pos * 1e6,
        sm = x$y_smooth * 1e6,
        sup = lorentz_sup(cs, lcpar = lcpar)
    )
    peak <- data.frame(
        left = x$peak$left[x$peak$high],
        center = x$peak$center[x$peak$high],
        right = x$peak$right[x$peak$high]
    )
    mse <- list(
        raw = mse(si, sit$sup, normed = FALSE),
        norm = mse(si, sit$sup, normed = TRUE),
        sm = mse(sit$sm, sit$sup, normed = FALSE),
        smnorm = mse(sit$sm, sit$sup, normed = TRUE)
    )
    obj <- named(cs, si, meta, args, sit, peak, lcpar, mse)
    class(obj) <- "decon2"
    obj
}

#' @noRd
#' @author 2024-2025 Tobias Schmidt: initial version.
as_decon2.rdecon <- function(x, ...) {
    assert(is_rdecon(x))
    cs <- x$mdrb_spectrum$chemical_shifts()
    si <- x$mdrb_spectrum$intensities()
    meta <- x$spectrum$meta
    args <- x$args
    sup <- x$mdrb_decon$superposition_vec(cs)
    lcpar <- as.data.frame(x$mdrb_decon$lorentzians())
    wsrm <- si # Rust backend doesn't remove the water signal
    nvrm <- si # Rust backend doesn't remove negative values
    spec <- list(y_pos = nvrm)
    reps <- args$smopts[[1]]
    size <- args$smopts[[2]]
    sm <- smooth_signals(spec, reps, size, verbose = FALSE)$y_smooth
    sit <- named(wsrm, nvrm, sm, sup)
    mse <- list(
        raw = mse(si, sup, norm=FALSE), # (1)
        norm = mse(si, sup, norm=TRUE),
        sm = mse(sm, sup, norm=FALSE),
        smnorm = mse(sm, sup, norm=TRUE)
        # (1) x$mdrb_decon$mse() deviates from mse() results, so we need to
        # calculate ourselves until Rust backend provides the correct values
        # (see TODOS.md). (Update 2025-09-14: TODOS are no longer tracked in
        # TODOS.md, but outside of the repository. To retrieve the last actively
        # maintained version of TODOS.md, checkout commit 8b1f61b, i.e.,
        # v1.5.0.)
    )
    peak <- get_peak(lcpar$x0, cs) # Should be provided directly by Rust backend in future versions
    obj <- named(cs, si, meta, args, sit, peak, lcpar, mse)
    class(obj) <- "decon2"
    obj
}

#' @noRd
#' @author 2024-2025 Tobias Schmidt: initial version.
as_ispec <- function(x, sf = c(1e3, 1e6)) {
    if (is_ispec(x)) {
        return(x)
    }
    if (is_char(x)) x <- read_spectrum(x)
    s <- if (is_spectrum(x)) x else as_spectrum(x)
    y_raw <- s$si # Raw signal intensities
    y_scaled <- y_raw / sf[2] # Scaled signal intensities
    n <- length(y_raw) # Number of data points
    dp <- seq(n - 1, 0, -1) # Data point numbers
    sdp <- seq((n - 1) / sf[1], 0, -1 / sf[1]) # Scaled data point numbers [^1]
    ppm <- s$cs # Parts per million
    hz <- s$meta$fq # Frequency in Hz
    ppm_range <- diff(range(s$cs)) # Range of the chemical shifts in ppm.
    ppm_max <- max(s$cs) # Maximum chemical shift in ppm.
    ppm_min <- min(s$cs) # Minimum chemical shift in ppm.
    ppm_step <- ppm_range / (n - 1) # Step size calculated correctly.
    ppm_nstep <- ppm_range / n # Wrong, but backwards compatible [^2].
    name <- s$meta$name # Name of the spectrum
    meta <- s$meta # Other Metadata to the spectrum
    g <- named(
        y_raw, y_scaled, n, dp, sdp, sf, ppm, hz,
        ppm_range, ppm_max, ppm_min, ppm_step, ppm_nstep,
        name, meta
    )
    structure(g, class = "ispec")
    # [^1]: Same as `dp / sf[1]`, but with slight numeric differences, so we
    #       stick with the old calculation method for backwards compatibility.
    # [^2]: Example: ppm = 11, 23, 35, 47 ==> ppm_step == 12, ppm_nstep ~= 10.6
    #       (not really useful, but we need it for backwards compatibility with
    #       MetaboDecon1D results)
}

#' @noRd
#' @author 2024-2025 Tobias Schmidt: initial version.
as_idecon <- function(x) {
    if (is_idecon(x)) {
        x
    } else if (is_rdecon(x)) {
        y_raw <- x$spectrum$si
        ppm <- x$spectrum$cs
        hz <- x$spectrum$meta$fq
        name <- x$spectrum$meta$name
        meta <- x$spectrum$meta
        args <- x$args
        lcpar <- x$mdrb_decon$lorentzians()
        n <- length(y_raw)
        dp <- seq(n - 1, 0, -1)
        sdp <- dp / 1e3
        sf <- c(1e3, 1e6)
        ppm_range <- width(ppm)
        ppm_max <- max(ppm)
        ppm_min <- min(ppm)
        ppm_step <- ppm_range / (n - 1)
        ppm_nstep <- ppm_range / n
        y_scaled <- y_raw / 1e6
        y_nows <- y_scaled
        y_pos <- abs(y_scaled)
        y_smooth <- smooth_signals(named(y_pos), args$smopts[[1]], args$smopts[[2]], verbose = FALSE)$y_smooth
        d <- (calc_second_derivative(y_smooth))
        A <- (-convert_width(lcpar$A, ppm, sdp) / 1e6)
        lambda <- (-convert_width(lcpar$lambda, ppm, sdp))
        w <- convert_pos(lcpar$x0, ppm, sdp)
        integrals <- (-A) * pi
        lcr <- named(A, lambda, w, integrals)
        center <- round(convert_pos(lcpar$x0, ppm, 1:n))
        npeaks <- length(center)
        left <- center - 1
        center <- center
        right <- center + 1
        score <- rep(999, npeaks)
        high <- rep(TRUE, npeaks)
        region <- rep("norm", npeaks)
        peak <- data.frame(left, center, right, score, high, region)
        Z <- lci <- lca <- NA
        x <- named(
            y_raw, y_scaled, n, dp, sdp, sf, ppm, hz,
            ppm_range, ppm_max, ppm_min, ppm_step, ppm_nstep,
            name, meta,
            args, y_nows, y_pos, Z, y_smooth, d, peak, lci, lca, lcr
        )
        structure(x, class = "idecon")
    } else if (all(idecon_members %in% names(x))) {
        structure(x, class = "idecon")
    } else {
        stop("Input must have all elements listed in `idecon_members`")
    }
}

#' @noRd
#' @author 2024-2025 Tobias Schmidt: initial version.
as_rdecon <- function(x) {
    assert( # (1)
        is_spectrum(x$spectrum),
        is.list(x$args),
        typeof(x$mdrb_spectrum) == "externalptr",
        typeof(x$mdrb_deconvr) == "externalptr",
        typeof(x$mdrb_decon) == "externalptr",
        class(x$mdrb_spectrum) == "Spectrum",
        class(x$mdrb_deconvr) == "Deconvoluter",
        class(x$mdrb_decon) == "Deconvolution"
    )
    stopifnot(length(x) == 5) # (1)
    structure(x, class = "rdecon")
    # (1) This function is private, so in theory it can never be called with
    # invalid arguments, as all public functions validate their inputs first.
    # Therefore, using assert for type checking is correct, as assert-checks are
    # deactivated when the package is loaded via library(), i.e. the
    # "production" code will run faster.
    #
    # However, in practice, it's very easy to run into nasty problems as soon as
    # assertions are disabled (e.g. when calling this function with invalid
    # arguments during unit testing). To prevent such scenarios, we include a
    # very tiny, super-fast sanity check here, using stopifnot. This check
    # will always run, even in production code, and might us save a lot of
    # headaches in the future.
}

#' @noRd
#' @author 2024-2025 Tobias Schmidt: initial version.
as_ispecs <- function(x, sf = c(1e3, 1e6)) {
    if (is_ispecs(x)) {
        return(x)
    }
    gg <- if (is_ispec(x)) {
        list(x)
    } else if (is_spectrum(x)) {
        list(as_ispec(x))
    } else if (is_spectra(x)) {
        lapply(x, as_ispec, sf = sf)
    } else {
        stop("Input must be ispec, ispecs or spectra, not ", class(x))
    }
    gg <- structure(gg, class = "ispecs")
    gg <- set_names(gg, get_names(gg))
    gg
}

#' @noRd
#' @author 2024-2025 Tobias Schmidt: initial version.
as_idecons <- function(x) {
    if (is_idecons(x)) return(x)
    stopifnot(is.list(x), all(sapply(x, is_idecon)))
    structure(x, class = "idecons")
}

#' @noRd
#' @author 2024-2025 Tobias Schmidt: initial version.
as_rdecons <- function(x) {
    if (is_rdecons(x)) return(x)
    assert(is.list(x), all(sapply(x, is_rdecon)))
    structure(x, class = "rdecons")
}

#' @noRd
#' @title Convert a List of Singlets to a Collection
#' @author 2024-2025 Tobias Schmidt: initial version.
as_collection <- function(x, cls) {
    assert(
        is.list(x),
        is_char(cls, 1, "(decon[0-2]|idecon|rdecon)"),
        cls == "decon0" || all(sapply(x, class) == cls)
    )
    switch(cls,
        "decon0" = as_decons0(x),
        "decon1" = as_decons1(x),
        "decon2" = as_decons2(x),
        "idecon" = as_idecons(x),
        "rdecon" = as_rdecons(x),
        stop("Unsupported class: ", cls)
    )
}

#' @noRd
#' @title Convert any Singlet to a "v1.2+" Singlet
#' @author 2024-2025 Tobias Schmidt: initial version.
as_v12_singlet <- function(obj) {
    if (is_spectrum(obj)) obj
    else if (is_ispec(obj)) as_spectrum(obj)
    else if (is_idecon(obj)) as_decon2(obj)
    else if (is_rdecon(obj)) as_decon2(obj)
    else if (is_decon0(obj)) stop("decon0 objects are not supported. Convert with as_decon2.")
    else if (is_decon1(obj)) as_decon2(obj)
    else if (is_decon2(obj)) obj
    else if (is_align(obj)) obj
    else stop(sprintf("Objects of class %s are not supported.", class(obj)))
}

#' @noRd
#' @title Convert a Collection to a "v1.2+" Collection
#' @author 2024-2025 Tobias Schmidt: initial version.
as_v12_collection <- function(obj) {
    if (is_spectra(obj)) obj
    else if (is_ispecs(obj)) as_spectra(obj)
    else if (is_idecons(obj)) as_decons2(obj)
    else if (is_decons0(obj)) stop("decons0 objects are not supported. Convert with as_decons2.")
    else if (is_decons1(obj)) as_decons2(obj)
    else if (is_decons2(obj)) obj
    else if (is_aligns(obj)) obj
    else stop(sprintf("Objects of class %s are not supported.", class(obj)))
}

# Constructors (Private) #####

#' @noRd
#' @author 2024-2025 Tobias Schmidt: initial version.
new_rdecon <- function(spectrum, args, mdrb_spectrum, mdrb_deconvr, mdrb_decon) {
    x <- named(spectrum, args, mdrb_spectrum, mdrb_deconvr, mdrb_decon)
    as_rdecon(x)
}

# Getters (Private) #####

#' @noRd
#' @title Returns the name of an iterable.
#' @param x An iterable object, e.g. a single metabodecon object.
#' @param default Default name if no name is found.
#' @return The name of the object as string or whatever is given as `default`.
#' @author 2024-2025 Tobias Schmidt: initial version.
#' @examples
#' s1 <- list()
#' s2 <- list(name = "foo")
#' s3 <- list(name = "foo", meta = list(name = "bar"))
#' get_name(s1) # ""
#' get_name(s2) # "foo"
#' get_name(s3) # "bar"
get_name <- function(x, default = "") {
    (if (is.list(x)) x$meta$name %||% x$name) %||% default
}

#' @noRd
#' @title Returns the names of a metabodecon collection object.
#' @param x A metabodecon collection object.
#' @param default Default names if no names are found. Passed on to `get_default_names`.
#' @return A character vector of names.
#' @author 2024-2025 Tobias Schmidt: initial version.
#' @examples
#' s1 <- list()
#' s2 <- list(name = "foo")
#' s3 <- list(name = "foo", meta = list(name = "bar"))
#'
#' get_names(list(s1, s1)) # c("spectrum_1", "spectrum_2")
#' get_names(list(s1, myspec = s1)) # c("spectrum_1", "myspec")
#' get_names(list(s1, myspec = s2)) # c("spectrum_1", "foo")
#' get_names(list(s1, myspec = s3)) # c("spectrum_1", "bar")
get_names <- function(x, default = "spectrum_%d") {
    obj_names <- sapply(x, get_name, "")
    obj_names_empty <- obj_names == ""
    if (any(obj_names_empty)) {
        list_names <- names(x) %||% rep("", length(x))
        list_names_empty <- list_names == ""
        if (any(list_names_empty)) {
            default_names <- get_default_names(x, default)
            list_names[list_names_empty] <- default_names[list_names_empty]
        }
        obj_names[obj_names_empty] <- list_names[obj_names_empty]
    }
    names(obj_names) <- NULL
    obj_names
}

#' @noRd
#' @author 2024-2025 Tobias Schmidt: initial version.
get_default_names <- function(x, default) {
    if (length(default) == 1 && grepl("%d", default)) {
        return(sprintf(default, seq_along(x)))
    }
    if (length(unique(default)) == length(x)) {
        return(default)
    }
    stop(paste(
        "Default names must be a single string with a `%d` placeholder",
        "or a character vector of unique spectrum names."
    ))
}

#' @noRd
#' @description
#' Generates dummy peak selection results based on  peak  centers  and  chemical
#' shifts. Returns the results in a format suitable  for  the  specified  target
#' type (`decon2` or `idecon`).
#'
#' @details
#' This helper function creates plausible peak selection results from the  final
#' peak centers after peak parameter approximation. It is necessary because  the
#' metabodecon-rust  backend  currently  does  not  provide  an  interface   for
#' accessing the true peak selection results.  These  results  are  required  by
#' `decon2` and `idecon` objects (e.g., for plotting). If  the  metabodecon-rust
#' backend introduces a way to access the peak selection results in the  future,
#' this function can be removed.
#'
#' @author 2024-2025 Tobias Schmidt: initial version.
get_peak <- function(x0, cs, target = "decon2") {
    assert(
        is_num(x0),
        is_num(cs),
        is_char(target, 1, "(idecon|decon2)")
    )
    center <- round(convert_pos(x0, cs, seq_along(cs)))
    npeaks <- length(center)
    left <- center - 1
    center <- center
    right <- center + 1
    if (target == "idecon") {
        score <- rep(999, npeaks)
        high <- rep(TRUE, npeaks)
        region <- rep("norm", npeaks)
        peak <- data.frame(left, center, right, score, high, region)
    } else {
        peak <- data.frame(left, center, right)
    }
    peak
}

# Setters #####

#' @noRd
#' @author 2024-2025 Tobias Schmidt: initial version.
set_names <- function(x, nams) {
    assert(is.list(x))
    has_names <- all(sapply(x, function(e) "name" %in% names(e)))
    has_meta_names <- all(sapply(x, function(e) "name" %in% names(e$meta)))
    names(x) <- nams
    if (has_names) for (i in seq_along(x)) x[[i]]$name <- nams[[i]]
    if (has_meta_names) for (i in seq_along(x)) x[[i]]$meta$name <- nams[[i]]
    x
}

# Members (Private) #####

spectrum_members <- c(
    "cs",
    "si",
    "meta"
)

ispec_members <- c(
    "y_raw",
    "y_scaled",
    "n",
    "dp",
    "sdp",
    "sf",
    "ppm",
    "hz",
    "ppm_range",
    "ppm_max",
    "ppm_min",
    "ppm_step",
    "ppm_nstep",
    "name",
    "meta"
)

idecon_members <- c(
    ispec_members,
    "args",
    "y_nows",
    "y_pos",
    "Z",
    "y_smooth",
    "d",
    "peak",
    "lci",
    "lca",
    "lcr"
)

rdecon_members <- c(
    "spectrum",
    "args",
    "mdrb_spectrum",
    "mdrb_deconvr",
    "mdrb_decon"
)

decon0_members <- c(
    "number_of_files",
    "filename",
    "x_values",
    "x_values_ppm",
    "y_values",
    "spectrum_superposition",
    "mse_normed",
    "index_peak_triplets_middle",
    "index_peak_triplets_left",
    "index_peak_triplets_right",
    "peak_triplets_middle",
    "peak_triplets_left",
    "peak_triplets_right",
    "integrals",
    "signal_free_region",
    "range_water_signal_ppm",
    "A",
    "lambda",
    "x_0"
)

decon0_members_optional <- c(
    "signal_free_region",
    "range_water_signal_ppm"
)

decon0_members_mandatory <- setdiff(
    decon0_members,
    decon0_members_optional
)

decon1_members <- c(
    decon0_members,
    "y_values_raw",
    "x_values_hz",
    "mse_normed_raw",
    "signal_free_region_ppm",
    "x_0_hz",
    "x_0_dp",
    "x_0_ppm",
    "A_hz",
    "A_dp",
    "A_ppm",
    "lambda_hz",
    "lambda_dp",
    "lambda_ppm"
)

decon2_members <- c(
    "cs",
    "si",
    "meta",
    "args",
    "sit",
    "peak",
    "lcpar",
    "mse"
)

align_members <- decon2_members

Try the metabodecon package in your browser

Any scripts or data that you put into this service are public.

metabodecon documentation built on Nov. 5, 2025, 7:12 p.m.