From 537998bf3b1eb4e7f2771d5c53f5f2fbc472a25a Mon Sep 17 00:00:00 2001 From: Nicolau Manubens Date: Mon, 22 Oct 2018 20:03:09 +0200 Subject: [PATCH 01/19] Updated Readme. --- README.md | 53 ++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 46 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index a6cfa35..ac934e1 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,51 @@ ---- -title: "Apply a Function Taking Multiple Numeric Objects as Input Across Multiple Arrays" -author: "Alasdair" -date: "14 July 2017" -output: html_document ---- +## Apply a Function Taking Multiple Numeric Objects as Input Across Multiple Arrays This package extends the apply and plyr families of functions to applications which involve the use of multiple arrays as input. -This is especially useful for climate data related applications, where data is often distributed across multiple arrays with different dimensions (e.g experimental array 1, experimental array 2 and the observations). The multiApply:Apply function reduces the need write loops for every application. +This is especially useful for climate data related applications, where data is often distributed across multiple arrays with different dimensions (e.g experimental array 1, experimental array 2 and the observations). The multiApply::Apply function reduces the need to write loops for every application. +### Installation +In order to install and load the latest published version of the package on CRAN, you can run the following lines in your R session: + +```r +install.packages('multiApply') +library(multiApply) +``` + +Also, you can install the latest stable version from this GitHub repository as follows: + +```r +devtools::install_git('https://earth.bsc.es/gitlab/ces/multiApply') +``` + +### How to use + +This package consistis in a single function, `Apply`, which is used in a similar fashion as the base `apply`. Full documentation can be found in `?Apply`. + +A simple example is provided next. In this example, we have two data arrays. The first, with information on the number of items sold in 5 different stores (located in different countries) during the past 1000 days, for 200 different items. The second, with information on the price point for each item in each store. + +The example shows how to compute the total income for each of the stores, straightforwardly combining the input data arrays, by automatically applying repeatedly the 'atomic' function that performs only the essential calculations for a single case. + +```r +dims <- c(store = 5, item = 200, day = 1000) +sales_amount <- array(rnorm(prod(dims)), dims) + +dims <- c(store = 5, item = 200) +sales_price <- array(rnorm(prod(dims)), dims) + +income_function <- function(x, y) { + # Expected inputs: + # x: array with dimensions (item, day) + # y: price point vector with dimension (item) + sum(rowSums(x) * y) +} + +income <- Apply(data = list(sales_amount, sales_price), + target_dims = list(c('item', 'day'), 'item'), + income_function) + +dim(income[[1]]) +# store +# 5 +``` -- GitLab From 82ce62c4f26df8e50a73a7590f431ff9db657d66 Mon Sep 17 00:00:00 2001 From: Nicolau Manubens Date: Mon, 22 Oct 2018 20:07:56 +0200 Subject: [PATCH 02/19] Small fixes in readme. --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ac934e1..5450824 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -## Apply a Function Taking Multiple Numeric Objects as Input Across Multiple Arrays +## multiApply -This package extends the apply and plyr families of functions to applications which involve the use of multiple arrays as input. +This package extends the apply and plyr families of functions to applications which involve the use of multiple arrays as input, and is useful to apply a function taking multiple numeric objects as input across multiple multi-dimensional arrays. -This is especially useful for climate data related applications, where data is often distributed across multiple arrays with different dimensions (e.g experimental array 1, experimental array 2 and the observations). The multiApply::Apply function reduces the need to write loops for every application. +This is especially useful for climate data related applications, where data is often distributed across multiple arrays with different dimensions (e.g experimental array 1, experimental array 2 and the observations). The `multiApply::Apply` function reduces the need to write loops for every application. ### Installation -- GitLab From bf84d3cd976cfdab5e0f8467e3384205f1448c77 Mon Sep 17 00:00:00 2001 From: Nicolau Manubens Date: Tue, 23 Oct 2018 20:07:46 +0200 Subject: [PATCH 03/19] First small improvements. --- R/Apply.R | 38 ++++++++++++++++++++++++++++++++------ 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/R/Apply.R b/R/Apply.R index b49b11d..51b4ce8 100644 --- a/R/Apply.R +++ b/R/Apply.R @@ -43,6 +43,9 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., output_dims = NULL, stop("Dimension names of arrays in 'data' must be at least ", "one character long.") } + if (length(unique(names(dim(data[[i]])))) != length(names(dim(data[[i]])))) { + stop("Arrays in 'data' must not have repeated dimension names.") + } } else { is_unnamed[i] <- TRUE } @@ -414,12 +417,23 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., output_dims = NULL, arrays_of_results <- NULL found_first_result <- FALSE + # Sharing workload across cores. Each core will run 4 chunks if possible. + # the larger the splitting factor, the smaller the amount of data that + # will be processed at once and the finer the granules to be distributed + # across cores, but the larger the overhead for granule startup, etc. total_size <- prod(dim(ma)) - if (!is.null(ncores)) { - chunk_size <- round(total_size / (ncores * 4)) + splitting_factor <- 1 + if (splitting_factor == 'greatest') { + chunks_per_core <- ceiling(total_size / ncores) } else { - chunk_size <- 4 + chunks_per_core <- 4 * splitting_factor + } + if (!is.null(ncores)) { + chunk_size <- round(total_size / (ncores * chunks_per_core)) } + #} else { + # chunk_size <- 4 + #} if (chunk_size < 1) { chunk_size <- 1 } @@ -430,8 +444,16 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., output_dims = NULL, } # need to add progress bar + # ADAPT THIS FUNCTION TO TAKE ALL n INDICES IN CHUNK m AT ONCE. + # IF ONLY ONE INPUT ARRAY, MAKE USE OF apply. iteration <- function(m) { sub_arrays_of_results <- list() + weights <- lapply(1:length(data_indexed), + function(i) { + ind_dims <- dim(data_indexed_indices[[i]]) + sapply(1:length(ind_dims), + function(k) prod(c(1, ind_dims)[1:k])) + }) found_first_sub_result <- FALSE for (n in 1:chunk_sizes[m]) { # j is the index of the data piece to load in data_indexed @@ -446,9 +468,13 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., output_dims = NULL, inds_to_take <- which(names(marg_indices) %in% names(dim(data_indexed_indices[[i]]))) if (length(inds_to_take) > 0) { marg_inds_to_take <- marg_indices[inds_to_take][names(dim(data_indexed_indices[[i]]))] - input[[i]] <- data_indexed[[i]][[do.call("[", - c(list(x = data_indexed_indices[[i]]), marg_inds_to_take, - list(drop = TRUE)))]] + marg_inds_to_take <- marg_indices[inds_to_take][names(dim(data_indexed_indices[[i]]))] + inds_from_dii <- 1 + sum((marg_inds_to_take - 1) * weights[[i]]) + inds <- data_indexed_indices[[i]][inds_from_dii] + input[[i]] <- data_indexed[[i]][[inds]] + #input[[i]] <- data_indexed[[i]][[do.call("[", + # c(list(x = data_indexed_indices[[i]]), marg_inds_to_take, + # list(drop = TRUE)))]] } else { input[[i]] <- data_indexed[[i]][[1]] } -- GitLab From 5404e84cdb2a5fb859dc1967d74ce90f25706035 Mon Sep 17 00:00:00 2001 From: Nicolau Manubens Date: Tue, 6 Nov 2018 21:42:35 +0100 Subject: [PATCH 04/19] Fixed performance issue. Major changes. API changes. --- R/Apply.R | 424 +++++++++++++++++++++++++++--------------------------- 1 file changed, 209 insertions(+), 215 deletions(-) diff --git a/R/Apply.R b/R/Apply.R index 51b4ce8..081be93 100644 --- a/R/Apply.R +++ b/R/Apply.R @@ -1,15 +1,16 @@ #' Wrapper for Applying Atomic Functions to Arrays. #' #' This wrapper applies a given function, which takes N [multi-dimensional] arrays as inputs (which may have different numbers of dimensions and dimension lengths), and applies it to a list of N [multi-dimensional] arrays with at least as many dimensions as expected by the given function. The user can specify which dimensions of each array (or matrix) the function is to be applied over with the \code{margins} or \code{target_dims} option. A user can apply a function that receives (in addition to other helper parameters) 1 or more arrays as input, each with a different number of dimensions, and returns any number of multidimensional arrays. The target dimensions can be specified by their names. It is recommended to use this wrapper with multidimensional arrays with named dimensions. -#' @param data A single object (vector, matrix or array) or a list of objects. They must be in the same order as expected by AtomicFun. -#' @param target_dims List of vectors containing the dimensions to be input into AtomicFun for each of the objects in the data. These vectors can contain either integers specifying the dimension position, or characters corresponding to the dimension names. This parameter is mandatory if margins is not specified. If both margins and target_dims are specified, margins takes priority over target_dims. -#' @param AtomicFun Function to be applied to the arrays. -#' @param ... Additional arguments to be used in the AtomicFun. -#' @param output_dims Optional list of vectors containing the names of the dimensions to be output from the AtomicFun for each of the objects it returns (or a single vector if the function has only one output). +#' @param data A single object (vector, matrix or array) or a list of objects. They must be in the same order as expected by fun. +#' @param target_dims List of vectors containing the dimensions to be input into fun for each of the objects in the data. These vectors can contain either integers specifying the dimension position, or characters corresponding to the dimension names. This parameter is mandatory if margins is not specified. If both margins and target_dims are specified, margins takes priority over target_dims. +#' @param fun Function to be applied to the arrays. +#' @param ... Additional arguments to be used in the fun. +#' @param output_dims Optional list of vectors containing the names of the dimensions to be output from the fun for each of the objects it returns (or a single vector if the function has only one output). #' @param margins List of vectors containing the margins for the input objects to be split by. Or, if there is a single vector of margins specified and a list of objects in data, then the single set of margins is applied over all objects. These vectors can contain either integers specifying the dimension position, or characters corresponding to the dimension names. If both margins and target_dims are specified, margins takes priority over target_dims. #' @param ncores The number of multicore threads to use for parallel computation. +#' @param split_factor Factor telling to which degree the input data should be split into smaller pieces to be processed by the available cores. By default (split_factor = 1) the data is split into 4 pieces for each of the cores (as specified in ncores). A split_factor of 2 will result in 8 pieces for each of the cores, and so on. The special value 'greatest' will split the input data into as many pieces as possible. #' @details When using a single object as input, Apply is almost identical to the apply function. For multiple input objects, the output array will have dimensions equal to the dimensions specified in 'margins'. -#' @return List of arrays or matrices or vectors resulting from applying AtomicFun to data. +#' @return List of arrays or matrices or vectors resulting from applying fun to data. #' @references Wickham, H (2011), The Split-Apply-Combine Strategy for Data Analysis, Journal of Statistical Software. #' @export #' @examples @@ -20,9 +21,9 @@ #' test_fun <- function(x, y, z) {((sum(x > z) / (length(x))) / #' (sum(y > z) / (length(y)))) * 100} #' margins = list(c(1, 2), c(1, 2), c(1,2)) -#' test <- Apply(data, margins = margins, AtomicFun = "test_fun") -Apply <- function(data, target_dims = NULL, AtomicFun, ..., output_dims = NULL, - margins = NULL, ncores = NULL) { +#' test <- Apply(data, margins = margins, fun = "test_fun") +Apply <- function(data, target_dims = NULL, fun, ..., output_dims = NULL, + margins = NULL, ncores = NULL, split_factor = 1) { # Check data if (!is.list(data)) { data <- list(data) @@ -32,6 +33,7 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., output_dims = NULL, } is_vector <- rep(FALSE, length(data)) is_unnamed <- rep(FALSE, length(data)) + unnamed_dims <- c() for (i in 1 : length(data)) { if (is.null(dim(data[[i]]))) { is_vector[i] <- TRUE @@ -45,29 +47,50 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., output_dims = NULL, } if (length(unique(names(dim(data[[i]])))) != length(names(dim(data[[i]])))) { stop("Arrays in 'data' must not have repeated dimension names.") + } + if (any(is.na(names(dim(data[[i]]))))) { + stop("Arrays in 'data' must not have NA as dimension names.") } } else { is_unnamed[i] <- TRUE + new_unnamed_dims <- c() + for (j in 1 : length(dim(data[[i]]))) { + len_of_dim_j <- dim(data[[i]])[j] + found_match <- which(unnamed_dims == len_of_dim_j) + if (length(found_match) > 1) { + stop("Arrays in 'data' have multiple unnamed dimensions of the ", + "same length. Please provide dimension names.") + } else if (length(found_match) == 1) { + names(dim(data[[i]]))[j] <- names(unnamed_dims)[found_match] + } else { + new_dim <- len_of_dim_j + names(new_dim) <- paste0('unnamed_dim_', length(unnamed_dims) + + length(new_unnamed_dims) + 1) + new_unnamed_dims <- c(new_unnamed_dims, new_dim) + names(dim(data[[i]]))[j] <- names(new_dim) + } + } + unnamed_dims <- c(unnamed_dims, new_unnamed_dims) } } - # Check AtomicFun - if (is.character(AtomicFun)) { - try({AtomicFun <- get(AtomicFun)}, silent = TRUE) - if (!is.function(AtomicFun)) { - stop("Could not find the function '", AtomicFun, "'.") + # Check fun + if (is.character(fun)) { + try({fun <- get(fun)}, silent = TRUE) + if (!is.function(fun)) { + stop("Could not find the function '", fun, "'.") } } - if (!is.function(AtomicFun)) { - stop("Parameter 'AtomicFun' must be a function or a character string ", + if (!is.function(fun)) { + stop("Parameter 'fun' must be a function or a character string ", "with the name of a function.") } - if ('startR_step' %in% class(AtomicFun)) { + if ('startR_step' %in% class(fun)) { if (is.null(target_dims)) { - target_dims <- attr(AtomicFun, 'target_dims') + target_dims <- attr(fun, 'target_dims') } if (is.null(output_dims)) { - output_dims <- attr(AtomicFun, 'output_dims') + output_dims <- attr(fun, 'output_dims') } } @@ -90,6 +113,9 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., output_dims = NULL, stop("Parameter 'margins' must be one or a list of numeric or ", "character vectors.") } + if (any(sapply(margins, length) == 0)) { + stop("Parameter 'margins' must not contain length-0 vectors.") + } duplicate_dim_specs <- sapply(margins, function(x) { length(unique(x)) != length(x) @@ -120,10 +146,16 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., output_dims = NULL, margins_names[[i]] <- margins[[i]] margins[[i]] <- margins2_new_num } - if (!is.null(names(dim(data[[i]])))) { - target_dims_names[[i]] <- names(dim(data[[i]]))[- margins[[i]]] + if (length(margins[[i]]) == length(dim(data[[i]]))) { + target_dims_names[i] <- list(NULL) + target_dims[i] <- list(NULL) + } else { + if (!is.null(names(dim(data[[i]])))) { + margins_names[[i]] <- names(dim(data[[i]]))[margins[[i]]] + target_dims_names[[i]] <- names(dim(data[[i]]))[- margins[[i]]] + } + target_dims[[i]] <- (1 : length(dim(data[[i]])))[- margins[[i]]] } - target_dims[[i]] <- (1 : length(dim(data[[i]])))[- margins[[i]]] } else { target_dims[[i]] <- 1 : length(dim(data[[i]])) if (!is.null(names(dim(data[[i]])))) { @@ -154,29 +186,42 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., output_dims = NULL, } margins <- vector('list', length(data)) for (i in 1 : length(data)) { - if (is.character(unlist(target_dims[i]))) { - if (is.null(names(dim(data[[i]])))) { - stop("Parameter 'target_dims' contains dimension names, but ", - "some of the corresponding objects in 'data' do not have ", - "dimension names.") + if (length(target_dims[[i]]) > 0) { + if (is.character(unlist(target_dims[i]))) { + if (is.null(names(dim(data[[i]])))) { + stop("Parameter 'target_dims' contains dimension names, but ", + "some of the corresponding objects in 'data' do not have ", + "dimension names.") + } + targs2 <- target_dims[[i]] + targs2_new_num <- c() + for (j in 1 : length(targs2)) { + matches <- which(names(dim(data[[i]])) == targs2[j]) + if (length(matches) < 1) { + stop("Could not find dimension '", targs2[j], "' in ", i, + "th object provided in 'data'.") + } + targs2_new_num[j] <- matches[1] + } + target_dims_names[[i]] <- target_dims[[i]] + target_dims[[i]] <- targs2_new_num } - targs2 <- target_dims[[i]] - targs2_new_num <- c() - for (j in 1 : length(targs2)) { - matches <- which(names(dim(data[[i]])) == targs2[j]) - if (length(matches) < 1) { - stop("Could not find dimension '", targs2[j], "' in ", i, - "th object provided in 'data'.") + if (length(target_dims) == length(dim(data[[i]]))) { + margins_names[i] <- list(NULL) + margins[i] <- list(NULL) + } else { + if (!is.null(names(dim(data[[i]])))) { + target_dims_names[[i]] <- names(dim(data[[i]]))[target_dims[[i]]] + margins_names[[i]] <- names(dim(data[[i]]))[- target_dims[[i]]] } - targs2_new_num[j] <- matches[1] + margins[[i]] <- (1 : length(dim(data[[i]])))[- target_dims[[i]]] + } + } else { + margins[[i]] <- 1 : length(dim(data[[i]])) + if (!is.null(names(dim(data[[i]])))) { + margins_names[[i]] <- names(dim(data[[i]])) } - target_dims_names[[i]] <- target_dims[[i]] - target_dims[[i]] <- targs2_new_num - } - if (!is.null(names(dim(data[[i]])))) { - margins_names[[i]] <- names(dim(data[[i]]))[- target_dims[[i]]] } - margins[[i]] <- (1 : length(dim(data[[i]])))[- target_dims[[i]]] } } # Reorder dimensions of input data for target dims to be left-most @@ -188,7 +233,11 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., output_dims = NULL, marg_dims <- (1 : length(dim(data[[i]])))[- target_dims[[i]]] data[[i]] <- .aperm2(data[[i]], c(target_dims[[i]], marg_dims)) target_dims[[i]] <- 1 : length(target_dims[[i]]) - margins[[i]] <- (length(target_dims[[i]]) + 1) : length(dim(data[[i]])) + target_dims_names[[i]] <- names(dim(data[[i]]))[target_dims[[i]]] + if (length(target_dims[[i]]) < length(dim(data[[i]]))) { + margins[[i]] <- (length(target_dims[[i]]) + 1) : length(dim(data[[i]])) + margins_names[[i]] <- names(dim(data[[i]]))[margins[[i]]] + } } } } @@ -221,16 +270,11 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., output_dims = NULL, # Consistency checks of margins of all input objects # for each data array, add its margins to the list if not present. - # if there are unnamed margins in the list, check their size matches the margins being added - # and simply assing them a name # those margins present, check that they match - # if unnamed margins, check consistency with found margins - # if more mrgins than found, add numbers to the list, without names # with this we end up with a named list of margin sizes - # for data arrays with unnamed margins, we can assume their margins names are those of the first entries in the resulting list all_found_margins_lengths <- afml <- list() for (i in 1:length(data)) { - if (!is.null(margins_names[[i]])) { + #if (!is.null(margins_names[[i]])) { if (length(afml) > 0) { matches <- which(margins_names[[i]] %in% names(afml)) if (length(matches) > 0) { @@ -242,72 +286,11 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., output_dims = NULL, } else { margs_to_add <- as.list(dim(data[[i]])[margins[[i]]]) } - unnamed_margins <- which(sapply(names(afml), nchar) == 0) - if (length(unnamed_margins) > 0) { - stop_with_error <- FALSE - if (length(unnamed_margins) <= length(margs_to_add)) { - if (any(unlist(afml[unnamed_margins]) != unlist(margs_to_add[1:length(unnamed_margins)]))) { - stop_with_error <- TRUE - } - names(afml)[unnamed_margins] <- names(margs_to_add)[1:length(unnamed_margins)] - margs_to_add <- margs_to_add[- (1:length(margs_to_add))] - } else { - if (any(unlist(afml[unnamed_margins[1:length(margs_to_add)]]) != unlist(margs_to_add))) { - stop_with_error <- TRUE - } - names(afml)[unnamed_margins[1:length(margs_to_add)]] <- names(margs_to_add) - margs_to_add <- list() - } - if (stop_with_error) { - stop("Found unnamed margins (for some objects in parameter ", - "'data') that have been associated by their position to ", - "named margins in other objects in 'data' and do not have ", - "matching length. It could also be that the unnamed ", - "margins don not follow the same order as the named ", - "margins. In that case, either put the corresponding names ", - "to the dimensions of the objects in 'data', or put them ", - "in a consistent order.") - } - } afml <- c(afml, margs_to_add) } else { afml <- as.list(dim(data[[i]])[margins[[i]]]) } - } else { - margs_to_add <- as.list(dim(data[[i]])[margins[[i]]]) - names(margs_to_add) <- rep('', length(margs_to_add)) - if (length(afml) > 0) { - stop_with_error <- FALSE - if (length(afml) >= length(margs_to_add)) { - if (any(unlist(margs_to_add) != unlist(afml[1:length(margs_to_add)]))) { - stop_with_error <- TRUE - } - } else { - if (any(unlist(margs_to_add)[1:length(afml)] != unlist(afml))) { - stop_with_error <- TRUE - } - margs_to_add <- margs_to_add[- (1:length(afml))] - afml <- c(afml, margs_to_add) - } - if (stop_with_error) { - stop("Found unnamed margins (for some objects in parameter ", - "'data') that have been associated by their position to ", - "named margins in other objects in 'data' and do not have ", - "matching length. It could also be that the unnamed ", - "margins don not follow the same order as in other ", - "objects. In that case, either put the corresponding names ", - "to the dimensions of the objects in 'data', or put them ", - "in a consistent order.") - } - } else { - afml <- margs_to_add - } - } - } - missing_margin_names <- which(names(afml) == '') - if (length(missing_margin_names) > 0) { - names(afml)[missing_margin_names] <- paste0('_unnamed_margin_', - 1:length(missing_margin_names), '_') + #} } # afml is now a named list with the lenghts of all margins. Each margin # appears once only. If some names are not provided, they are set automatically @@ -319,8 +302,7 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., output_dims = NULL, # across them, and use data arrays repeatedly as needed. margins_afml <- margins for (i in 1:length(data)) { - if (!is.null(margins_names[[i]])) { - + #if (!is.null(margins_names[[i]])) { margins_afml[[i]] <- sapply(margins_names[[i]], function(x) { sapply(x, @@ -330,12 +312,7 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., output_dims = NULL, ) } ) - } else if (length(margins_afml[[i]]) > 0) { - margins_afml[[i]] <- margins_afml[[i]] - min(margins_afml[[i]]) + 1 - # The missing margin and dim names are filled in. - margins_names[[i]] <- names(afml)[margins_afml[[i]]] - names(dim(data[[i]]))[margins[[i]]] <- margins_names[[i]] - } + #} } common_margs <- margins_afml[[1]] if (length(margins_afml) > 1) { @@ -359,74 +336,22 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., output_dims = NULL, # non_common_margs is now a numeric vector with the indices of the # non-common margins (i.e. their position in afml) - .isolate <- function(data, margin_length, drop = FALSE) { - eval(dim(environment()$data)) - structure(list(env = environment(), index = margin_length, - drop = drop, subs = as.name("[")), - class = c("indexed_array")) - } - .consolidate <- function(subsets, dimnames, out_dims) { - lapply(setNames(1:length(subsets), names(subsets)), - function(x) { - if (length(out_dims[[x]]) > 0) { - dims <- dim(subsets[[x]]) - if (!is_unnamed[x]) { - names(dims) <- dimnames[[x]] - } - dims <- dims[out_dims[[x]]] - array(subsets[[x]], dim = dims) - } else { - as.vector(subsets[[x]]) - } - }) - } - - data_indexed <- vector('list', length(data)) - data_indexed_indices <- vector('list', length(data)) - for (i in 1 : length(data)) { - margs_i <- which(names(dim(data[[i]])) %in% names(afml[c(non_common_margs, common_margs)])) - false_margs_i <- which(margs_i %in% target_dims[[i]]) - margs_i <- setdiff(margs_i, false_margs_i) - if (length(margs_i) > 0) { - margin_length <- lapply(dim(data[[i]]), function(x) 1 : x) - margin_length[- margs_i] <- "" - } else { - margin_length <- as.list(rep("", length(dim(data[[i]])))) - } - margin_length <- expand.grid(margin_length, KEEP.OUT.ATTRS = FALSE, - stringsAsFactors = FALSE) - data_indexed[[i]] <- .isolate(data[[i]], margin_length) - if (length(margs_i) > 0) { - data_indexed_indices[[i]] <- array(1:prod(dim(data[[i]])[margs_i]), - dim = dim(data[[i]])[margs_i]) - } else { - data_indexed_indices[[i]] <- array(1, dim = 1) - } - } - - splatted_f <- splat(AtomicFun) - - # Iterate along all non-common margins if (length(c(non_common_margs, common_margs)) > 0) { marg_inds_ordered <- sort(c(non_common_margs, common_margs)) - margins_array <- ma <- array(1:prod(unlist(afml[marg_inds_ordered])), - dim = unlist(afml[marg_inds_ordered])) + margins_array_dims <- mad <- unlist(afml[marg_inds_ordered]) } else { - ma <- array(1) + margins_array_dims <- mad <- NULL } - arrays_of_results <- NULL - found_first_result <- FALSE # Sharing workload across cores. Each core will run 4 chunks if possible. - # the larger the splitting factor, the smaller the amount of data that + # the larger the split factor, the smaller the amount of data that # will be processed at once and the finer the granules to be distributed # across cores, but the larger the overhead for granule startup, etc. - total_size <- prod(dim(ma)) - splitting_factor <- 1 - if (splitting_factor == 'greatest') { + total_size <- prod(mad) + if (split_factor == 'greatest') { chunks_per_core <- ceiling(total_size / ncores) } else { - chunks_per_core <- 4 * splitting_factor + chunks_per_core <- 4 * split_factor } if (!is.null(ncores)) { chunk_size <- round(total_size / (ncores * chunks_per_core)) @@ -443,44 +368,112 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., output_dims = NULL, chunk_sizes <- c(chunk_sizes, total_size %% chunk_size) } -# need to add progress bar - # ADAPT THIS FUNCTION TO TAKE ALL n INDICES IN CHUNK m AT ONCE. - # IF ONLY ONE INPUT ARRAY, MAKE USE OF apply. + input_margin_weights <- lapply(1:length(data), + function(i) { + marg_sizes <- dim(data[[i]])[margins[[i]]] + sapply(1:length(marg_sizes), + function(k) prod(c(1, marg_sizes)[1:k])) + }) + + # Flattening margin dimensions so that the iteration function can access + # them easily. + for (i in 1:length(data)) { + if (length(margins[[i]]) > 0) { + dims <- dim(data[[i]]) + margins_inds <- 1:length(margins[[i]]) + length(target_dims[[i]]) + dim(data[[i]]) <- c(dims[-margins_inds], + '_margins_dim_' = prod(dims[margins_inds])) + } else { + dim(data[[i]]) <- c(dim(data[[i]]), '_margins_dim_' = 1) + } + } + + # TODO: need to add progress bar + # TODO: IF ONLY ONE INPUT ARRAY, MAKE USE OF apply. + splatted_f <- splat(fun) + + # For a selected use case, these are the timings: + # - total: 17 s + # - preparation + post: 1 s + # - llply (40 iterations): 16 s + # - one iteration: 1.5s with profiling of 50 sub-iterations (0.4 without) + # - intro: 0 s + # - for loop with profiling of 50 sub-iterations (5000 sub-iterations): 1.5 s + # - one sub-iteration: 0.0003 s + # - intro: 0.000125 s + # - first half: 0.00005 s (computation of index) + # - second half: 0.00007 s (resolution of index and data access) + # - splatted_f: 0.000125 s + # - outro: 0.00005 iteration <- function(m) { + # INTRO + n <- 1 + first_index <- n + (m - 1) * chunk_size + first_marg_indices <- arrayInd(first_index, mad) + names(first_marg_indices) <- names(mad) + n <- chunk_sizes[m] + last_index <- n + (m - 1) * chunk_size + last_marg_indices <- arrayInd(last_index, mad) + names(last_marg_indices) <- names(mad) + + first_data_margin_dim_index_to_take <- list() + input <- list() + for (i in 1:length(data)) { + if (length(margins[[i]]) > 1) { + input_first_margin_dim_index <- first_marg_indices[margins_names[[i]]] + input_first_margin_dim_index <- 1 + sum((input_first_margin_dim_index - 1) * + input_margin_weights[[i]]) + input_last_margin_dim_index <- last_marg_indices[margins_names[[i]]] + input_last_margin_dim_index <- 1 + sum((input_last_margin_dim_index - 1) * + input_margin_weights[[i]]) + } else { + input_first_margin_dim_index <- 1 + input_last_margin_dim_index <- 1 + } + first_data_margin_dim_index_to_take[[i]] <- input_first_margin_dim_index + input_indices_to_take <- as.list(rep(TRUE, length(dim(data[[i]])))) + input_indices_to_take[[length(dim(data[[i]]))]] <- + input_first_margin_dim_index : input_last_margin_dim_index + input[[i]] <- do.call('[', c(list(x = data[[i]]), input_indices_to_take, + list(drop = FALSE))) + } + sub_arrays_of_results <- list() - weights <- lapply(1:length(data_indexed), - function(i) { - ind_dims <- dim(data_indexed_indices[[i]]) - sapply(1:length(ind_dims), - function(k) prod(c(1, ind_dims)[1:k])) - }) found_first_sub_result <- FALSE + iteration_indices_to_take <- list() + for (i in 1:length(input)) { + iteration_indices_to_take[[i]] <- as.list(rep(TRUE, length(dim(input[[i]])))) + } + + # FOR LOOP for (n in 1:chunk_sizes[m]) { - # j is the index of the data piece to load in data_indexed - j <- n + (m - 1) * chunk_size - marg_indices <- arrayInd(j, dim(ma)) - names(marg_indices) <- names(dim(ma)) - input <- list() - # Each iteration of n, the variable input is populated with sub-arrays for - # each object in data (if possible). For each set of 'input's, the - # splatted_f is applied in parallel if possible. - for (i in 1:length(data_indexed)) { - inds_to_take <- which(names(marg_indices) %in% names(dim(data_indexed_indices[[i]]))) - if (length(inds_to_take) > 0) { - marg_inds_to_take <- marg_indices[inds_to_take][names(dim(data_indexed_indices[[i]]))] - marg_inds_to_take <- marg_indices[inds_to_take][names(dim(data_indexed_indices[[i]]))] - inds_from_dii <- 1 + sum((marg_inds_to_take - 1) * weights[[i]]) - inds <- data_indexed_indices[[i]][inds_from_dii] - input[[i]] <- data_indexed[[i]][[inds]] - #input[[i]] <- data_indexed[[i]][[do.call("[", - # c(list(x = data_indexed_indices[[i]]), marg_inds_to_take, - # list(drop = TRUE)))]] - } else { - input[[i]] <- data_indexed[[i]][[1]] - } + # SUB-ITERATION INTRO + # SUB-INTRO FIRST HALF + iteration_index <- (m - 1) * chunk_size + n + iteration_margin_indices <- arrayInd(iteration_index, mad) + names(iteration_margin_indices) <- names(mad) + iteration_input <- list() + iteration_index <- (m - 1) * chunk_size + n + iteration_margin_indices <- arrayInd(iteration_index, mad) + names(iteration_margin_indices) <- names(mad) + iteration_input <- list() + # SUB-IT. INTRO SECOND HALF + for (i in 1:length(input)) { + input_margin_dim_index <- iteration_margin_indices[margins_names[[i]]] + input_margin_dim_index <- 1 + sum((input_margin_dim_index - 1) * + input_margin_weights[[i]]) + iteration_indices_to_take[[i]][[length(dim(data[[i]]))]] <- + input_margin_dim_index - first_data_margin_dim_index_to_take[[i]] + 1 + iteration_input[[i]] <- do.call('[', c(list(x = input[[i]]), + iteration_indices_to_take[[i]], + list(drop = FALSE))) + dim(iteration_input[[i]]) <- dim(iteration_input[[i]])[-length(dim(iteration_input[[i]]))] } - result <- splatted_f(.consolidate(input, lapply(lapply(data, dim), names), - target_dims), ...) + + # SPLATTED_F + result <- splatted_f(iteration_input, ...) + + # SUB-ITERATION OUTRO if (!is.list(result)) { result <- list(result) } @@ -488,7 +481,7 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., output_dims = NULL, sub_arrays_of_results <- vector('list', length(result)) if (!is.null(output_dims)) { if (length(output_dims) != length(sub_arrays_of_results)) { - stop("The 'AtomicFun' returns ", length(sub_arrays_of_results), + stop("The 'fun' returns ", length(sub_arrays_of_results), " elements, but ", length(output_dims), " elements were expected.") } @@ -525,13 +518,13 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., output_dims = NULL, if (!is.null(output_dims)) { # Check number of outputs. if (length(output_dims) != length(result)) { - stop("Expected AtomicFun to return ", length(output_dims), " components, ", + stop("Expected fun to return ", length(output_dims), " components, ", "but ", length(result), " found.") } # Check number of output dimensions is correct. for (component in 1:length(result)) { if (length(atomic_fun_out_dims[[component]]) != length(output_dims[[component]])) { - stop("Expected ", component, "st returned element by 'AtomicFun' ", + stop("Expected ", component, "st returned element by 'fun' ", "to have ", length(output_dims[[component]]), " dimensions, ", "but ", length(atomic_fun_out_dims[[component]]), " found.") } @@ -546,8 +539,9 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., output_dims = NULL, if (parallel) registerDoParallel(ncores) result <- llply(1:length(chunk_sizes), iteration, .parallel = parallel) if (parallel) registerDoSEQ() - # Merge the results + arrays_of_results <- NULL + found_first_result <- FALSE chunk_length <- NULL fun_out_dims <- vector('list', length(result[[1]])) for (m in 1:length(result)) { @@ -555,7 +549,7 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., output_dims = NULL, arrays_of_results <- vector('list', length(result[[1]])) if (!is.null(output_dims)) { if (length(output_dims) != length(arrays_of_results)) { - stop("The 'AtomicFun' returns ", length(arrays_of_results), " elements, but ", + stop("The 'fun' returns ", length(arrays_of_results), " elements, but ", length(output_dims), " elements were expected.") } names(arrays_of_results) <- names(output_dims) @@ -572,7 +566,7 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., output_dims = NULL, fun_out_dims[[component]] <- component_dims[- length(component_dims)] } arrays_of_results[[component]] <- array(dim = c(fun_out_dims[[component]], - dim(ma))) + mad)) dimnames_to_remove <- which(grepl('^_unnamed_margin_', names(dim(arrays_of_results[[component]])))) if (length(dimnames_to_remove) > 0) { -- GitLab From db2956ef373366bd26daed3f623dd92811d32d93 Mon Sep 17 00:00:00 2001 From: Nicolau Manubens Date: Wed, 7 Nov 2018 22:03:37 +0100 Subject: [PATCH 05/19] Added automatic testing. WIP. Added bugfixes. --- DESCRIPTION | 8 +- R/Apply.R | 99 ++-- R/zzz.R | 10 +- README.md | 2 + tests/testthat.R | 4 + tests/testthat/test-sanity-checks.R | 775 ++++++++++++++++++++++++++++ 6 files changed, 851 insertions(+), 47 deletions(-) create mode 100644 tests/testthat.R create mode 100644 tests/testthat/test-sanity-checks.R diff --git a/DESCRIPTION b/DESCRIPTION index 0e1cede..1f13960 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -3,9 +3,9 @@ Title: Apply Functions to Multiple Multidimensional Arguments Version: 1.0.0 Authors@R: c( person("BSC-CNS", role = c("aut", "cph")), - person("Alasdair", "Hunter", , "alasdair.hunter@bsc.es", role = c("aut", "cre")), - person("Nicolau", "Manubens", , "nicolau.manubens@bsc.es", role = "aut")) -Description: The base apply function and its variants, as well as the related functions in the 'plyr' package, typically apply user-defined functions to a single argument (or a list of vectorized arguments in the case of mapply). The 'multiApply' package extends this paradigm to functions taking a list of multiple unidimensional or multidimensional arguments (or combinations thereof) as input, which can have different numbers of dimensions as well as different dimension lengths. + person("Nicolau", "Manubens", , "nicolau.manubens@bsc.es", role = "aut"), + person("Alasdair", "Hunter", , "alasdair.hunter@bsc.es", role = c("aut", "cre"))) +Description: The base apply function and its variants, as well as the related functions in the 'plyr' package, typically apply user-defined functions to a single argument (or a list of vectorized arguments in the case of mapply). The 'multiApply' package extends this paradigm to functions taking one or a list of multiple unidimensional or multidimensional arguments (or combinations thereof) as input, which can have different numbers of dimensions as well as different dimension lengths, and returning one or a list of unidimensional or multidimensional arrays as output. In contrast to apply and variants, this package suggests the use of 'target dimensions' as opposite to the 'margins' for specifying the dimensions relevant to the function to be applied. Also, two remarkable differences are the support for functions returning multiple array outputs and the transparent use of multi-core. Depends: R (>= 3.2.0) Imports: @@ -13,6 +13,8 @@ Imports: doParallel, foreach, plyr +Suggests: + testthat License: LGPL-3 URL: https://earth.bsc.es/gitlab/ces/multiApply BugReports: https://earth.bsc.es/gitlab/ces/multiApply/issues diff --git a/R/Apply.R b/R/Apply.R index 081be93..8b3baa5 100644 --- a/R/Apply.R +++ b/R/Apply.R @@ -35,6 +35,9 @@ Apply <- function(data, target_dims = NULL, fun, ..., output_dims = NULL, is_unnamed <- rep(FALSE, length(data)) unnamed_dims <- c() for (i in 1 : length(data)) { + if (length(data[[i]]) < 1) { + stop("Arrays in 'data' must be of length > 0.") + } if (is.null(dim(data[[i]]))) { is_vector[i] <- TRUE is_unnamed[i] <- TRUE @@ -64,8 +67,8 @@ Apply <- function(data, target_dims = NULL, fun, ..., output_dims = NULL, names(dim(data[[i]]))[j] <- names(unnamed_dims)[found_match] } else { new_dim <- len_of_dim_j - names(new_dim) <- paste0('unnamed_dim_', length(unnamed_dims) + - length(new_unnamed_dims) + 1) + names(new_dim) <- paste0('_unnamed_dim_', length(unnamed_dims) + + length(new_unnamed_dims) + 1, '_') new_unnamed_dims <- c(new_unnamed_dims, new_dim) names(dim(data[[i]]))[j] <- names(new_dim) } @@ -85,25 +88,28 @@ Apply <- function(data, target_dims = NULL, fun, ..., output_dims = NULL, stop("Parameter 'fun' must be a function or a character string ", "with the name of a function.") } - if ('startR_step' %in% class(fun)) { + if (!is.null(attributes(fun))) { if (is.null(target_dims)) { - target_dims <- attr(fun, 'target_dims') + if ('target_dims' %in% names(attributes(fun))) { + target_dims <- attr(fun, 'target_dims') + } } if (is.null(output_dims)) { - output_dims <- attr(fun, 'output_dims') + if ('output_dims' %in% names(attributes(fun))) { + output_dims <- attr(fun, 'output_dims') + } } } # Check target_dims and margins - if (is.null(margins) && is.null(target_dims)) { + arglist <- as.list(match.call()) + if (!any(c('margins', 'target_dims') %in% names(arglist)) && + is.null(target_dims)) { stop("One of 'margins' or 'target_dims' must be specified.") } - if (!is.null(margins)) { - target_dims <- NULL - } margins_names <- vector('list', length(data)) target_dims_names <- vector('list', length(data)) - if (!is.null(margins)) { + if ('margins' %in% names(arglist)) { # Check margins and build target_dims accordingly if (!is.list(margins)) { margins <- rep(list(margins), length(data)) @@ -113,8 +119,8 @@ Apply <- function(data, target_dims = NULL, fun, ..., output_dims = NULL, stop("Parameter 'margins' must be one or a list of numeric or ", "character vectors.") } - if (any(sapply(margins, length) == 0)) { - stop("Parameter 'margins' must not contain length-0 vectors.") + if (any(sapply(margins, function(x) is.character(x) && (length(x) == 0)))) { + stop("Parameter 'margins' must not contain length-0 character vectors.") } duplicate_dim_specs <- sapply(margins, function(x) { @@ -149,11 +155,10 @@ Apply <- function(data, target_dims = NULL, fun, ..., output_dims = NULL, if (length(margins[[i]]) == length(dim(data[[i]]))) { target_dims_names[i] <- list(NULL) target_dims[i] <- list(NULL) + margins_names[[i]] <- names(dim(data[[i]])) } else { - if (!is.null(names(dim(data[[i]])))) { - margins_names[[i]] <- names(dim(data[[i]]))[margins[[i]]] - target_dims_names[[i]] <- names(dim(data[[i]]))[- margins[[i]]] - } + margins_names[[i]] <- names(dim(data[[i]]))[margins[[i]]] + target_dims_names[[i]] <- names(dim(data[[i]]))[- margins[[i]]] target_dims[[i]] <- (1 : length(dim(data[[i]])))[- margins[[i]]] } } else { @@ -169,12 +174,12 @@ Apply <- function(data, target_dims = NULL, fun, ..., output_dims = NULL, target_dims <- rep(list(target_dims), length(data)) } if (any(!sapply(target_dims, - function(x) is.character(x) || is.numeric(x)))) { + function(x) is.character(x) || is.numeric(x) || is.null(x)))) { stop("Parameter 'target_dims' must be one or a list of numeric or ", "character vectors.") } - if (any(sapply(target_dims, length) == 0)) { - stop("Parameter 'target_dims' must not contain length-0 vectors.") + if (any(sapply(target_dims, function(x) is.character(x) && (length(x) == 0)))) { + stop("Parameter 'target_dims' must not contain length-0 character vectors.") } duplicate_dim_specs <- sapply(target_dims, function(x) { @@ -209,11 +214,10 @@ Apply <- function(data, target_dims = NULL, fun, ..., output_dims = NULL, if (length(target_dims) == length(dim(data[[i]]))) { margins_names[i] <- list(NULL) margins[i] <- list(NULL) + target_dims_names[[i]] <- names(dim(data[[i]])) } else { - if (!is.null(names(dim(data[[i]])))) { - target_dims_names[[i]] <- names(dim(data[[i]]))[target_dims[[i]]] - margins_names[[i]] <- names(dim(data[[i]]))[- target_dims[[i]]] - } + target_dims_names[[i]] <- names(dim(data[[i]]))[target_dims[[i]]] + margins_names[[i]] <- names(dim(data[[i]]))[- target_dims[[i]]] margins[[i]] <- (1 : length(dim(data[[i]])))[- target_dims[[i]]] } } else { @@ -302,7 +306,7 @@ Apply <- function(data, target_dims = NULL, fun, ..., output_dims = NULL, # across them, and use data arrays repeatedly as needed. margins_afml <- margins for (i in 1:length(data)) { - #if (!is.null(margins_names[[i]])) { + if (length(margins[[i]]) > 0) { margins_afml[[i]] <- sapply(margins_names[[i]], function(x) { sapply(x, @@ -312,7 +316,7 @@ Apply <- function(data, target_dims = NULL, fun, ..., output_dims = NULL, ) } ) - #} + } } common_margs <- margins_afml[[1]] if (length(margins_afml) > 1) { @@ -327,15 +331,18 @@ Apply <- function(data, target_dims = NULL, fun, ..., output_dims = NULL, } } } - non_common_margs <- 1:length(afml) - if (length(common_margs) > 0) { - non_common_margs <- non_common_margs[- common_margs] + if (length(afml) > 0) { + non_common_margs <- 1:length(afml) + if (length(common_margs) > 0) { + non_common_margs <- non_common_margs[- common_margs] + } + } else { + non_common_margs <- NULL } # common_margs is now a numeric vector with the indices of the common # margins (i.e. their position in afml) # non_common_margs is now a numeric vector with the indices of the # non-common margins (i.e. their position in afml) - if (length(c(non_common_margs, common_margs)) > 0) { marg_inds_ordered <- sort(c(non_common_margs, common_margs)) margins_array_dims <- mad <- unlist(afml[marg_inds_ordered]) @@ -387,7 +394,6 @@ Apply <- function(data, target_dims = NULL, fun, ..., output_dims = NULL, dim(data[[i]]) <- c(dim(data[[i]]), '_margins_dim_' = 1) } } - # TODO: need to add progress bar # TODO: IF ONLY ONE INPUT ARRAY, MAKE USE OF apply. splatted_f <- splat(fun) @@ -419,7 +425,7 @@ Apply <- function(data, target_dims = NULL, fun, ..., output_dims = NULL, first_data_margin_dim_index_to_take <- list() input <- list() for (i in 1:length(data)) { - if (length(margins[[i]]) > 1) { + if (length(margins[[i]]) > 0) { input_first_margin_dim_index <- first_marg_indices[margins_names[[i]]] input_first_margin_dim_index <- 1 + sum((input_first_margin_dim_index - 1) * input_margin_weights[[i]]) @@ -467,7 +473,12 @@ Apply <- function(data, target_dims = NULL, fun, ..., output_dims = NULL, iteration_input[[i]] <- do.call('[', c(list(x = input[[i]]), iteration_indices_to_take[[i]], list(drop = FALSE))) - dim(iteration_input[[i]]) <- dim(iteration_input[[i]])[-length(dim(iteration_input[[i]]))] + num_dims <- length(dim(iteration_input[[i]])) + if (num_dims > 1) { + dim(iteration_input[[i]]) <- dim(iteration_input[[i]])[-length(dim(iteration_input[[i]]))] + } else { + dim(iteration_input[[i]]) <- NULL + } } # SPLATTED_F @@ -491,6 +502,10 @@ Apply <- function(data, target_dims = NULL, fun, ..., output_dims = NULL, } else { names(sub_arrays_of_results) <- paste0('output', 1:length(result)) } + len0_names <- which(nchar(names(sub_arrays_of_results)) == 0) + if (length(len0_names) > 0) { + names(sub_arrays_of_results)[len0_names] <- paste0('output', len0_names) + } } atomic_fun_out_dims <- vector('list', length(result)) for (component in 1:length(result)) { @@ -565,15 +580,17 @@ Apply <- function(data, target_dims = NULL, fun, ..., output_dims = NULL, if (length(component_dims) > 1) { fun_out_dims[[component]] <- component_dims[- length(component_dims)] } - arrays_of_results[[component]] <- array(dim = c(fun_out_dims[[component]], - mad)) - dimnames_to_remove <- which(grepl('^_unnamed_margin_', - names(dim(arrays_of_results[[component]])))) - if (length(dimnames_to_remove) > 0) { - names(dim(arrays_of_results[[component]]))[dimnames_to_remove] <- rep('', length(dimnames_to_remove)) - } - if (all(names(dim(arrays_of_results[[component]])) == '')) { - names(dim(arrays_of_results[[component]])) <- NULL + if (length(fun_out_dims[[component]]) + length(mad) > 0) { + arrays_of_results[[component]] <- array(dim = c(fun_out_dims[[component]], + mad)) + dimnames_to_remove <- which(grepl('^_unnamed_dim_', + names(dim(arrays_of_results[[component]])))) + if (length(dimnames_to_remove) > 0) { + names(dim(arrays_of_results[[component]]))[dimnames_to_remove] <- rep('', length(dimnames_to_remove)) + } + if (all(names(dim(arrays_of_results[[component]])) == '')) { + names(dim(arrays_of_results[[component]])) <- NULL + } } chunk_length <- prod(component_dims) } diff --git a/R/zzz.R b/R/zzz.R index 3e04077..4df33e9 100644 --- a/R/zzz.R +++ b/R/zzz.R @@ -1,9 +1,13 @@ # Function to permute arrays of non-atomic elements (e.g. POSIXct) .aperm2 <- function(x, new_order) { - y <- array(1:length(x), dim = dim(x)) - y <- aperm(y, new_order) old_dims <- dim(x) - x <- x[as.vector(y)] + if (is.numeric(x)) { + x <- aperm(x, new_order) + } else { + y <- array(1:length(x), dim = dim(x)) + y <- aperm(y, new_order) + x <- x[as.vector(y)] + } dim(x) <- old_dims[new_order] x } diff --git a/README.md b/README.md index 5450824..6c984cb 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ ## multiApply +[![](https://cranlogs.r-pkg.org/badges/ggplot2)](https://cran.rstudio.com/web/packages/ggplot2/index.html) + This package extends the apply and plyr families of functions to applications which involve the use of multiple arrays as input, and is useful to apply a function taking multiple numeric objects as input across multiple multi-dimensional arrays. This is especially useful for climate data related applications, where data is often distributed across multiple arrays with different dimensions (e.g experimental array 1, experimental array 2 and the observations). The `multiApply::Apply` function reduces the need to write loops for every application. diff --git a/tests/testthat.R b/tests/testthat.R new file mode 100644 index 0000000..b948b35 --- /dev/null +++ b/tests/testthat.R @@ -0,0 +1,4 @@ +library(testthat) +library(multiApply) + +test_check("multiApply") diff --git a/tests/testthat/test-sanity-checks.R b/tests/testthat/test-sanity-checks.R new file mode 100644 index 0000000..d1ff9fd --- /dev/null +++ b/tests/testthat/test-sanity-checks.R @@ -0,0 +1,775 @@ +context("Sanity checks") + +test_that("required arguments are provided", { + expect_error( + Apply(), + "missing, with no default" + ) + expect_error( + Apply(1:10), + "missing, with no default" + ) + expect_error( + Apply(1:10, fun = mean), + "must be specified" + ) +}) + +test_that("arguments have the right type", { + expect_equal( + Apply(1:10, NULL, mean), + list(output1 = array(1:10, dim = 10)) + ) + expect_error( + Apply(numeric(0), NULL, mean), + "must be of length > 0" + ) + #expect_error( + #) +}) + + +#context("Parameter order") +# +#test_that("", { +# expect_that( +# Apply(), +# throws_error("") +# ) +#}) + + +context("Use cases") + +test_that("in1: 1 val; targ. dims: 0; out1: 0 val", { + f <- function(x) NULL + expect_equal( + Apply(1, NULL, f), + list(output1 = array(logical(0), dim = c(0, 1))) + ) + f <- function(x) numeric(0) + expect_equal( + Apply(1, NULL, f), + list(output1 = array(logical(0), dim = c(0, 1))) + ) +}) + +test_that("in1: 1 val; targ. dims: 0; out1: 0 val", { + f <- function(x) NULL + expect_equal( + Apply(1, 1, f), + list(output1 = array(logical(0))) + ) + f <- function(x) numeric(0) + expect_equal( + Apply(1, 1, f), + list(output1 = array(logical(0))) + ) +}) + +test_that("in1: 1 val; targ. dims: 0; out1: 1 val", { + expect_equal( + Apply(1, NULL, mean), + list(output1 = array(1)) + ) +}) + +test_that("in1: 1 val; targ. dims: 1; out1: 1 val", { + expect_equal( + Apply(1, 1, mean), + list(output1 = 1) + ) +}) + +test_that("in1: 1 val; targ. dims: 0; out1: 3 val", { + f <- function(x) x:(x + 2) + expect_equal( + Apply(1, NULL, f), + list(output1 = array(1:3, dim = c(3, 1))) + ) +}) + +test_that("in1: 1 val; targ. dims: 1; out1: 3 val", { + f <- function(x) x:(x + 2) + expect_equal( + Apply(1, 1, f), + list(output1 = array(1:3)) + ) +}) + +test_that("in1: 1 val; targ. dims: 0; out1: 1 dim", { + # unnamed output dim + f <- function(x) array(x:(x + 2)) + expect_equal( + Apply(1, NULL, f), + list(output1 = array(1:3, dim = c(3, 1))) + ) + # named output dim + f <- function(x) array(x:(x + 2), dim = c(time = 3)) + expect_equal( + Apply(1, NULL, f), + list(output1 = array(1:3, dim = c(time = 3, 1))) + ) +}) + +test_that("in1: 1 val; targ. dims: 1; out1: 1 dim", { + # unnamed output dim + f <- function(x) array(x:(x + 2)) + expect_equal( + Apply(1, 1, f), + list(output1 = array(1:3)) + ) + # named output dim + f <- function(x) array(x:(x + 2), dim = c(time = 3)) + expect_equal( + Apply(1, 1, f), + list(output1 = array(1:3, dim = c(time = 3))) + ) +}) + +test_that("in1: 1 val; targ. dims: 0; out1: 2 dim", { + # unnamed output dims + f <- function(x) array(x:(x + 2), dim = c(3, 4)) + expect_equal( + Apply(1, NULL, f), + list(output1 = array(1:3, dim = c(3, 4, 1))) + ) + # named output dim 1 + f <- function(x) array(x:(x + 2), dim = c(time = 3, 4)) + expect_equal( + Apply(1, NULL, f), + list(output1 = array(1:3, dim = c(time = 3, 4, 1))) + ) + # named output dim 2 + f <- function(x) array(x:(x + 2), dim = c(3, region = 4)) + expect_equal( + Apply(1, NULL, f), + list(output1 = array(1:3, dim = c(3, region = 4, 1))) + ) + # named output dims + f <- function(x) array(x:(x + 2), dim = c(time = 3, region = 4)) + expect_equal( + Apply(1, NULL, f), + list(output1 = array(1:3, dim = c(time = 3, region = 4, 1))) + ) +}) + +test_that("in1: 1 val; targ. dims: 1; out1: 2 dim", { + # unnamed output dims + f <- function(x) array(x:(x + 2), dim = c(3, 4)) + expect_equal( + Apply(1, 1, f), + list(output1 = array(1:3, dim = c(3, 4))) + ) + # named output dims + f <- function(x) array(x:(x + 2), dim = c(time = 3, region = 4)) + expect_equal( + Apply(1, 1, f), + list(output1 = array(1:3, dim = c(time = 3, region = 4))) + ) +}) + +test_that("in1: 1 val; targ. dims: 0; out1: 1 val; out 2: 1 val", { + # unnamed outputs + f <- function(x) list(mean(x), mean(x) + 1) + expect_equal( + Apply(1, NULL, f), + list(output1 = array(1), + output2 = array(2)) + ) + # named outputs + f <- function(x) list(a = mean(x), b = mean(x) + 1) + expect_equal( + Apply(1, NULL, f), + list(a = array(1), + b = array(2)) + ) +}) + +test_that("in1: 1 val; targ. dims: 1; out1: 3 val; out 2: 2 dim", { + f <- function(x) list(a = x:(x + 2), array(x:(x + 2), dim = c(3, region = 4))) + expect_equal( + Apply(1, 1, f), + list(a = array(1:3), + output2 = array(1:3, dim = c(3, region = 4))) + ) +}) + +test_that("in1: 1 dim; targ. dims: 0; out1: 0 val", { + # unnamed dim + # unnamed output + f <- function(x) NULL + expect_equal( + Apply(array(1:10), NULL, f), + list(output1 = array(logical(0), dim = c(0, 10))) + ) + # named dim + # named output + f <- function(x) list(out1 = NULL) + expect_equal( + Apply(array(1:10, dim = c(a = 10)), NULL, f), + list(out1 = array(logical(0), dim = c(0, a = 10))) + ) +}) + +test_that("in1: 1 dim; targ. dims: 1; out1: 0 val", { + # unnamed dim + # unnamed output + f <- function(x) NULL + expect_equal( + Apply(array(1:10), 1, f), + list(output1 = array(logical(0))) + ) + # named dim + # named output + f <- function(x) list(out1 = NULL) + expect_equal( + Apply(array(1:10, dim = c(a = 10)), 1, f), + list(out1 = array(logical(0), dim = c(0))) + ) + # named target dim + expect_equal( + Apply(array(1:10, dim = c(a = 10)), 'a', f), + list(out1 = array(logical(0), dim = c(0))) + ) +}) + +test_that("in1: 1 dim; targ. dims: 0; out1: 1 val", { + # unnamed dim + # unnamed output + expect_equal( + Apply(array(1:10), NULL, mean), + list(output1 = array(1:10)) + ) + # named dim + # named output + f <- function(x) list(out1 = mean(x)) + expect_equal( + Apply(array(1:10, dim = c(a = 10)), NULL, f), + list(out1 = array(1:10, dim = c(a = 10))) + ) +}) + +test_that("in1: 1 dim; targ. dims: 1; out1: 1 val", { + # unnamed dim + # unnamed output + expect_equal( + Apply(array(1:10), 1, mean), + list(output1 = 5.5) + ) + # named dim + # named output + f <- function(x) list(out1 = mean(x)) + expect_equal( + Apply(array(1:10, dim = c(a = 10)), 1, f), + list(out1 = 5.5) + ) + # named target dim + expect_equal( + Apply(array(1:10, dim = c(a = 10)), 'a', f), + list(out1 = 5.5) + ) +}) + +test_that("in1: 1 dim; targ. dims: 0; out1: 3 vals", { + # unnamed dim + # unnamed output + f <- function(x) x:(x + 2) + expect_equal( + Apply(array(1:10), NULL, f), + list(output1 = array(sapply(1:10, function(x) x:(x + 2)), dim = c(3, 10))) + ) + # named dim + # named output + f <- function(x) list(out1 = x:(x + 2)) + expect_equal( + Apply(array(1:10, dim = c(a = 10)), NULL, f), + list(out1 = array(sapply(1:10, function(x) x:(x + 2)), dim = c(3, a = 10))) + ) +}) + +test_that("in1: 1 dim; targ. dims: 1; out1: 3 vals", { + # unnamed dim + # unnamed output + f <- function(x) x[1]:(x[1] + 2) + expect_equal( + Apply(array(1:10), 1, f), + list(output1 = array(1:3)) + ) + # named dim + # named output + f <- function(x) list(out1 = x[1]:(x[1] + 2)) + expect_equal( + Apply(array(1:10, dim = c(a = 10)), 1, f), + list(out1 = array(1:3)) + ) + # named target dim + expect_equal( + Apply(array(1:10, dim = c(a = 10)), 'a', f), + list(out1 = array(1:3)) + ) +}) + +test_that("in1: 1 dim; targ. dims: 0; out1: 1 dim", { + # unnamed input dim + # unnamed output + # unnamed output dim + f <- function(x) array(x:(x + 2)) + expect_equal( + Apply(array(1:10), NULL, f), + list(output1 = array(sapply(1:10, function(x) x:(x + 2)), dim = c(3, 10))) + ) + # named input dim + # named output + # named output dim + f <- function(x) list(out1 = array(x:(x + 2), dim = c(b = 3))) + expect_equal( + Apply(array(1:10, dim = c(a = 10)), NULL, f), + list(out1 = array(sapply(1:10, function(x) x:(x + 2)), dim = c(b = 3, a = 10))) + ) +}) + +test_that("in1: 1 dim; targ. dims: 1; out1: 1 dim", { + # unnamed input dim + # unnamed output + # unnamed output dim + f <- function(x) array(x[1]:(x[1] + 2)) + expect_equal( + Apply(array(1:10), 1, f), + list(output1 = array(1:3)) + ) + # named input dim + # named output + # named output dim + f <- function(x) list(out1 = array(x[1]:(x[1] + 2), dim = c(b = 3))) + expect_equal( + Apply(array(1:10, dim = c(a = 10)), 1, f), + list(out1 = array(1:3, dim = c(b = 3))) + ) + # named target dim + expect_equal( + Apply(array(1:10, dim = c(a = 10)), 'a', f), + list(out1 = array(1:3, dim = c(b = 3))) + ) +}) + +test_that("in1: 1 dim; targ. dims: 0; out1: 2 dims", { + # unnamed input dim + # unnamed output + # unnamed output dims + f <- function(x) array(x:(x + 2), dim = c(3, 4)) + expect_equal( + Apply(array(1:10), NULL, f), + list(output1 = array(sapply(1:10, function(x) rep(x:(x + 2), 4)), dim = c(3, 4, 10))) + ) + # named input dim + # named output + # named output dim + f <- function(x) list(out1 = array(x:(x + 2), dim = c(b = 3, c = 4))) + expect_equal( + Apply(array(1:10, dim = c(a = 10)), NULL, f), + list(out1 = array(sapply(1:10, function(x) rep(x:(x + 2), 4)), dim = c(b = 3, c = 4, a = 10))) + ) +}) + +test_that("in1: 1 dim; targ. dims: 1; out1: 2 dims", { + # unnamed input dim + # unnamed output + # unnamed output dim + f <- function(x) array(x[1]:(x[1] + 2), dim = c(3, 4)) + expect_equal( + Apply(array(1:10), 1, f), + list(output1 = array(sapply(1:10, function(x) rep(x:(x + 2), 4)), dim = c(3, 4))) + ) + # named input dim + # named output + # named output dim + f <- function(x) list(out1 = array(x[1]:(x[1] + 2), dim = c(b = 3, c = 4))) + expect_equal( + Apply(array(1:10, dim = c(a = 10)), 1, f), + list(out1 = array(sapply(1:10, function(x) rep(x:(x + 2), 4)), dim = c(b = 3, c = 4))) + ) + # named target dim + expect_equal( + Apply(array(1:10, dim = c(a = 10)), 'a', f), + list(out1 = array(sapply(1:10, function(x) rep(x:(x + 2), 4)), dim = c(b = 3, c = 4))) + ) +}) + +# FAILS +test_that("in1: 1 dim; targ. dims: 0; out1: 1 dim; out2: 2 dims; out3: 1 val", { + f <- function(x) list(array(x:(x + 2), dim = c(3)), + array(x:(x + 3), dim = c(4, 5)), + mean(x)) + expect_equal( + Apply(array(1:10), NULL, f), + list(output1 = array(sapply(1:10, function(x) x:(x + 2)), dim = c(3, 10)), + output2 = array(sapply(1:10, function(x) rep(x:(x + 3), 5)), dim = c(4, 5, 10)), + output3 = array(1:10)) + ) +}) + +test_that("in1: 1 dim; targ. dims: 1; out1: 1 dim; out2: 2 dims; out3: 1 val", { +}) + +test_that("in1: 2 dim; targ. dims: 0; out1: 1 dim; out2: 2 dims; out3: 1 val", { +}) + +test_that("in1: 2 dim; targ. dims: 1; out1: 1 dim; out2: 2 dims; out3: 1 val", { +# first target dim +## without name +## with name +# second target dim +## without name +## with name +}) + +test_that("in1: 2 dim; targ. dims: 2; out1: 1 dim; out2: 2 dims; out3: 1 val", { +# without name +# with name +}) + +test_that("in1: 3 dim; targ. dims: 0; out1: 1 dim; out2: 2 dims; out3: 1 val", { +}) + +test_that("in1: 3 dim; targ. dims: 1; out1: 1 dim; out2: 2 dims; out3: 1 val", { +# first target dim +## without name +## with name +# second target dim +## without name +## with name +# third target dim +## without name +## with name +}) + +test_that("in1: 3 dim; targ. dims: 2; out1: 1 dim; out2: 2 dims; out3: 1 val", { +# first couple +## without name +## with name +# second couple +## without name +## with name +# extremes couple +## without name +## with name +}) + +test_that("in1: 1 val; in2: 1 val; targ. dims: 0, 0; out1: 0 val", { +}) + +test_that("in1: 1 val; in2: 1 val; targ. dims: 1, 0; out1: 0 val", { +}) + +test_that("in1: 1 val; in2: 1 val; targ. dims: 0, 1; out1: 0 val", { +}) + +test_that("in1: 1 val; in2: 1 val; targ. dims: 1, 1; out1: 0 val", { +}) + +test_that("in1: 1 val; in2: 1 val; targ. dims: 0, 0; out1: 1 val", { +}) + +test_that("in1: 1 val; in2: 1 val; targ. dims: 1, 0; out1: 1 val", { +}) + +test_that("in1: 1 val; in2: 1 val; targ. dims: 0, 1; out1: 1 val", { +}) + +test_that("in1: 1 val; in2: 1 val; targ. dims: 1, 1; out1: 1 val", { +}) + +test_that("in1: 1 val; in2: 1 val; targ. dims: 0, 0; out1: 3 val", { +}) + +test_that("in1: 1 val; in2: 1 val; targ. dims: 1, 0; out1: 3 val", { +}) + +test_that("in1: 1 val; in2: 1 val; targ. dims: 0, 1; out1: 3 val", { +}) + +test_that("in1: 1 val; in2: 1 val; targ. dims: 1, 1; out1: 3 val", { +}) + +test_that("in1: 1 val; in2: 1 val; targ. dims: 0, 0; out1: 1 dim", { +}) + +test_that("in1: 1 val; in2: 1 val; targ. dims: 1, 0; out1: 1 dim", { +}) + +test_that("in1: 1 val; in2: 1 val; targ. dims: 0, 1; out1: 1 dim", { +}) + +test_that("in1: 1 val; in2: 1 val; targ. dims: 1, 1; out1: 1 dim", { +}) + +test_that("in1: 1 val; in2: 1 val; targ. dims: 0, 0; out1: 1 dim; out2: 1 val; out3: 3 dim", { +}) + +test_that("in1: 1 val; in2: 1 val; targ. dims: 1, 0; out1: 1 dim; out2: 1 val; out3: 3 dim", { +}) + +test_that("in1: 1 val; in2: 1 val; targ. dims: 0, 1; out1: 1 dim; out2: 1 val; out3: 3 dim", { +}) + +test_that("in1: 1 val; in2: 1 val; targ. dims: 1, 1; out1: 1 dim; out2: 1 val; out3: 3 dim", { +}) + + + + + + + + + + +test_that("in1: 1 val; in2: 1 dim; targ. dims: 0, 0; out1: 0 val", { +}) + +test_that("in1: 1 val; in2: 1 dim; targ. dims: 1, 0; out1: 0 val", { +}) + +test_that("in1: 1 val; in2: 1 dim; targ. dims: 0, 1; out1: 0 val", { +}) + +test_that("in1: 1 val; in2: 1 dim; targ. dims: 1, 1; out1: 0 val", { +}) + +test_that("in1: 1 val; in2: 1 dim; targ. dims: 0, 0; out1: 1 val", { +}) + +test_that("in1: 1 val; in2: 1 dim; targ. dims: 1, 0; out1: 1 val", { +}) + +test_that("in1: 1 val; in2: 1 dim; targ. dims: 0, 1; out1: 1 val", { +}) + +test_that("in1: 1 val; in2: 1 dim; targ. dims: 1, 1; out1: 1 val", { +}) + +test_that("in1: 1 val; in2: 1 dim; targ. dims: 0, 0; out1: 3 val", { +}) + +test_that("in1: 1 val; in2: 1 dim; targ. dims: 1, 0; out1: 3 val", { +}) + +test_that("in1: 1 val; in2: 1 dim; targ. dims: 0, 1; out1: 3 val", { +}) + +test_that("in1: 1 val; in2: 1 dim; targ. dims: 1, 1; out1: 3 val", { +}) + +test_that("in1: 1 val; in2: 1 dim; targ. dims: 0, 0; out1: 1 dim", { +}) + +test_that("in1: 1 val; in2: 1 dim; targ. dims: 1, 0; out1: 1 dim", { +}) + +test_that("in1: 1 val; in2: 1 dim; targ. dims: 0, 1; out1: 1 dim", { +}) + +test_that("in1: 1 val; in2: 1 dim; targ. dims: 1, 1; out1: 1 dim", { +}) + +test_that("in1: 1 val; in2: 1 dim; targ. dims: 0, 0; out1: 1 dim; out2: 1 val; out3: 3 dim", { +}) + +test_that("in1: 1 val; in2: 1 dim; targ. dims: 1, 0; out1: 1 dim; out2: 1 val; out3: 3 dim", { +}) + +test_that("in1: 1 val; in2: 1 dim; targ. dims: 0, 1; out1: 1 dim; out2: 1 val; out3: 3 dim", { +}) + +test_that("in1: 1 val; in2: 1 dim; targ. dims: 1, 1; out1: 1 dim; out2: 1 val; out3: 3 dim", { +}) + + + + + + + + + + +test_that("in1: 2 dim; in2: 1 dim; targ. dims: 0, 0; out1: 0 val", { +}) + +test_that("in1: 2 dim; in2: 1 dim; targ. dims: 1, 0; out1: 0 val", { +#first in first dim +#first in second dim +}) + +test_that("in1: 2 dim; in2: 1 dim; targ. dims: 0, 1; out1: 0 val", { +}) + +test_that("in1: 2 dim; in2: 1 dim; targ. dims: 1, 1; out1: 0 val", { +#shared target dims +##first in first dim +##first in second dim +#not shared target dims +##first in first dim +##first in second dim +}) + +test_that("in1: 2 dim; in2: 1 dim; targ. dims: 2, 1; out1: 0 val", { +# shared first target dim +# shared second target dim +# not shared target dims +}) + +test_that("in1: 2 dim; in2: 1 dim; targ. dims: 0, 0; out1: 1 val", { +}) + +test_that("in1: 2 dim; in2: 1 dim; targ. dims: 1, 0; out1: 1 val", { +#first in first dim +#first in second dim +}) + +test_that("in1: 2 dim; in2: 1 dim; targ. dims: 0, 1; out1: 1 val", { +}) + +test_that("in1: 2 dim; in2: 1 dim; targ. dims: 1, 1; out1: 1 val", { +#shared target dims +##first in first dim +##first in second dim +#not shared target dims +##first in first dim +##first in second dim +}) + +test_that("in1: 2 dim; in2: 1 dim; targ. dims: 2, 1; out1: 1 val", { +# shared first target dim +# shared second target dim +# not shared target dims +}) + +test_that("in1: 2 dim; in2: 1 dim; targ. dims: 0, 0; out1: 3 val", { +}) + +test_that("in1: 2 dim; in2: 1 dim; targ. dims: 1, 0; out1: 3 val", { +#first in first dim +#first in second dim +}) + +test_that("in1: 2 dim; in2: 1 dim; targ. dims: 0, 1; out1: 3 val", { +}) + +test_that("in1: 2 dim; in2: 1 dim; targ. dims: 1, 1; out1: 3 val", { +#shared target dims +##first in first dim +##first in second dim +#not shared target dims +##first in first dim +##first in second dim +}) + +test_that("in1: 2 dim; in2: 1 dim; targ. dims: 2, 1; out1: 3 val", { +# shared first target dim +# shared second target dim +# not shared target dims +}) + +test_that("in1: 2 dim; in2: 1 dim; targ. dims: 0, 0; out1: 1 dim", { +}) + +test_that("in1: 2 dim; in2: 1 dim; targ. dims: 1, 0; out1: 1 dim", { +#first in first dim +#first in second dim +}) + +test_that("in1: 2 dim; in2: 1 dim; targ. dims: 0, 1; out1: 1 dim", { +}) + +test_that("in1: 2 dim; in2: 1 dim; targ. dims: 1, 1; out1: 1 dim", { +#shared target dims +##first in first dim +##first in second dim +#not shared target dims +##first in first dim +##first in second dim +}) + +test_that("in1: 2 dim; in2: 1 dim; targ. dims: 2, 1; out1: 1 dim", { +# shared first target dim +# shared second target dim +# not shared target dims +}) + +test_that("in1: 2 dim; in2: 1 dim; targ. dims: 0, 0; out1: 1 dim; out2: 1 val; out3: 3 dim", { +}) + +test_that("in1: 2 dim; in2: 1 dim; targ. dims: 1, 0; out1: 1 dim; out2: 1 val; out3: 3 dim", { +#first in first dim +#first in second dim +}) + +test_that("in1: 2 dim; in2: 1 dim; targ. dims: 0, 1; out1: 1 dim; out2: 1 val; out3: 3 dim", { +}) + +test_that("in1: 2 dim; in2: 1 dim; targ. dims: 1, 1; out1: 1 dim; out2: 1 val; out3: 3 dim", { +#shared target dims +##first in first dim +##first in second dim +#not shared target dims +##first in first dim +##first in second dim +}) + +test_that("in1: 2 dim; in2: 1 dim; targ. dims: 2, 1; out1: 1 dim; out2: 1 val; out3: 3 dim", { +# shared first target dim +# shared second target dim +# not shared target dims +}) + + + + + + + + + + + + +test_that("in1: 2 dim; in2: 3 dim; targ. dims: 0-2, 0-3; out1: 2 dim", { +# shared first target dim +# shared second target dim +# shared two target dims (first two in second in) +# shared two target dims (last two in second in) +# shared two target dims (extreme two in second in) +# not shared target dims +}) + +test_that("in1: 2 dim; in2: 3 dim; targ. dims: 0-2, 0-3; out1: 1 dim; out2: 1 val; out3: 3 dim", { +# shared first target dim +# shared second target dim +# shared two target dims (first two in second in) +# shared two target dims (last two in second in) +# shared two target dims (extreme two in second in) +# not shared target dims +}) + +test_that("in1: 2 dim; in2: 3 dim; in3: 1 dim; targ. dims: 0-2, 0-3, 0-1; out1: 2 dim", { +# shared first target dim +# shared second target dim +# shared two target dims (first two in second in) +# shared two target dims (last two in second in) +# shared two target dims (extreme two in second in) +# not shared target dims +}) + + + +# TODOS: +# TESTS FOR MARGINS +# TESTS FOR DISORDERED TARGET_DIMS +# TESTS FOR FUN WITH TARGET_DIMS AND OUTPUT_DIMS ATTACHED +# TESTS FOR FUNCTIONS RECEIVING ADDITIONAL PARAMETERS +# TESTS FOR SPLIT FACTOR +# TESTS FOR NCORES +# TESTS OF WALLCLOCK TIME +# TESTS OF MEMORY FOOTPRINT -- GitLab From c501e1de84109b341f881a864d3f383fd7243c35 Mon Sep 17 00:00:00 2001 From: Nicolau Manubens Date: Fri, 9 Nov 2018 22:10:48 +0100 Subject: [PATCH 06/19] Major bug fixes. Extended unit tests. --- R/Apply.R | 86 +- tests/testthat/test-sanity-checks.R | 742 +---------------- tests/testthat/test-use-cases.R | 1183 +++++++++++++++++++++++++++ 3 files changed, 1220 insertions(+), 791 deletions(-) create mode 100644 tests/testthat/test-use-cases.R diff --git a/R/Apply.R b/R/Apply.R index 8b3baa5..83ab76b 100644 --- a/R/Apply.R +++ b/R/Apply.R @@ -60,11 +60,9 @@ Apply <- function(data, target_dims = NULL, fun, ..., output_dims = NULL, for (j in 1 : length(dim(data[[i]]))) { len_of_dim_j <- dim(data[[i]])[j] found_match <- which(unnamed_dims == len_of_dim_j) - if (length(found_match) > 1) { + if (length(found_match) > 0) { stop("Arrays in 'data' have multiple unnamed dimensions of the ", "same length. Please provide dimension names.") - } else if (length(found_match) == 1) { - names(dim(data[[i]]))[j] <- names(unnamed_dims)[found_match] } else { new_dim <- len_of_dim_j names(new_dim) <- paste0('_unnamed_dim_', length(unnamed_dims) + @@ -211,7 +209,7 @@ Apply <- function(data, target_dims = NULL, fun, ..., output_dims = NULL, target_dims_names[[i]] <- target_dims[[i]] target_dims[[i]] <- targs2_new_num } - if (length(target_dims) == length(dim(data[[i]]))) { + if (length(target_dims[[i]]) == length(dim(data[[i]]))) { margins_names[i] <- list(NULL) margins[i] <- list(NULL) target_dims_names[[i]] <- names(dim(data[[i]])) @@ -407,8 +405,6 @@ Apply <- function(data, target_dims = NULL, fun, ..., output_dims = NULL, # - for loop with profiling of 50 sub-iterations (5000 sub-iterations): 1.5 s # - one sub-iteration: 0.0003 s # - intro: 0.000125 s - # - first half: 0.00005 s (computation of index) - # - second half: 0.00007 s (resolution of index and data access) # - splatted_f: 0.000125 s # - outro: 0.00005 iteration <- function(m) { @@ -417,60 +413,43 @@ Apply <- function(data, target_dims = NULL, fun, ..., output_dims = NULL, first_index <- n + (m - 1) * chunk_size first_marg_indices <- arrayInd(first_index, mad) names(first_marg_indices) <- names(mad) - n <- chunk_sizes[m] - last_index <- n + (m - 1) * chunk_size - last_marg_indices <- arrayInd(last_index, mad) - names(last_marg_indices) <- names(mad) - - first_data_margin_dim_index_to_take <- list() - input <- list() - for (i in 1:length(data)) { - if (length(margins[[i]]) > 0) { - input_first_margin_dim_index <- first_marg_indices[margins_names[[i]]] - input_first_margin_dim_index <- 1 + sum((input_first_margin_dim_index - 1) * - input_margin_weights[[i]]) - input_last_margin_dim_index <- last_marg_indices[margins_names[[i]]] - input_last_margin_dim_index <- 1 + sum((input_last_margin_dim_index - 1) * - input_margin_weights[[i]]) - } else { - input_first_margin_dim_index <- 1 - input_last_margin_dim_index <- 1 - } - first_data_margin_dim_index_to_take[[i]] <- input_first_margin_dim_index - input_indices_to_take <- as.list(rep(TRUE, length(dim(data[[i]])))) - input_indices_to_take[[length(dim(data[[i]]))]] <- - input_first_margin_dim_index : input_last_margin_dim_index - input[[i]] <- do.call('[', c(list(x = data[[i]]), input_indices_to_take, - list(drop = FALSE))) - } - sub_arrays_of_results <- list() found_first_sub_result <- FALSE iteration_indices_to_take <- list() - for (i in 1:length(input)) { - iteration_indices_to_take[[i]] <- as.list(rep(TRUE, length(dim(input[[i]])))) + for (i in 1:length(data)) { + iteration_indices_to_take[[i]] <- as.list(rep(TRUE, length(dim(data[[i]])))) + } + + add_one_multidim <- function(index, dims) { + stop_iterating <- FALSE + check_dim <- 1 + ndims <- length(index) + while (!stop_iterating) { + index[check_dim] <- index[check_dim] + 1 + if (index[check_dim] > dims[check_dim]) { + index[check_dim] <- 1 + check_dim <- check_dim + 1 + if (check_dim > ndims) { + check_dim <- rep(1, ndims) + stop_iterating <- TRUE + } + } else { + stop_iterating <- TRUE + } + } + index } # FOR LOOP for (n in 1:chunk_sizes[m]) { # SUB-ITERATION INTRO - # SUB-INTRO FIRST HALF - iteration_index <- (m - 1) * chunk_size + n - iteration_margin_indices <- arrayInd(iteration_index, mad) - names(iteration_margin_indices) <- names(mad) iteration_input <- list() - iteration_index <- (m - 1) * chunk_size + n - iteration_margin_indices <- arrayInd(iteration_index, mad) - names(iteration_margin_indices) <- names(mad) - iteration_input <- list() - # SUB-IT. INTRO SECOND HALF - for (i in 1:length(input)) { - input_margin_dim_index <- iteration_margin_indices[margins_names[[i]]] + for (i in 1:length(data)) { + input_margin_dim_index <- first_marg_indices[margins_names[[i]]] input_margin_dim_index <- 1 + sum((input_margin_dim_index - 1) * input_margin_weights[[i]]) - iteration_indices_to_take[[i]][[length(dim(data[[i]]))]] <- - input_margin_dim_index - first_data_margin_dim_index_to_take[[i]] + 1 - iteration_input[[i]] <- do.call('[', c(list(x = input[[i]]), + iteration_indices_to_take[[i]][[length(dim(data[[i]]))]] <- input_margin_dim_index + iteration_input[[i]] <- do.call('[', c(list(x = data[[i]]), iteration_indices_to_take[[i]], list(drop = FALSE))) num_dims <- length(dim(iteration_input[[i]])) @@ -480,6 +459,9 @@ Apply <- function(data, target_dims = NULL, fun, ..., output_dims = NULL, dim(iteration_input[[i]]) <- NULL } } + if (!is.null(mad)) { + first_marg_indices <- add_one_multidim(first_marg_indices, mad) + } # SPLATTED_F result <- splatted_f(iteration_input, ...) @@ -557,7 +539,7 @@ Apply <- function(data, target_dims = NULL, fun, ..., output_dims = NULL, # Merge the results arrays_of_results <- NULL found_first_result <- FALSE - chunk_length <- NULL + result_chunk_lengths <- vector('list', length(result[[1]])) fun_out_dims <- vector('list', length(result[[1]])) for (m in 1:length(result)) { if (!found_first_result) { @@ -577,6 +559,7 @@ Apply <- function(data, target_dims = NULL, fun, ..., output_dims = NULL, for (component in 1:length(result[[m]])) { component_dims <- dim(result[[m]][[component]]) if (!found_first_result) { + result_chunk_lengths[[component]] <- prod(component_dims) if (length(component_dims) > 1) { fun_out_dims[[component]] <- component_dims[- length(component_dims)] } @@ -592,10 +575,9 @@ Apply <- function(data, target_dims = NULL, fun, ..., output_dims = NULL, names(dim(arrays_of_results[[component]])) <- NULL } } - chunk_length <- prod(component_dims) } arrays_of_results[[component]][(1:prod(component_dims)) + - (m - 1) * chunk_length] <- result[[m]][[component]] + (m - 1) * result_chunk_lengths[[component]]] <- result[[m]][[component]] } if (!found_first_result) { found_first_result <- TRUE diff --git a/tests/testthat/test-sanity-checks.R b/tests/testthat/test-sanity-checks.R index d1ff9fd..77fdb95 100644 --- a/tests/testthat/test-sanity-checks.R +++ b/tests/testthat/test-sanity-checks.R @@ -2,15 +2,15 @@ context("Sanity checks") test_that("required arguments are provided", { expect_error( - Apply(), + Apply(), "missing, with no default" ) expect_error( - Apply(1:10), + Apply(1:10), "missing, with no default" ) expect_error( - Apply(1:10, fun = mean), + Apply(1:10, fun = mean), "must be specified" ) }) @@ -37,739 +37,3 @@ test_that("arguments have the right type", { # throws_error("") # ) #}) - - -context("Use cases") - -test_that("in1: 1 val; targ. dims: 0; out1: 0 val", { - f <- function(x) NULL - expect_equal( - Apply(1, NULL, f), - list(output1 = array(logical(0), dim = c(0, 1))) - ) - f <- function(x) numeric(0) - expect_equal( - Apply(1, NULL, f), - list(output1 = array(logical(0), dim = c(0, 1))) - ) -}) - -test_that("in1: 1 val; targ. dims: 0; out1: 0 val", { - f <- function(x) NULL - expect_equal( - Apply(1, 1, f), - list(output1 = array(logical(0))) - ) - f <- function(x) numeric(0) - expect_equal( - Apply(1, 1, f), - list(output1 = array(logical(0))) - ) -}) - -test_that("in1: 1 val; targ. dims: 0; out1: 1 val", { - expect_equal( - Apply(1, NULL, mean), - list(output1 = array(1)) - ) -}) - -test_that("in1: 1 val; targ. dims: 1; out1: 1 val", { - expect_equal( - Apply(1, 1, mean), - list(output1 = 1) - ) -}) - -test_that("in1: 1 val; targ. dims: 0; out1: 3 val", { - f <- function(x) x:(x + 2) - expect_equal( - Apply(1, NULL, f), - list(output1 = array(1:3, dim = c(3, 1))) - ) -}) - -test_that("in1: 1 val; targ. dims: 1; out1: 3 val", { - f <- function(x) x:(x + 2) - expect_equal( - Apply(1, 1, f), - list(output1 = array(1:3)) - ) -}) - -test_that("in1: 1 val; targ. dims: 0; out1: 1 dim", { - # unnamed output dim - f <- function(x) array(x:(x + 2)) - expect_equal( - Apply(1, NULL, f), - list(output1 = array(1:3, dim = c(3, 1))) - ) - # named output dim - f <- function(x) array(x:(x + 2), dim = c(time = 3)) - expect_equal( - Apply(1, NULL, f), - list(output1 = array(1:3, dim = c(time = 3, 1))) - ) -}) - -test_that("in1: 1 val; targ. dims: 1; out1: 1 dim", { - # unnamed output dim - f <- function(x) array(x:(x + 2)) - expect_equal( - Apply(1, 1, f), - list(output1 = array(1:3)) - ) - # named output dim - f <- function(x) array(x:(x + 2), dim = c(time = 3)) - expect_equal( - Apply(1, 1, f), - list(output1 = array(1:3, dim = c(time = 3))) - ) -}) - -test_that("in1: 1 val; targ. dims: 0; out1: 2 dim", { - # unnamed output dims - f <- function(x) array(x:(x + 2), dim = c(3, 4)) - expect_equal( - Apply(1, NULL, f), - list(output1 = array(1:3, dim = c(3, 4, 1))) - ) - # named output dim 1 - f <- function(x) array(x:(x + 2), dim = c(time = 3, 4)) - expect_equal( - Apply(1, NULL, f), - list(output1 = array(1:3, dim = c(time = 3, 4, 1))) - ) - # named output dim 2 - f <- function(x) array(x:(x + 2), dim = c(3, region = 4)) - expect_equal( - Apply(1, NULL, f), - list(output1 = array(1:3, dim = c(3, region = 4, 1))) - ) - # named output dims - f <- function(x) array(x:(x + 2), dim = c(time = 3, region = 4)) - expect_equal( - Apply(1, NULL, f), - list(output1 = array(1:3, dim = c(time = 3, region = 4, 1))) - ) -}) - -test_that("in1: 1 val; targ. dims: 1; out1: 2 dim", { - # unnamed output dims - f <- function(x) array(x:(x + 2), dim = c(3, 4)) - expect_equal( - Apply(1, 1, f), - list(output1 = array(1:3, dim = c(3, 4))) - ) - # named output dims - f <- function(x) array(x:(x + 2), dim = c(time = 3, region = 4)) - expect_equal( - Apply(1, 1, f), - list(output1 = array(1:3, dim = c(time = 3, region = 4))) - ) -}) - -test_that("in1: 1 val; targ. dims: 0; out1: 1 val; out 2: 1 val", { - # unnamed outputs - f <- function(x) list(mean(x), mean(x) + 1) - expect_equal( - Apply(1, NULL, f), - list(output1 = array(1), - output2 = array(2)) - ) - # named outputs - f <- function(x) list(a = mean(x), b = mean(x) + 1) - expect_equal( - Apply(1, NULL, f), - list(a = array(1), - b = array(2)) - ) -}) - -test_that("in1: 1 val; targ. dims: 1; out1: 3 val; out 2: 2 dim", { - f <- function(x) list(a = x:(x + 2), array(x:(x + 2), dim = c(3, region = 4))) - expect_equal( - Apply(1, 1, f), - list(a = array(1:3), - output2 = array(1:3, dim = c(3, region = 4))) - ) -}) - -test_that("in1: 1 dim; targ. dims: 0; out1: 0 val", { - # unnamed dim - # unnamed output - f <- function(x) NULL - expect_equal( - Apply(array(1:10), NULL, f), - list(output1 = array(logical(0), dim = c(0, 10))) - ) - # named dim - # named output - f <- function(x) list(out1 = NULL) - expect_equal( - Apply(array(1:10, dim = c(a = 10)), NULL, f), - list(out1 = array(logical(0), dim = c(0, a = 10))) - ) -}) - -test_that("in1: 1 dim; targ. dims: 1; out1: 0 val", { - # unnamed dim - # unnamed output - f <- function(x) NULL - expect_equal( - Apply(array(1:10), 1, f), - list(output1 = array(logical(0))) - ) - # named dim - # named output - f <- function(x) list(out1 = NULL) - expect_equal( - Apply(array(1:10, dim = c(a = 10)), 1, f), - list(out1 = array(logical(0), dim = c(0))) - ) - # named target dim - expect_equal( - Apply(array(1:10, dim = c(a = 10)), 'a', f), - list(out1 = array(logical(0), dim = c(0))) - ) -}) - -test_that("in1: 1 dim; targ. dims: 0; out1: 1 val", { - # unnamed dim - # unnamed output - expect_equal( - Apply(array(1:10), NULL, mean), - list(output1 = array(1:10)) - ) - # named dim - # named output - f <- function(x) list(out1 = mean(x)) - expect_equal( - Apply(array(1:10, dim = c(a = 10)), NULL, f), - list(out1 = array(1:10, dim = c(a = 10))) - ) -}) - -test_that("in1: 1 dim; targ. dims: 1; out1: 1 val", { - # unnamed dim - # unnamed output - expect_equal( - Apply(array(1:10), 1, mean), - list(output1 = 5.5) - ) - # named dim - # named output - f <- function(x) list(out1 = mean(x)) - expect_equal( - Apply(array(1:10, dim = c(a = 10)), 1, f), - list(out1 = 5.5) - ) - # named target dim - expect_equal( - Apply(array(1:10, dim = c(a = 10)), 'a', f), - list(out1 = 5.5) - ) -}) - -test_that("in1: 1 dim; targ. dims: 0; out1: 3 vals", { - # unnamed dim - # unnamed output - f <- function(x) x:(x + 2) - expect_equal( - Apply(array(1:10), NULL, f), - list(output1 = array(sapply(1:10, function(x) x:(x + 2)), dim = c(3, 10))) - ) - # named dim - # named output - f <- function(x) list(out1 = x:(x + 2)) - expect_equal( - Apply(array(1:10, dim = c(a = 10)), NULL, f), - list(out1 = array(sapply(1:10, function(x) x:(x + 2)), dim = c(3, a = 10))) - ) -}) - -test_that("in1: 1 dim; targ. dims: 1; out1: 3 vals", { - # unnamed dim - # unnamed output - f <- function(x) x[1]:(x[1] + 2) - expect_equal( - Apply(array(1:10), 1, f), - list(output1 = array(1:3)) - ) - # named dim - # named output - f <- function(x) list(out1 = x[1]:(x[1] + 2)) - expect_equal( - Apply(array(1:10, dim = c(a = 10)), 1, f), - list(out1 = array(1:3)) - ) - # named target dim - expect_equal( - Apply(array(1:10, dim = c(a = 10)), 'a', f), - list(out1 = array(1:3)) - ) -}) - -test_that("in1: 1 dim; targ. dims: 0; out1: 1 dim", { - # unnamed input dim - # unnamed output - # unnamed output dim - f <- function(x) array(x:(x + 2)) - expect_equal( - Apply(array(1:10), NULL, f), - list(output1 = array(sapply(1:10, function(x) x:(x + 2)), dim = c(3, 10))) - ) - # named input dim - # named output - # named output dim - f <- function(x) list(out1 = array(x:(x + 2), dim = c(b = 3))) - expect_equal( - Apply(array(1:10, dim = c(a = 10)), NULL, f), - list(out1 = array(sapply(1:10, function(x) x:(x + 2)), dim = c(b = 3, a = 10))) - ) -}) - -test_that("in1: 1 dim; targ. dims: 1; out1: 1 dim", { - # unnamed input dim - # unnamed output - # unnamed output dim - f <- function(x) array(x[1]:(x[1] + 2)) - expect_equal( - Apply(array(1:10), 1, f), - list(output1 = array(1:3)) - ) - # named input dim - # named output - # named output dim - f <- function(x) list(out1 = array(x[1]:(x[1] + 2), dim = c(b = 3))) - expect_equal( - Apply(array(1:10, dim = c(a = 10)), 1, f), - list(out1 = array(1:3, dim = c(b = 3))) - ) - # named target dim - expect_equal( - Apply(array(1:10, dim = c(a = 10)), 'a', f), - list(out1 = array(1:3, dim = c(b = 3))) - ) -}) - -test_that("in1: 1 dim; targ. dims: 0; out1: 2 dims", { - # unnamed input dim - # unnamed output - # unnamed output dims - f <- function(x) array(x:(x + 2), dim = c(3, 4)) - expect_equal( - Apply(array(1:10), NULL, f), - list(output1 = array(sapply(1:10, function(x) rep(x:(x + 2), 4)), dim = c(3, 4, 10))) - ) - # named input dim - # named output - # named output dim - f <- function(x) list(out1 = array(x:(x + 2), dim = c(b = 3, c = 4))) - expect_equal( - Apply(array(1:10, dim = c(a = 10)), NULL, f), - list(out1 = array(sapply(1:10, function(x) rep(x:(x + 2), 4)), dim = c(b = 3, c = 4, a = 10))) - ) -}) - -test_that("in1: 1 dim; targ. dims: 1; out1: 2 dims", { - # unnamed input dim - # unnamed output - # unnamed output dim - f <- function(x) array(x[1]:(x[1] + 2), dim = c(3, 4)) - expect_equal( - Apply(array(1:10), 1, f), - list(output1 = array(sapply(1:10, function(x) rep(x:(x + 2), 4)), dim = c(3, 4))) - ) - # named input dim - # named output - # named output dim - f <- function(x) list(out1 = array(x[1]:(x[1] + 2), dim = c(b = 3, c = 4))) - expect_equal( - Apply(array(1:10, dim = c(a = 10)), 1, f), - list(out1 = array(sapply(1:10, function(x) rep(x:(x + 2), 4)), dim = c(b = 3, c = 4))) - ) - # named target dim - expect_equal( - Apply(array(1:10, dim = c(a = 10)), 'a', f), - list(out1 = array(sapply(1:10, function(x) rep(x:(x + 2), 4)), dim = c(b = 3, c = 4))) - ) -}) - -# FAILS -test_that("in1: 1 dim; targ. dims: 0; out1: 1 dim; out2: 2 dims; out3: 1 val", { - f <- function(x) list(array(x:(x + 2), dim = c(3)), - array(x:(x + 3), dim = c(4, 5)), - mean(x)) - expect_equal( - Apply(array(1:10), NULL, f), - list(output1 = array(sapply(1:10, function(x) x:(x + 2)), dim = c(3, 10)), - output2 = array(sapply(1:10, function(x) rep(x:(x + 3), 5)), dim = c(4, 5, 10)), - output3 = array(1:10)) - ) -}) - -test_that("in1: 1 dim; targ. dims: 1; out1: 1 dim; out2: 2 dims; out3: 1 val", { -}) - -test_that("in1: 2 dim; targ. dims: 0; out1: 1 dim; out2: 2 dims; out3: 1 val", { -}) - -test_that("in1: 2 dim; targ. dims: 1; out1: 1 dim; out2: 2 dims; out3: 1 val", { -# first target dim -## without name -## with name -# second target dim -## without name -## with name -}) - -test_that("in1: 2 dim; targ. dims: 2; out1: 1 dim; out2: 2 dims; out3: 1 val", { -# without name -# with name -}) - -test_that("in1: 3 dim; targ. dims: 0; out1: 1 dim; out2: 2 dims; out3: 1 val", { -}) - -test_that("in1: 3 dim; targ. dims: 1; out1: 1 dim; out2: 2 dims; out3: 1 val", { -# first target dim -## without name -## with name -# second target dim -## without name -## with name -# third target dim -## without name -## with name -}) - -test_that("in1: 3 dim; targ. dims: 2; out1: 1 dim; out2: 2 dims; out3: 1 val", { -# first couple -## without name -## with name -# second couple -## without name -## with name -# extremes couple -## without name -## with name -}) - -test_that("in1: 1 val; in2: 1 val; targ. dims: 0, 0; out1: 0 val", { -}) - -test_that("in1: 1 val; in2: 1 val; targ. dims: 1, 0; out1: 0 val", { -}) - -test_that("in1: 1 val; in2: 1 val; targ. dims: 0, 1; out1: 0 val", { -}) - -test_that("in1: 1 val; in2: 1 val; targ. dims: 1, 1; out1: 0 val", { -}) - -test_that("in1: 1 val; in2: 1 val; targ. dims: 0, 0; out1: 1 val", { -}) - -test_that("in1: 1 val; in2: 1 val; targ. dims: 1, 0; out1: 1 val", { -}) - -test_that("in1: 1 val; in2: 1 val; targ. dims: 0, 1; out1: 1 val", { -}) - -test_that("in1: 1 val; in2: 1 val; targ. dims: 1, 1; out1: 1 val", { -}) - -test_that("in1: 1 val; in2: 1 val; targ. dims: 0, 0; out1: 3 val", { -}) - -test_that("in1: 1 val; in2: 1 val; targ. dims: 1, 0; out1: 3 val", { -}) - -test_that("in1: 1 val; in2: 1 val; targ. dims: 0, 1; out1: 3 val", { -}) - -test_that("in1: 1 val; in2: 1 val; targ. dims: 1, 1; out1: 3 val", { -}) - -test_that("in1: 1 val; in2: 1 val; targ. dims: 0, 0; out1: 1 dim", { -}) - -test_that("in1: 1 val; in2: 1 val; targ. dims: 1, 0; out1: 1 dim", { -}) - -test_that("in1: 1 val; in2: 1 val; targ. dims: 0, 1; out1: 1 dim", { -}) - -test_that("in1: 1 val; in2: 1 val; targ. dims: 1, 1; out1: 1 dim", { -}) - -test_that("in1: 1 val; in2: 1 val; targ. dims: 0, 0; out1: 1 dim; out2: 1 val; out3: 3 dim", { -}) - -test_that("in1: 1 val; in2: 1 val; targ. dims: 1, 0; out1: 1 dim; out2: 1 val; out3: 3 dim", { -}) - -test_that("in1: 1 val; in2: 1 val; targ. dims: 0, 1; out1: 1 dim; out2: 1 val; out3: 3 dim", { -}) - -test_that("in1: 1 val; in2: 1 val; targ. dims: 1, 1; out1: 1 dim; out2: 1 val; out3: 3 dim", { -}) - - - - - - - - - - -test_that("in1: 1 val; in2: 1 dim; targ. dims: 0, 0; out1: 0 val", { -}) - -test_that("in1: 1 val; in2: 1 dim; targ. dims: 1, 0; out1: 0 val", { -}) - -test_that("in1: 1 val; in2: 1 dim; targ. dims: 0, 1; out1: 0 val", { -}) - -test_that("in1: 1 val; in2: 1 dim; targ. dims: 1, 1; out1: 0 val", { -}) - -test_that("in1: 1 val; in2: 1 dim; targ. dims: 0, 0; out1: 1 val", { -}) - -test_that("in1: 1 val; in2: 1 dim; targ. dims: 1, 0; out1: 1 val", { -}) - -test_that("in1: 1 val; in2: 1 dim; targ. dims: 0, 1; out1: 1 val", { -}) - -test_that("in1: 1 val; in2: 1 dim; targ. dims: 1, 1; out1: 1 val", { -}) - -test_that("in1: 1 val; in2: 1 dim; targ. dims: 0, 0; out1: 3 val", { -}) - -test_that("in1: 1 val; in2: 1 dim; targ. dims: 1, 0; out1: 3 val", { -}) - -test_that("in1: 1 val; in2: 1 dim; targ. dims: 0, 1; out1: 3 val", { -}) - -test_that("in1: 1 val; in2: 1 dim; targ. dims: 1, 1; out1: 3 val", { -}) - -test_that("in1: 1 val; in2: 1 dim; targ. dims: 0, 0; out1: 1 dim", { -}) - -test_that("in1: 1 val; in2: 1 dim; targ. dims: 1, 0; out1: 1 dim", { -}) - -test_that("in1: 1 val; in2: 1 dim; targ. dims: 0, 1; out1: 1 dim", { -}) - -test_that("in1: 1 val; in2: 1 dim; targ. dims: 1, 1; out1: 1 dim", { -}) - -test_that("in1: 1 val; in2: 1 dim; targ. dims: 0, 0; out1: 1 dim; out2: 1 val; out3: 3 dim", { -}) - -test_that("in1: 1 val; in2: 1 dim; targ. dims: 1, 0; out1: 1 dim; out2: 1 val; out3: 3 dim", { -}) - -test_that("in1: 1 val; in2: 1 dim; targ. dims: 0, 1; out1: 1 dim; out2: 1 val; out3: 3 dim", { -}) - -test_that("in1: 1 val; in2: 1 dim; targ. dims: 1, 1; out1: 1 dim; out2: 1 val; out3: 3 dim", { -}) - - - - - - - - - - -test_that("in1: 2 dim; in2: 1 dim; targ. dims: 0, 0; out1: 0 val", { -}) - -test_that("in1: 2 dim; in2: 1 dim; targ. dims: 1, 0; out1: 0 val", { -#first in first dim -#first in second dim -}) - -test_that("in1: 2 dim; in2: 1 dim; targ. dims: 0, 1; out1: 0 val", { -}) - -test_that("in1: 2 dim; in2: 1 dim; targ. dims: 1, 1; out1: 0 val", { -#shared target dims -##first in first dim -##first in second dim -#not shared target dims -##first in first dim -##first in second dim -}) - -test_that("in1: 2 dim; in2: 1 dim; targ. dims: 2, 1; out1: 0 val", { -# shared first target dim -# shared second target dim -# not shared target dims -}) - -test_that("in1: 2 dim; in2: 1 dim; targ. dims: 0, 0; out1: 1 val", { -}) - -test_that("in1: 2 dim; in2: 1 dim; targ. dims: 1, 0; out1: 1 val", { -#first in first dim -#first in second dim -}) - -test_that("in1: 2 dim; in2: 1 dim; targ. dims: 0, 1; out1: 1 val", { -}) - -test_that("in1: 2 dim; in2: 1 dim; targ. dims: 1, 1; out1: 1 val", { -#shared target dims -##first in first dim -##first in second dim -#not shared target dims -##first in first dim -##first in second dim -}) - -test_that("in1: 2 dim; in2: 1 dim; targ. dims: 2, 1; out1: 1 val", { -# shared first target dim -# shared second target dim -# not shared target dims -}) - -test_that("in1: 2 dim; in2: 1 dim; targ. dims: 0, 0; out1: 3 val", { -}) - -test_that("in1: 2 dim; in2: 1 dim; targ. dims: 1, 0; out1: 3 val", { -#first in first dim -#first in second dim -}) - -test_that("in1: 2 dim; in2: 1 dim; targ. dims: 0, 1; out1: 3 val", { -}) - -test_that("in1: 2 dim; in2: 1 dim; targ. dims: 1, 1; out1: 3 val", { -#shared target dims -##first in first dim -##first in second dim -#not shared target dims -##first in first dim -##first in second dim -}) - -test_that("in1: 2 dim; in2: 1 dim; targ. dims: 2, 1; out1: 3 val", { -# shared first target dim -# shared second target dim -# not shared target dims -}) - -test_that("in1: 2 dim; in2: 1 dim; targ. dims: 0, 0; out1: 1 dim", { -}) - -test_that("in1: 2 dim; in2: 1 dim; targ. dims: 1, 0; out1: 1 dim", { -#first in first dim -#first in second dim -}) - -test_that("in1: 2 dim; in2: 1 dim; targ. dims: 0, 1; out1: 1 dim", { -}) - -test_that("in1: 2 dim; in2: 1 dim; targ. dims: 1, 1; out1: 1 dim", { -#shared target dims -##first in first dim -##first in second dim -#not shared target dims -##first in first dim -##first in second dim -}) - -test_that("in1: 2 dim; in2: 1 dim; targ. dims: 2, 1; out1: 1 dim", { -# shared first target dim -# shared second target dim -# not shared target dims -}) - -test_that("in1: 2 dim; in2: 1 dim; targ. dims: 0, 0; out1: 1 dim; out2: 1 val; out3: 3 dim", { -}) - -test_that("in1: 2 dim; in2: 1 dim; targ. dims: 1, 0; out1: 1 dim; out2: 1 val; out3: 3 dim", { -#first in first dim -#first in second dim -}) - -test_that("in1: 2 dim; in2: 1 dim; targ. dims: 0, 1; out1: 1 dim; out2: 1 val; out3: 3 dim", { -}) - -test_that("in1: 2 dim; in2: 1 dim; targ. dims: 1, 1; out1: 1 dim; out2: 1 val; out3: 3 dim", { -#shared target dims -##first in first dim -##first in second dim -#not shared target dims -##first in first dim -##first in second dim -}) - -test_that("in1: 2 dim; in2: 1 dim; targ. dims: 2, 1; out1: 1 dim; out2: 1 val; out3: 3 dim", { -# shared first target dim -# shared second target dim -# not shared target dims -}) - - - - - - - - - - - - -test_that("in1: 2 dim; in2: 3 dim; targ. dims: 0-2, 0-3; out1: 2 dim", { -# shared first target dim -# shared second target dim -# shared two target dims (first two in second in) -# shared two target dims (last two in second in) -# shared two target dims (extreme two in second in) -# not shared target dims -}) - -test_that("in1: 2 dim; in2: 3 dim; targ. dims: 0-2, 0-3; out1: 1 dim; out2: 1 val; out3: 3 dim", { -# shared first target dim -# shared second target dim -# shared two target dims (first two in second in) -# shared two target dims (last two in second in) -# shared two target dims (extreme two in second in) -# not shared target dims -}) - -test_that("in1: 2 dim; in2: 3 dim; in3: 1 dim; targ. dims: 0-2, 0-3, 0-1; out1: 2 dim", { -# shared first target dim -# shared second target dim -# shared two target dims (first two in second in) -# shared two target dims (last two in second in) -# shared two target dims (extreme two in second in) -# not shared target dims -}) - - - -# TODOS: -# TESTS FOR MARGINS -# TESTS FOR DISORDERED TARGET_DIMS -# TESTS FOR FUN WITH TARGET_DIMS AND OUTPUT_DIMS ATTACHED -# TESTS FOR FUNCTIONS RECEIVING ADDITIONAL PARAMETERS -# TESTS FOR SPLIT FACTOR -# TESTS FOR NCORES -# TESTS OF WALLCLOCK TIME -# TESTS OF MEMORY FOOTPRINT diff --git a/tests/testthat/test-use-cases.R b/tests/testthat/test-use-cases.R new file mode 100644 index 0000000..a9a6498 --- /dev/null +++ b/tests/testthat/test-use-cases.R @@ -0,0 +1,1183 @@ +context("Use cases") + +test_that("in1: 1 val; targ. dims: 0; out1: 0 val", { + f <- function(x) NULL + expect_equal( + Apply(1, NULL, f), + list(output1 = array(logical(0), dim = c(0, 1))) + ) + f <- function(x) numeric(0) + expect_equal( + Apply(1, NULL, f), + list(output1 = array(logical(0), dim = c(0, 1))) + ) +}) + +test_that("in1: 1 val; targ. dims: 0; out1: 0 val", { + f <- function(x) NULL + expect_equal( + Apply(1, 1, f), + list(output1 = array(logical(0))) + ) + f <- function(x) numeric(0) + expect_equal( + Apply(1, 1, f), + list(output1 = array(logical(0))) + ) +}) + +test_that("in1: 1 val; targ. dims: 0; out1: 1 val", { + expect_equal( + Apply(1, NULL, mean), + list(output1 = array(1)) + ) +}) + +test_that("in1: 1 val; targ. dims: 1; out1: 1 val", { + expect_equal( + Apply(1, 1, mean), + list(output1 = 1) + ) +}) + +test_that("in1: 1 val; targ. dims: 0; out1: 3 val", { + f <- function(x) x:(x + 2) + expect_equal( + Apply(1, NULL, f), + list(output1 = array(1:3, dim = c(3, 1))) + ) +}) + +test_that("in1: 1 val; targ. dims: 1; out1: 3 val", { + f <- function(x) x:(x + 2) + expect_equal( + Apply(1, 1, f), + list(output1 = array(1:3)) + ) +}) + +test_that("in1: 1 val; targ. dims: 0; out1: 1 dim", { + # unnamed output dim + f <- function(x) array(x:(x + 2)) + expect_equal( + Apply(1, NULL, f), + list(output1 = array(1:3, dim = c(3, 1))) + ) + # named output dim + f <- function(x) array(x:(x + 2), dim = c(time = 3)) + expect_equal( + Apply(1, NULL, f), + list(output1 = array(1:3, dim = c(time = 3, 1))) + ) +}) + +test_that("in1: 1 val; targ. dims: 1; out1: 1 dim", { + # unnamed output dim + f <- function(x) array(x:(x + 2)) + expect_equal( + Apply(1, 1, f), + list(output1 = array(1:3)) + ) + # named output dim + f <- function(x) array(x:(x + 2), dim = c(time = 3)) + expect_equal( + Apply(1, 1, f), + list(output1 = array(1:3, dim = c(time = 3))) + ) +}) + +test_that("in1: 1 val; targ. dims: 0; out1: 2 dim", { + # unnamed output dims + f <- function(x) array(x:(x + 2), dim = c(3, 4)) + expect_equal( + Apply(1, NULL, f), + list(output1 = array(1:3, dim = c(3, 4, 1))) + ) + # named output dim 1 + f <- function(x) array(x:(x + 2), dim = c(time = 3, 4)) + expect_equal( + Apply(1, NULL, f), + list(output1 = array(1:3, dim = c(time = 3, 4, 1))) + ) + # named output dim 2 + f <- function(x) array(x:(x + 2), dim = c(3, region = 4)) + expect_equal( + Apply(1, NULL, f), + list(output1 = array(1:3, dim = c(3, region = 4, 1))) + ) + # named output dims + f <- function(x) array(x:(x + 2), dim = c(time = 3, region = 4)) + expect_equal( + Apply(1, NULL, f), + list(output1 = array(1:3, dim = c(time = 3, region = 4, 1))) + ) +}) + +test_that("in1: 1 val; targ. dims: 1; out1: 2 dim", { + # unnamed output dims + f <- function(x) array(x:(x + 2), dim = c(3, 4)) + expect_equal( + Apply(1, 1, f), + list(output1 = array(1:3, dim = c(3, 4))) + ) + # named output dims + f <- function(x) array(x:(x + 2), dim = c(time = 3, region = 4)) + expect_equal( + Apply(1, 1, f), + list(output1 = array(1:3, dim = c(time = 3, region = 4))) + ) +}) + +test_that("in1: 1 val; targ. dims: 0; out1: 1 val; out 2: 1 val", { + # unnamed outputs + f <- function(x) list(mean(x), mean(x) + 1) + expect_equal( + Apply(1, NULL, f), + list(output1 = array(1), + output2 = array(2)) + ) + # named outputs + f <- function(x) list(a = mean(x), b = mean(x) + 1) + expect_equal( + Apply(1, NULL, f), + list(a = array(1), + b = array(2)) + ) +}) + +test_that("in1: 1 val; targ. dims: 1; out1: 3 val; out 2: 2 dim", { + f <- function(x) list(a = x:(x + 2), array(x:(x + 2), dim = c(3, region = 4))) + expect_equal( + Apply(1, 1, f), + list(a = array(1:3), + output2 = array(1:3, dim = c(3, region = 4))) + ) +}) + +test_that("in1: 1 dim; targ. dims: 0; out1: 0 val", { + # unnamed dim + # unnamed output + f <- function(x) NULL + expect_equal( + Apply(array(1:10), NULL, f), + list(output1 = array(logical(0), dim = c(0, 10))) + ) + # named dim + # named output + f <- function(x) list(out1 = NULL) + expect_equal( + Apply(array(1:10, dim = c(a = 10)), NULL, f), + list(out1 = array(logical(0), dim = c(0, a = 10))) + ) +}) + +test_that("in1: 1 dim; targ. dims: 1; out1: 0 val", { + # unnamed dim + # unnamed output + f <- function(x) NULL + expect_equal( + Apply(array(1:10), 1, f), + list(output1 = array(logical(0))) + ) + # named dim + # named output + f <- function(x) list(out1 = NULL) + expect_equal( + Apply(array(1:10, dim = c(a = 10)), 1, f), + list(out1 = array(logical(0), dim = c(0))) + ) + # named target dim + expect_equal( + Apply(array(1:10, dim = c(a = 10)), 'a', f), + list(out1 = array(logical(0), dim = c(0))) + ) +}) + +test_that("in1: 1 dim; targ. dims: 0; out1: 1 val", { + # unnamed dim + # unnamed output + expect_equal( + Apply(array(1:10), NULL, mean), + list(output1 = array(1:10)) + ) + # named dim + # named output + f <- function(x) list(out1 = mean(x)) + expect_equal( + Apply(array(1:10, dim = c(a = 10)), NULL, f), + list(out1 = array(1:10, dim = c(a = 10))) + ) +}) + +test_that("in1: 1 dim; targ. dims: 1; out1: 1 val", { + # unnamed dim + # unnamed output + expect_equal( + Apply(array(1:10), 1, mean), + list(output1 = 5.5) + ) + # named dim + # named output + f <- function(x) list(out1 = mean(x)) + expect_equal( + Apply(array(1:10, dim = c(a = 10)), 1, f), + list(out1 = 5.5) + ) + # named target dim + expect_equal( + Apply(array(1:10, dim = c(a = 10)), 'a', f), + list(out1 = 5.5) + ) +}) + +test_that("in1: 1 dim; targ. dims: 0; out1: 3 vals", { + # unnamed dim + # unnamed output + f <- function(x) x:(x + 2) + expect_equal( + Apply(array(1:10), NULL, f), + list(output1 = array(sapply(1:10, function(x) x:(x + 2)), dim = c(3, 10))) + ) + # named dim + # named output + f <- function(x) list(out1 = x:(x + 2)) + expect_equal( + Apply(array(1:10, dim = c(a = 10)), NULL, f), + list(out1 = array(sapply(1:10, function(x) x:(x + 2)), dim = c(3, a = 10))) + ) +}) + +test_that("in1: 1 dim; targ. dims: 1; out1: 3 vals", { + # unnamed dim + # unnamed output + f <- function(x) x[1]:(x[1] + 2) + expect_equal( + Apply(array(1:10), 1, f), + list(output1 = array(1:3)) + ) + # named dim + # named output + f <- function(x) list(out1 = x[1]:(x[1] + 2)) + expect_equal( + Apply(array(1:10, dim = c(a = 10)), 1, f), + list(out1 = array(1:3)) + ) + # named target dim + expect_equal( + Apply(array(1:10, dim = c(a = 10)), 'a', f), + list(out1 = array(1:3)) + ) +}) + +test_that("in1: 1 dim; targ. dims: 0; out1: 1 dim", { + # unnamed input dim + # unnamed output + # unnamed output dim + f <- function(x) array(x:(x + 2)) + expect_equal( + Apply(array(1:10), NULL, f), + list(output1 = array(sapply(1:10, function(x) x:(x + 2)), dim = c(3, 10))) + ) + # named input dim + # named output + # named output dim + f <- function(x) list(out1 = array(x:(x + 2), dim = c(b = 3))) + expect_equal( + Apply(array(1:10, dim = c(a = 10)), NULL, f), + list(out1 = array(sapply(1:10, function(x) x:(x + 2)), dim = c(b = 3, a = 10))) + ) +}) + +test_that("in1: 1 dim; targ. dims: 1; out1: 1 dim", { + # unnamed input dim + # unnamed output + # unnamed output dim + f <- function(x) array(x[1]:(x[1] + 2)) + expect_equal( + Apply(array(1:10), 1, f), + list(output1 = array(1:3)) + ) + # named input dim + # named output + # named output dim + f <- function(x) list(out1 = array(x[1]:(x[1] + 2), dim = c(b = 3))) + expect_equal( + Apply(array(1:10, dim = c(a = 10)), 1, f), + list(out1 = array(1:3, dim = c(b = 3))) + ) + # named target dim + expect_equal( + Apply(array(1:10, dim = c(a = 10)), 'a', f), + list(out1 = array(1:3, dim = c(b = 3))) + ) +}) + +test_that("in1: 1 dim; targ. dims: 0; out1: 2 dims", { + # unnamed input dim + # unnamed output + # unnamed output dims + f <- function(x) array(x:(x + 2), dim = c(3, 4)) + expect_equal( + Apply(array(1:10), NULL, f), + list(output1 = array(sapply(1:10, function(x) rep(x:(x + 2), 4)), dim = c(3, 4, 10))) + ) + # named input dim + # named output + # named output dim + f <- function(x) list(out1 = array(x:(x + 2), dim = c(b = 3, c = 4))) + expect_equal( + Apply(array(1:10, dim = c(a = 10)), NULL, f), + list(out1 = array(sapply(1:10, function(x) rep(x:(x + 2), 4)), dim = c(b = 3, c = 4, a = 10))) + ) +}) + +test_that("in1: 1 dim; targ. dims: 1; out1: 2 dims", { + # unnamed input dim + # unnamed output + # unnamed output dim + f <- function(x) array(x[1]:(x[1] + 2), dim = c(3, 4)) + expect_equal( + Apply(array(1:10), 1, f), + list(output1 = array(sapply(1:10, function(x) rep(x:(x + 2), 4)), dim = c(3, 4))) + ) + # named input dim + # named output + # named output dim + f <- function(x) list(out1 = array(x[1]:(x[1] + 2), dim = c(b = 3, c = 4))) + expect_equal( + Apply(array(1:10, dim = c(a = 10)), 1, f), + list(out1 = array(sapply(1:10, function(x) rep(x:(x + 2), 4)), dim = c(b = 3, c = 4))) + ) + # named target dim + expect_equal( + Apply(array(1:10, dim = c(a = 10)), 'a', f), + list(out1 = array(sapply(1:10, function(x) rep(x:(x + 2), 4)), dim = c(b = 3, c = 4))) + ) +}) + +test_that("in1: 1 dim; targ. dims: 0; out1: 1 dim; out2: 2 dims; out3: 1 val", { + # unnamed input dim + # unnamed output + # unnamed output dim + f <- function(x) list(array(x:(x + 2), dim = c(3)), + array(x:(x + 3), dim = c(4, 5)), + mean(x)) + expect_equal( + Apply(array(1:10), NULL, f), + list(output1 = array(sapply(1:10, function(x) x:(x + 2)), dim = c(3, 10)), + output2 = array(sapply(1:10, function(x) rep(x:(x + 3), 5)), dim = c(4, 5, 10)), + output3 = array(1:10)) + ) + # named input dim + # named output + # named output dim + f <- function(x) list(a = array(x:(x + 2), dim = c(b = 3)), + b = array(x:(x + 3), dim = c(c = 4, d = 5)), + c = mean(x)) + expect_equal( + Apply(array(1:10, dim = c(a = 10)), NULL, f), + list(a = array(sapply(1:10, function(x) x:(x + 2)), dim = c(b = 3, a = 10)), + b = array(sapply(1:10, function(x) rep(x:(x + 3), 5)), + dim = c(c = 4, d = 5, a = 10)), + c = array(1:10, dim = c(a = 10))) + ) +}) + +test_that("in1: 1 dim; targ. dims: 1; out1: 1 dim; out2: 2 dims; out3: 1 val", { + # unnamed input dim + # unnamed output + # unnamed output dim + f <- function(x) list(array(x[1]:(x[1] + 2), dim = c(3)), + array(x[1]:(x[1] + 3), dim = c(4, 5)), + mean(x)) + expect_equal( + Apply(array(1:10), 1, f), + list(output1 = array(sapply(1, function(x) x:(x + 2)), dim = c(3)), + output2 = array(sapply(1, function(x) rep(x:(x + 3), 5)), dim = c(4, 5)), + output3 = 5.5) + ) + # named input dim + # named output + # named output dim + f <- function(x) list(a = array(x[1]:(x[1] + 2), dim = c(b = 3)), + b = array(x[1]:(x[1] + 3), dim = c(c = 4, d = 5)), + c = mean(x)) + expect_equal( + Apply(array(1:10, dim = c(a = 10)), 1, f), + list(a = array(sapply(1, function(x) x:(x + 2)), dim = c(b = 3)), + b = array(sapply(1, function(x) rep(x:(x + 3), 5)), dim = c(c = 4, d = 5)), + c = 5.5) + ) +}) + +test_that("in1: 2 dim; targ. dims: 0-2; out1: 1 dim; out2: 2 dims; out3: 1 val", { +# no target dim + # unnamed input dim + # unnamed output + # unnamed output dim + f <- function(x) list(array(x[1]:(x[1] + 2), dim = c(3)), + array(x[1]:(x[1] + 3), dim = c(4, 5)), + mean(x)) + expect_equal( + Apply(array(1:10, dim = c(10, 3)), NULL, f), + list(output1 = array(rep(sapply(1:10, function(x) x:(x + 2)), 3), dim = c(3, 10, 3)), + output2 = array(rep(sapply(1:10, function(x) rep(x:(x + 3), 5)), 3), + dim = c(4, 5, 10, 3)), + output3 = array(1:10, dim = c(10, 3))) + ) + # named input dim + # named output + # named output dim + f <- function(x) list(a = array(x[1]:(x[1] + 2), dim = c(c = 3)), + b = array(x[1]:(x[1] + 3), dim = c(d = 4, e = 5)), + c = mean(x)) + expect_equal( + Apply(array(1:10, dim = c(a = 10, b = 3)), NULL, f), + list(a = array(rep(sapply(1:10, function(x) x:(x + 2)), 3), + dim = c(c = 3, a = 10, b = 3)), + b = array(rep(sapply(1:10, function(x) rep(x:(x + 3), 5)), 3), + dim = c(d = 4, e = 5, a = 10, b = 3)), + c = array(1:10, dim = c(a = 10, b = 3))) + ) +# first target dim + # unnamed input dim + # unnamed output + # unnamed output dim + f <- function(x) list(array(x[1]:(x[1] + 2), dim = c(3)), + array(x[1]:(x[1] + 3), dim = c(4, 5)), + mean(x)) + expect_equal( + Apply(array(1:10, dim = c(10, 3)), 1, f), + list(output1 = array(rep(sapply(1, function(x) x:(x + 2)), 3), dim = c(3, 3)), + output2 = array(rep(sapply(1, function(x) rep(x:(x + 3), 5)), 3), + dim = c(4, 5, 3)), + output3 = array(5.5, dim = c(3))) + ) + # named input dim + # named output + # named output dim + f <- function(x) list(a = array(x[1]:(x[1] + 2), dim = c(c = 3)), + b = array(x[1]:(x[1] + 3), dim = c(d = 4, e = 5)), + c = mean(x)) + expect_equal( + Apply(array(1:10, dim = c(a = 10, b = 3)), 'a', f), + list(a = array(rep(sapply(1, function(x) x:(x + 2)), 3), dim = c(c = 3, b = 3)), + b = array(rep(sapply(1, function(x) rep(x:(x + 3), 5)), 3), + dim = c(d = 4, e = 5, b = 3)), + c = array(5.5, dim = c(b = 3))) + ) +# second target dim + # unnamed input dim + # unnamed output + # unnamed output dim + f <- function(x) list(array(x[1]:(x[1] + 2), dim = c(3)), + array(x[1]:(x[1] + 3), dim = c(4, 5)), + mean(x)) + expect_equal( + Apply(array(1:10, dim = c(10, 3)), 2, f), + list(output1 = array(sapply(1:10, function(x) x:(x + 2)), dim = c(3, 10)), + output2 = array(sapply(1:10, function(x) rep(x:(x + 3), 5)), + dim = c(4, 5, 10)), + output3 = array(1:10, dim = c(10))) + ) + # named input dim + # named output + # named output dim + f <- function(x) list(a = array(x[1]:(x[1] + 2), dim = c(c = 3)), + b = array(x[1]:(x[1] + 3), dim = c(d = 4, e = 5)), + c = mean(x)) + expect_equal( + Apply(array(1:10, dim = c(a = 10, b = 3)), 'b', f), + list(a = array(sapply(1:10, function(x) x:(x + 2)), + dim = c(c = 3, a = 10)), + b = array(sapply(1:10, function(x) rep(x:(x + 3), 5)), + dim = c(d = 4, e = 5, a = 10)), + c = array(1:10, dim = c(a = 10))) + ) +# two target dims + # unnamed input dim + # unnamed output + # unnamed output dim + f <- function(x) list(array(x[1]:(x[1] + 2), dim = c(3)), + array(x[1]:(x[1] + 3), dim = c(4, 5)), + mean(x)) + expect_equal( + Apply(array(1:10, dim = c(10, 3)), c(1, 2), f), + list(output1 = array(sapply(1, function(x) x:(x + 2)), dim = c(3)), + output2 = array(sapply(1, function(x) rep(x:(x + 3), 5)), + dim = c(4, 5)), + output3 = 5.5) + ) + # named input dim + # named output + # named output dim + f <- function(x) list(a = array(x[1]:(x[1] + 2), dim = c(c = 3)), + b = array(x[1]:(x[1] + 3), dim = c(d = 4, e = 5)), + c = mean(x)) + expect_equal( + Apply(array(1:10, dim = c(a = 10, b = 3)), c('a', 'b'), f), + list(a = array(sapply(1, function(x) x:(x + 2)), dim = c(c = 3)), + b = array(sapply(1, function(x) rep(x:(x + 3), 5)), + dim = c(d = 4, e = 5)), + c = 5.5) + ) +}) + + + + +#test_that("in1: 3 dim; targ. dims: 0-3; out1: 1 dim; out2: 2 dims; out3: 1 val", { +#}) + + + + +#test_that("in1: 1 val; in2: 1 val; targ. dims: 0, 0; out1: 0 val", { +#}) +# +#test_that("in1: 1 val; in2: 1 val; targ. dims: 1, 0; out1: 0 val", { +#}) +# +#test_that("in1: 1 val; in2: 1 val; targ. dims: 0, 1; out1: 0 val", { +#}) +# +#test_that("in1: 1 val; in2: 1 val; targ. dims: 1, 1; out1: 0 val", { +#}) +# +#test_that("in1: 1 val; in2: 1 val; targ. dims: 0, 0; out1: 1 val", { +#}) +# +#test_that("in1: 1 val; in2: 1 val; targ. dims: 1, 0; out1: 1 val", { +#}) +# +#test_that("in1: 1 val; in2: 1 val; targ. dims: 0, 1; out1: 1 val", { +#}) +# +#test_that("in1: 1 val; in2: 1 val; targ. dims: 1, 1; out1: 1 val", { +#}) +# +#test_that("in1: 1 val; in2: 1 val; targ. dims: 0, 0; out1: 3 val", { +#}) +# +#test_that("in1: 1 val; in2: 1 val; targ. dims: 1, 0; out1: 3 val", { +#}) +# +#test_that("in1: 1 val; in2: 1 val; targ. dims: 0, 1; out1: 3 val", { +#}) +# +#test_that("in1: 1 val; in2: 1 val; targ. dims: 1, 1; out1: 3 val", { +#}) +# +#test_that("in1: 1 val; in2: 1 val; targ. dims: 0, 0; out1: 1 dim", { +#}) +# +#test_that("in1: 1 val; in2: 1 val; targ. dims: 1, 0; out1: 1 dim", { +#}) +# +#test_that("in1: 1 val; in2: 1 val; targ. dims: 0, 1; out1: 1 dim", { +#}) +# +#test_that("in1: 1 val; in2: 1 val; targ. dims: 1, 1; out1: 1 dim", { +#}) +# +#test_that("in1: 1 val; in2: 1 val; targ. dims: 0, 0; out1: 1 dim; out2: 1 val; out3: 3 dim", { +#}) +# +#test_that("in1: 1 val; in2: 1 val; targ. dims: 1, 0; out1: 1 dim; out2: 1 val; out3: 3 dim", { +#}) +# +#test_that("in1: 1 val; in2: 1 val; targ. dims: 0, 1; out1: 1 dim; out2: 1 val; out3: 3 dim", { +#}) +# +#test_that("in1: 1 val; in2: 1 val; targ. dims: 1, 1; out1: 1 dim; out2: 1 val; out3: 3 dim", { +#}) +# +# +# +# +# +# +# +# +# +# +#test_that("in1: 1 val; in2: 1 dim; targ. dims: 0, 0; out1: 0 val", { +#}) +# +#test_that("in1: 1 val; in2: 1 dim; targ. dims: 1, 0; out1: 0 val", { +#}) +# +#test_that("in1: 1 val; in2: 1 dim; targ. dims: 0, 1; out1: 0 val", { +#}) +# +#test_that("in1: 1 val; in2: 1 dim; targ. dims: 1, 1; out1: 0 val", { +#}) +# +#test_that("in1: 1 val; in2: 1 dim; targ. dims: 0, 0; out1: 1 val", { +#}) +# +#test_that("in1: 1 val; in2: 1 dim; targ. dims: 1, 0; out1: 1 val", { +#}) +# +#test_that("in1: 1 val; in2: 1 dim; targ. dims: 0, 1; out1: 1 val", { +#}) +# +#test_that("in1: 1 val; in2: 1 dim; targ. dims: 1, 1; out1: 1 val", { +#}) +# +#test_that("in1: 1 val; in2: 1 dim; targ. dims: 0, 0; out1: 3 val", { +#}) +# +#test_that("in1: 1 val; in2: 1 dim; targ. dims: 1, 0; out1: 3 val", { +#}) +# +#test_that("in1: 1 val; in2: 1 dim; targ. dims: 0, 1; out1: 3 val", { +#}) +# +#test_that("in1: 1 val; in2: 1 dim; targ. dims: 1, 1; out1: 3 val", { +#}) +# +#test_that("in1: 1 val; in2: 1 dim; targ. dims: 0, 0; out1: 1 dim", { +#}) +# +#test_that("in1: 1 val; in2: 1 dim; targ. dims: 1, 0; out1: 1 dim", { +#}) +# +#test_that("in1: 1 val; in2: 1 dim; targ. dims: 0, 1; out1: 1 dim", { +#}) +# +#test_that("in1: 1 val; in2: 1 dim; targ. dims: 1, 1; out1: 1 dim", { +#}) +# +#test_that("in1: 1 val; in2: 1 dim; targ. dims: 0, 0; out1: 1 dim; out2: 1 val; out3: 3 dim", { +#}) +# +#test_that("in1: 1 val; in2: 1 dim; targ. dims: 1, 0; out1: 1 dim; out2: 1 val; out3: 3 dim", { +#}) +# +#test_that("in1: 1 val; in2: 1 dim; targ. dims: 0, 1; out1: 1 dim; out2: 1 val; out3: 3 dim", { +#}) +# +#test_that("in1: 1 val; in2: 1 dim; targ. dims: 1, 1; out1: 1 dim; out2: 1 val; out3: 3 dim", { +#}) + + + + + + + + + + +#test_that("in1: 2 dim; in2: 1 dim; targ. dims: 0, 0; out1: 0 val", { +#}) +# +#test_that("in1: 2 dim; in2: 1 dim; targ. dims: 1, 0; out1: 0 val", { +##first in first dim +##first in second dim +#}) +# +#test_that("in1: 2 dim; in2: 1 dim; targ. dims: 0, 1; out1: 0 val", { +#}) +# +#test_that("in1: 2 dim; in2: 1 dim; targ. dims: 1, 1; out1: 0 val", { +##shared target dims +###first in first dim +###first in second dim +##not shared target dims +###first in first dim +###first in second dim +#}) +# +#test_that("in1: 2 dim; in2: 1 dim; targ. dims: 2, 1; out1: 0 val", { +## shared first target dim +## shared second target dim +## not shared target dims +#}) +# +#test_that("in1: 2 dim; in2: 1 dim; targ. dims: 0, 0; out1: 1 val", { +#}) +# +#test_that("in1: 2 dim; in2: 1 dim; targ. dims: 1, 0; out1: 1 val", { +##first in first dim +##first in second dim +#}) +# +#test_that("in1: 2 dim; in2: 1 dim; targ. dims: 0, 1; out1: 1 val", { +#}) +# +#test_that("in1: 2 dim; in2: 1 dim; targ. dims: 1, 1; out1: 1 val", { +##shared target dims +###first in first dim +###first in second dim +##not shared target dims +###first in first dim +###first in second dim +#}) +# +#test_that("in1: 2 dim; in2: 1 dim; targ. dims: 2, 1; out1: 1 val", { +## shared first target dim +## shared second target dim +## not shared target dims +#}) +# +#test_that("in1: 2 dim; in2: 1 dim; targ. dims: 0, 0; out1: 3 val", { +#}) +# +#test_that("in1: 2 dim; in2: 1 dim; targ. dims: 1, 0; out1: 3 val", { +##first in first dim +##first in second dim +#}) +# +#test_that("in1: 2 dim; in2: 1 dim; targ. dims: 0, 1; out1: 3 val", { +#}) +# +#test_that("in1: 2 dim; in2: 1 dim; targ. dims: 1, 1; out1: 3 val", { +##shared target dims +###first in first dim +###first in second dim +##not shared target dims +###first in first dim +###first in second dim +#}) +# +#test_that("in1: 2 dim; in2: 1 dim; targ. dims: 2, 1; out1: 3 val", { +## shared first target dim +## shared second target dim +## not shared target dims +#}) +# +#test_that("in1: 2 dim; in2: 1 dim; targ. dims: 0, 0; out1: 1 dim", { +#}) +# +#test_that("in1: 2 dim; in2: 1 dim; targ. dims: 1, 0; out1: 1 dim", { +##first in first dim +##first in second dim +#}) +# +#test_that("in1: 2 dim; in2: 1 dim; targ. dims: 0, 1; out1: 1 dim", { +#}) +# +#test_that("in1: 2 dim; in2: 1 dim; targ. dims: 1, 1; out1: 1 dim", { +##shared target dims +###first in first dim +###first in second dim +##not shared target dims +###first in first dim +###first in second dim +#}) +# +#test_that("in1: 2 dim; in2: 1 dim; targ. dims: 2, 1; out1: 1 dim", { +## shared first target dim +## shared second target dim +## not shared target dims +#}) + +test_that("in1: 2 dim; in2: 1 dim; targ. dims: 0-2, 0-1; out1: 1 dim; out2: 1 val; out3: 3 dim", { +# no target dims +## no shared margins + # unnamed input dim + # unnamed output + # unnamed output dim + # equal dimensions -> crash + f <- function(x, y) { + z <- mean(x) + mean(y) + list(array(z:(z + 3)), + z, + array(z:(z + 4), dim = c(5, 6, 7))) + } + expect_error( + Apply(list(array(1:10, dim = c(10, 3)), + array(1:3 * 10, dim = c(3))), + NULL, f), + "multiple unnamed dimensions of the same length" + ) + # unnamed input dim + # unnamed output + # unnamed output dim + expect_equal( + Apply(list(array(1:10, dim = c(10, 4)), + array(1:3 * 10, dim = c(3))), + NULL, f), + list(output1 = array(sapply(c(10, 20, 30), function(x) { + x + rep(sapply(1:10, function(y) { + y:(y + 3) + }), 4) + }), + dim = c(4, 10, 4, 3)), + output2 = array(sapply(c(10, 20, 30), function (x) x + rep(1:10, 4)), + dim = c(10, 4, 3)), + output3 = array(sapply(c(10, 20, 30), function(x) { + x + rep(sapply(1:10, function(y) { + rep(y:(y + 4), 6 * 7) + }), 4) + }), + dim = c(5, 6, 7, 10, 4, 3))) + ) + # named input dim + # named output + # named output dim + f <- function(x, y) { + z <- mean(x) + mean(y) + list(a = array(z:(z + 3), dim = c(d = 4)), + b = z, + c = array(z:(z + 4), dim = c(e = 5, f = 6, g = 7))) + } + expect_equal( + Apply(list(array(1:10, dim = c(a = 10, b = 4)), + array(1:3 * 10, dim = c(c = 3))), + NULL, f), + list(a = array(sapply(c(10, 20, 30), function(x) { + x + rep(sapply(1:10, function(y) { + y:(y + 3) + }), 4) + }), + dim = c(d = 4, a = 10, b = 4, c = 3)), + b = array(sapply(c(10, 20, 30), function (x) x + rep(1:10, 4)), + dim = c(a = 10, b = 4, c = 3)), + c = array(sapply(c(10, 20, 30), function(x) { + x + rep(sapply(1:10, function(y) { + rep(y:(y + 4), 6 * 7) + }), 4) + }), + dim = c(e = 5, f = 6, g = 7, a = 10, b = 4, c = 3))) + ) +## one shared margin + # named input dim + # named output + # named output dim + f <- function(x, y) { + z <- mean(x) + mean(y) + list(a = array(z:(z + 3), dim = c(d = 4)), + b = z, + c = array(z:(z + 4), dim = c(e = 5, f = 6, g = 7))) + } + expect_equal( + Apply(list(array(1:10, dim = c(a = 10, b = 3)), + array(1:3 * 10, dim = c(b = 3))), + NULL, f), + list(a = array(sapply(c(10, 20, 30), function(x) { + x + rep(sapply(1:10, function(y) { + y:(y + 3) + }), 1) + }), + dim = c(d = 4, a = 10, b = 3)), + b = array(sapply(c(10, 20, 30), function (x) x + rep(1:10, 1)), + dim = c(a = 10, b = 3)), + c = array(sapply(c(10, 20, 30), function(x) { + x + rep(sapply(1:10, function(y) { + rep(y:(y + 4), 6 * 7) + }), 1) + }), + dim = c(e = 5, f = 6, g = 7, a = 10, b = 3))) + ) + +#one target dim only +##no shared margins +###first in first dim as target + # unnamed input dim + # unnamed output + # unnamed output dim + f <- function(x, y) { + list(array(rev(x)), + mean(x) + mean(y), + array(rep(x + y - 1, 30), dim = c(10, 5, 6))) + } + expect_equal( + Apply(list(array(1:10, dim = c(10, 4)), + array(1:3 * 10, dim = c(3))), + list(1, NULL), f), + list(output1 = array(sapply(c(10, 20, 30), function(x) { + rep(10:1, 4) + }), + dim = c(10, 4, 3)), + output2 = array(sapply(c(10, 20, 30), function(x) { + x + rep(5.5, 4) + }), + dim = c(4, 3)), + output3 = array(sapply(c(10, 20, 30), function(x) { + rep(1:10 + x - 1, 30 * 4) + }), + dim = c(10, 5, 6, 4, 3))) + ) + # named input dim + # named output + # named output dim + f <- function(x, y) { + list(a = array(rev(x), dim = c(d = 10)), + b = mean(x) + mean(y), + c = array(rep(x + y - 1, 30), dim = c(a = 10, e = 5, f = 6))) + } + expect_equal( + Apply(list(array(1:10, dim = c(a = 10, b = 4)), + array(1:3 * 10, dim = c(c = 3))), + list('a', NULL), f), + list(a = array(sapply(c(10, 20, 30), function(x) { + rep(10:1, 4) + }), + dim = c(d = 10, b = 4, c = 3)), + b = array(sapply(c(10, 20, 30), function(x) { + x + rep(5.5, 4) + }), + dim = c(b = 4, c = 3)), + c = array(sapply(c(10, 20, 30), function(x) { + rep(1:10 + x - 1, 30 * 4) + }), + dim = c(a = 10, e = 5, f = 6, b = 4, c = 3))) + ) +###first in second dim as target + # unnamed input dim + # unnamed output + # unnamed output dim + f <- function(x, y) { + list(array(x[1]:(x[1] + length(x) - 1)), + mean(x) + mean(y), + array(rep(x + y - 1, 30), dim = c(4, 5, 6))) + } + expect_equal( + Apply(list(array(1:10, dim = c(10, 4)), + array(1:3 * 10, dim = c(3))), + list(2, NULL), f), + list(output1 = array(sapply(c(10, 20, 30), function(x) { + sapply(1:10, function(y) { + y:(y + 3) + }) + }), + dim = c(4, 10, 3)), + output2 = array(sapply(c(10, 20, 30), function(x) { + 1:10 + x + }), + dim = c(10, 3)), + output3 = array(sapply(c(10, 20, 30), function(x) { + sapply(1:10, function(y) { + rep(rep(y, 4) + x - 1, 30) + }) + }), + dim = c(4, 5, 6, 10, 3))) + ) + # named input dim + # named output + # named output dim + f <- function(x, y) { + list(a = array(x[1]:(x[1] + length(x) - 1), dim = c(d = 4)), + b = mean(x) + mean(y), + c = array(rep(x + y - 1, 30), dim = c(b = 4, e = 5, f = 6))) + } + expect_equal( + Apply(list(array(1:10, dim = c(a = 10, b = 4)), + array(1:3 * 10, dim = c(c = 3))), + list('b', NULL), f), + list(a = array(sapply(c(10, 20, 30), function(x) { + sapply(1:10, function(y) { + y:(y + 3) + }) + }), + dim = c(d = 4, a = 10, c = 3)), + b = array(sapply(c(10, 20, 30), function(x) { + 1:10 + x + }), + dim = c(a = 10, c = 3)), + c = array(sapply(c(10, 20, 30), function(x) { + sapply(1:10, function(y) { + rep(rep(y, 4) + x - 1, 30) + }) + }), + dim = c(b = 4, e = 5, f = 6, a = 10, c = 3))) + ) +###second in first dim as target + # unnamed input dim + # unnamed output + # unnamed output dim + f <- function(x, y) { + list(array(rev(y)), + mean(x) + mean(y), + array(rep(x + y - 1, 30), dim = c(3, 5, 6))) + } + expect_equal( + Apply(list(array(1:10, dim = c(10, 4)), + array(1:3 * 10, dim = c(3))), + list(NULL, 1), f), + list(output1 = array(rep(sapply(1:10, + function(x) { + c(30, 20, 10) + }), 4), + dim = c(3, 10, 4)), + output2 = array(rep(sapply(1:10, + function(x) { + 20 + x + }), 4), + dim = c(10, 4)), + output3 = array(rep(sapply(1:10, + function(x) { + rep(c(10, 20, 30) + x - 1, 30) + }), 4), + dim = c(3, 5, 6, 10, 4))) + ) + # named input dim + # named output + # named output dim + f <- function(x, y) { + list(a = array(rev(y), dim = c(d = 3)), + b = mean(x) + mean(y), + c = array(rep(x + y - 1, 30), dim = c(c = 3, e = 5, f = 6))) + } + expect_equal( + Apply(list(array(1:10, dim = c(a = 10, b = 4)), + array(1:3 * 10, dim = c(c = 3))), + list(NULL, 'c'), f), + list(a = array(rep(sapply(1:10, + function(x) { + c(30, 20, 10) + }), 4), + dim = c(d = 3, a = 10, b = 4)), + b = array(rep(sapply(1:10, + function(x) { + 20 + x + }), 4), + dim = c(a = 10, b = 4)), + c = array(rep(sapply(1:10, + function(x) { + rep(c(10, 20, 30) + x - 1, 30) + }), 4), + dim = c(c = 3, e = 5, f = 6, a = 10, b = 4))) + ) +##one shared margin. the remaining dim in first in is the target + # named input dim + # named output + # named output dim + f <- function(x, y) { + list(a = array(x[1]:(x[1] + length(x) - 1), dim = c(d = 4)), + b = mean(x) + mean(y), + c = array(rep(x + y - 1, 30), dim = c(b = 4, e = 5, f = 6))) + } + expect_equal( + Apply(list(array(1:3, dim = c(a = 3, b = 4)), + array(1:3 * 10, dim = c(a = 3))), + list('b', NULL), f), + list(a = array(sapply(c(10, 20, 30), function(x) { + sapply(1:4, function(y) { + y[1]:(y[1] + 3) + }) + }), + dim = c(d = 4, a = 3)), + b = array(sapply(c(1:3), function(x) { + x + x * 10 + }), + dim = c(a = 3)), + c = array(sapply(c(1:3), function(x) { + rep(x * 10 + rep(x, 4) - 1, 30) + }), + dim = c(b = 4, e = 5, f = 6, a = 3))) + ) + +#one target dim from each in +##no shared target +##shared target + +#two target dims first in, no target dim second in + +#all target dims +##no shared target + # unnamed input dim + # unnamed output + # unnamed output dim + f <- function(x, y) { + list(array(rowMeans(x)), + mean(x) + mean(y), + array(sapply(y, function(z) z + x - 1), dim = c(10, 4, 3))) + } + expect_equal( + Apply(list(array(1:10, dim = c(10, 4)), + array(1:3 * 10, dim = c(3))), + list(c(1, 2), 1), f), + list(output1 = array(1:10), + output2 = 25.5, + output3 = array(sapply(c(10, 20, 30), + function(x) { + rep(1:10, 4) + x - 1 + }), + dim = c(10, 4, 3))) + ) + # named input dim + # named output + # named output dim + f <- function(x, y) { + list(a = array(rowMeans(x), c(a = 10)), + b = mean(x) + mean(y), + c = array(sapply(y, function(z) z + x - 1), + dim = c(a = 10, b = 4, c = 3))) + } + expect_equal( + Apply(list(array(1:10, dim = c(a = 10, b = 4)), + array(1:3 * 10, dim = c(c = 3))), + list(c('a', 'b'), 'c'), f), + list(a = array(1:10, dim = c(a = 10)), + b = 25.5, + c = array(sapply(c(10, 20, 30), + function(x) { + rep(1:10, 4) + x - 1 + }), + dim = c(a = 10, b = 4, c = 3))) + ) +##shared target + # named input dim + # named output + # named output dim + f <- function(x, y) { + list(a = array(rowMeans(x), c(a = 10)), + b = mean(x) + mean(y), + c = array(rep(t(apply(x, 1, function(z) z * y)), 3), + dim = c(a = 10, b = 4, c = 3))) + } + expect_equal( + Apply(list(array(1:10, dim = c(a = 10, b = 4)), + array(1:4 * 10, dim = c(b = 4))), + list(c('a', 'b'), 'b'), f), + list(a = array(1:10, dim = c(a = 10)), + b = 30.5, + c = array(rep(sapply(c(10, 20, 30, 40), + function(x) { + 1:10 * x + }), + 3), + dim = c(a = 10, b = 4, c = 3))) + ) +}) + +##test_that("in1: 2 dim; in2: 3 dim; targ. dims: 0-2, 0-3; out1: 2 dim", { +### shared first target dim +### shared second target dim +### shared two target dims (first two in second in) +### shared two target dims (last two in second in) +### shared two target dims (extreme two in second in) +### not shared target dims +##}) +# +#test_that("in1: 2 dim; in2: 3 dim; targ. dims: 0-2, 0-3; out1: 1 dim; out2: 1 val; out3: 3 dim", { +## shared first target dim +## shared second target dim +## shared two target dims (first two in second in) +## shared two target dims (last two in second in) +## shared two target dims (extreme two in second in) +## not shared target dims +#}) +# +#test_that("in1: 2 dim; in2: 3 dim; in3: 1 dim; targ. dims: 0-2, 0-3, 0-1; out1: 2 dim", { +## shared first target dim +## shared second target dim +## shared two target dims (first two in second in) +## shared two target dims (last two in second in) +## shared two target dims (extreme two in second in) +## not shared target dims +#}) + +# TODOS: +# TESTS FOR MARGINS +# TESTS FOR DISORDERED TARGET_DIMS +# TESTS FOR FUN WITH TARGET_DIMS AND OUTPUT_DIMS ATTACHED +# TESTS FOR FUNCTIONS RECEIVING ADDITIONAL PARAMETERS +# TESTS FOR SPLIT FACTOR +# TESTS FOR NCORES +# TESTS OF WALLCLOCK TIME +# TESTS OF MEMORY FOOTPRINT -- GitLab From 32992c71de894f11e88b17f965f1c69118846733 Mon Sep 17 00:00:00 2001 From: Nicolau Manubens Gil Date: Fri, 9 Nov 2018 22:16:17 +0100 Subject: [PATCH 07/19] Fixed download badge mistake in README.md. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6c984cb..975ccf3 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ ## multiApply -[![](https://cranlogs.r-pkg.org/badges/ggplot2)](https://cran.rstudio.com/web/packages/ggplot2/index.html) +[![](https://cranlogs.r-pkg.org/badges/multiApply)](https://cran.rstudio.com/web/packages/multiApply/index.html) This package extends the apply and plyr families of functions to applications which involve the use of multiple arrays as input, and is useful to apply a function taking multiple numeric objects as input across multiple multi-dimensional arrays. -- GitLab From 44f2ae552a39efcf0dd75bba7dbe3c72a476fcd7 Mon Sep 17 00:00:00 2001 From: Nicolau Manubens Date: Sun, 11 Nov 2018 15:55:41 +0100 Subject: [PATCH 08/19] Fix for unnamed dimensions. Updates in documentation. Added GitLab CI. --- .gitlab-ci.yml | 16 +++++++++++ NAMESPACE | 12 ++++---- R/Apply.R | 49 ++++++++++++++++++++++++++------- README.md | 5 +++- man/Apply.Rd | 35 +++++++++++++---------- tests/testthat/test-use-cases.R | 27 +++++++++++++++++- 6 files changed, 112 insertions(+), 32 deletions(-) create mode 100644 .gitlab-ci.yml diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..f72523a --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,16 @@ +stages: + - build + - report + +build: + stage: build + script: + - module load R + - R -e 'devtools::build()' + - R -e 'devtools::check()' + +report: + stage: report + script: + - module load R + - R -e 'covr::package_coverage()' diff --git a/NAMESPACE b/NAMESPACE index 283fbf0..aaad7dd 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,8 +1,8 @@ # Generated by roxygen2: do not edit by hand -importFrom(abind, abind) -importFrom(foreach, registerDoSEQ) -importFrom(doParallel, registerDoParallel) -importFrom(plyr, splat) -importFrom(plyr, llply) -importFrom(stats, setNames) + export(Apply) +importFrom(abind,abind) +importFrom(doParallel,registerDoParallel) +importFrom(foreach,registerDoSEQ) +importFrom(plyr,llply) +importFrom(plyr,splat) diff --git a/R/Apply.R b/R/Apply.R index 83ab76b..b126918 100644 --- a/R/Apply.R +++ b/R/Apply.R @@ -7,6 +7,7 @@ #' @param ... Additional arguments to be used in the fun. #' @param output_dims Optional list of vectors containing the names of the dimensions to be output from the fun for each of the objects it returns (or a single vector if the function has only one output). #' @param margins List of vectors containing the margins for the input objects to be split by. Or, if there is a single vector of margins specified and a list of objects in data, then the single set of margins is applied over all objects. These vectors can contain either integers specifying the dimension position, or characters corresponding to the dimension names. If both margins and target_dims are specified, margins takes priority over target_dims. +#' @param guess_dim_names Whether to automatically guess missing dimension names for dimensions of equal length across different inputs in 'data' with a warning (TRUE; default), or to crash whenever unnamed dimensions of equa length are identified across different inputs (FALSE). #' @param ncores The number of multicore threads to use for parallel computation. #' @param split_factor Factor telling to which degree the input data should be split into smaller pieces to be processed by the available cores. By default (split_factor = 1) the data is split into 4 pieces for each of the cores (as specified in ncores). A split_factor of 2 will result in 8 pieces for each of the cores, and so on. The special value 'greatest' will split the input data into as many pieces as possible. #' @details When using a single object as input, Apply is almost identical to the apply function. For multiple input objects, the output array will have dimensions equal to the dimensions specified in 'margins'. @@ -16,14 +17,21 @@ #' @examples #' #Change in the rate of exceedance for two arrays, with different #' #dimensions, for some matrix of exceedances. -#' data = list(array(rnorm(2000), c(10,10,20)), array(rnorm(1000), c(10,10,10)), -#' array(rnorm(100), c(10, 10))) -#' test_fun <- function(x, y, z) {((sum(x > z) / (length(x))) / -#' (sum(y > z) / (length(y)))) * 100} -#' margins = list(c(1, 2), c(1, 2), c(1,2)) -#' test <- Apply(data, margins = margins, fun = "test_fun") -Apply <- function(data, target_dims = NULL, fun, ..., output_dims = NULL, - margins = NULL, ncores = NULL, split_factor = 1) { +#' data <- list(array(rnorm(1000), c(5, 10, 20)), +#' array(rnorm(500), c(5, 10, 10)), +#' array(rnorm(50), c(5, 10))) +#' test_fun <- function(x, y, z) { +#' ((sum(x > z) / (length(x))) / +#' (sum(y > z) / (length(y)))) * 100 +#' } +#' test <- Apply(data, target = list(3, 3, NULL), test_fun) +#' @importFrom abind abind +#' @importFrom foreach registerDoSEQ +#' @importFrom doParallel registerDoParallel +#' @importFrom plyr splat llply +Apply <- function(data, target_dims = NULL, fun, ..., + output_dims = NULL, margins = NULL, guess_dim_names = TRUE, + ncores = NULL, split_factor = 1) { # Check data if (!is.list(data)) { data <- list(data) @@ -34,6 +42,7 @@ Apply <- function(data, target_dims = NULL, fun, ..., output_dims = NULL, is_vector <- rep(FALSE, length(data)) is_unnamed <- rep(FALSE, length(data)) unnamed_dims <- c() + guessed_any_dimnames <- FALSE for (i in 1 : length(data)) { if (length(data[[i]]) < 1) { stop("Arrays in 'data' must be of length > 0.") @@ -57,12 +66,19 @@ Apply <- function(data, target_dims = NULL, fun, ..., output_dims = NULL, } else { is_unnamed[i] <- TRUE new_unnamed_dims <- c() + unnamed_dims_copy <- unnamed_dims for (j in 1 : length(dim(data[[i]]))) { len_of_dim_j <- dim(data[[i]])[j] - found_match <- which(unnamed_dims == len_of_dim_j) - if (length(found_match) > 0) { + found_match <- which(unnamed_dims_copy == len_of_dim_j) + if (!guess_dim_names && (length(found_match) > 0)) { stop("Arrays in 'data' have multiple unnamed dimensions of the ", "same length. Please provide dimension names.") + } + if (length(found_match) > 0) { + found_match <- found_match[1] + names(dim(data[[i]]))[j] <- names(unnamed_dims_copy[found_match]) + unnamed_dims_copy <- unnamed_dims_copy[-found_match] + guessed_any_dimnames <- TRUE } else { new_dim <- len_of_dim_j names(new_dim) <- paste0('_unnamed_dim_', length(unnamed_dims) + @@ -74,6 +90,19 @@ Apply <- function(data, target_dims = NULL, fun, ..., output_dims = NULL, unnamed_dims <- c(unnamed_dims, new_unnamed_dims) } } + if (guessed_any_dimnames) { + dim_names_string <- "" + for (i in 1:length(data)) { + dim_names_string <- c(dim_names_string, "\n\tInput ", i, ":", + sapply(capture.output(print(dim(data[[i]]))), + function(x) paste0('\n\t\t', x))) + } + warning("Guessed names for some unnamed dimensions of equal length ", + "found across different inputs in 'data'. Please check ", + "carefully the assumed names below are correct, or provide ", + "dimension names for safety, or disable the parameter ", + "'guess_dimension_names'.", dim_names_string) + } # Check fun if (is.character(fun)) { diff --git a/README.md b/README.md index 975ccf3..3bdb2c3 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,9 @@ ## multiApply -[![](https://cranlogs.r-pkg.org/badges/multiApply)](https://cran.rstudio.com/web/packages/multiApply/index.html) +[![build status](https://earth.bsc.es/gitlab/ces/multiApply/badges/master/build.svg)](https://earth.bsc.es/gitlab/ces/multiApply/commits/master) +[![coverage report](https://earth.bsc.es/gitlab/ces/multiApply/badges/master/coverage.svg)](https://earth.bsc.es/gitlab/ces/multiApply/commits/master) +[![CRAN version](http://www.r-pkg.org/badges/version/multiApply)](https://cran.r-project.org/package=multiApply) +[![CRAN RStudio Downloads](https://cranlogs.r-pkg.org/badges/multiApply)](https://cran.rstudio.com/web/packages/multiApply/index.html) This package extends the apply and plyr families of functions to applications which involve the use of multiple arrays as input, and is useful to apply a function taking multiple numeric objects as input across multiple multi-dimensional arrays. diff --git a/man/Apply.Rd b/man/Apply.Rd index 2f69b6c..592c6af 100644 --- a/man/Apply.Rd +++ b/man/Apply.Rd @@ -4,26 +4,31 @@ \alias{Apply} \title{Wrapper for Applying Atomic Functions to Arrays.} \usage{ -Apply(data, target_dims = NULL, AtomicFun, ..., output_dims = NULL, - margins = NULL, ncores = NULL) +Apply(data, target_dims = NULL, fun, ..., output_dims = NULL, + margins = NULL, guess_dim_names = TRUE, ncores = NULL, + split_factor = 1) } \arguments{ -\item{data}{A single object (vector, matrix or array) or a list of objects. They must be in the same order as expected by AtomicFun.} +\item{data}{A single object (vector, matrix or array) or a list of objects. They must be in the same order as expected by fun.} -\item{target_dims}{List of vectors containing the dimensions to be input into AtomicFun for each of the objects in the data. These vectors can contain either integers specifying the dimension position, or characters corresponding to the dimension names. This parameter is mandatory if margins is not specified. If both margins and target_dims are specified, margins takes priority over target_dims.} +\item{target_dims}{List of vectors containing the dimensions to be input into fun for each of the objects in the data. These vectors can contain either integers specifying the dimension position, or characters corresponding to the dimension names. This parameter is mandatory if margins is not specified. If both margins and target_dims are specified, margins takes priority over target_dims.} -\item{AtomicFun}{Function to be applied to the arrays.} +\item{fun}{Function to be applied to the arrays.} -\item{...}{Additional arguments to be used in the AtomicFun.} +\item{...}{Additional arguments to be used in the fun.} -\item{output_dims}{Optional list of vectors containing the names of the dimensions to be output from the AtomicFun for each of the objects it returns (or a single vector if the function has only one output).} +\item{output_dims}{Optional list of vectors containing the names of the dimensions to be output from the fun for each of the objects it returns (or a single vector if the function has only one output).} \item{margins}{List of vectors containing the margins for the input objects to be split by. Or, if there is a single vector of margins specified and a list of objects in data, then the single set of margins is applied over all objects. These vectors can contain either integers specifying the dimension position, or characters corresponding to the dimension names. If both margins and target_dims are specified, margins takes priority over target_dims.} +\item{guess_dim_names}{Whether to automatically guess missing dimension names for dimensions of equal length across different inputs in 'data' with a warning (TRUE; default), or to crash whenever unnamed dimensions of equa length are identified across different inputs (FALSE).} + \item{ncores}{The number of multicore threads to use for parallel computation.} + +\item{split_factor}{Factor telling to which degree the input data should be split into smaller pieces to be processed by the available cores. By default (split_factor = 1) the data is split into 4 pieces for each of the cores (as specified in ncores). A split_factor of 2 will result in 8 pieces for each of the cores, and so on. The special value 'greatest' will split the input data into as many pieces as possible.} } \value{ -List of arrays or matrices or vectors resulting from applying AtomicFun to data. +List of arrays or matrices or vectors resulting from applying fun to data. } \description{ This wrapper applies a given function, which takes N [multi-dimensional] arrays as inputs (which may have different numbers of dimensions and dimension lengths), and applies it to a list of N [multi-dimensional] arrays with at least as many dimensions as expected by the given function. The user can specify which dimensions of each array (or matrix) the function is to be applied over with the \code{margins} or \code{target_dims} option. A user can apply a function that receives (in addition to other helper parameters) 1 or more arrays as input, each with a different number of dimensions, and returns any number of multidimensional arrays. The target dimensions can be specified by their names. It is recommended to use this wrapper with multidimensional arrays with named dimensions. @@ -34,12 +39,14 @@ When using a single object as input, Apply is almost identical to the apply func \examples{ #Change in the rate of exceedance for two arrays, with different #dimensions, for some matrix of exceedances. -data = list(array(rnorm(2000), c(10,10,20)), array(rnorm(1000), c(10,10,10)), - array(rnorm(100), c(10, 10))) -test_fun <- function(x, y, z) {((sum(x > z) / (length(x))) / - (sum(y > z) / (length(y)))) * 100} -margins = list(c(1, 2), c(1, 2), c(1,2)) -test <- Apply(data, margins = margins, AtomicFun = "test_fun") +data <- list(array(rnorm(1000), c(5, 10, 20)), + array(rnorm(500), c(5, 10, 10)), + array(rnorm(50), c(5, 10))) +test_fun <- function(x, y, z) { + ((sum(x > z) / (length(x))) / + (sum(y > z) / (length(y)))) * 100 +} +test <- Apply(data, target = list(3, 3, NULL), test_fun) } \references{ Wickham, H (2011), The Split-Apply-Combine Strategy for Data Analysis, Journal of Statistical Software. diff --git a/tests/testthat/test-use-cases.R b/tests/testthat/test-use-cases.R index a9a6498..e686250 100644 --- a/tests/testthat/test-use-cases.R +++ b/tests/testthat/test-use-cases.R @@ -790,9 +790,34 @@ test_that("in1: 2 dim; in2: 1 dim; targ. dims: 0-2, 0-1; out1: 1 dim; out2: 1 va expect_error( Apply(list(array(1:10, dim = c(10, 3)), array(1:3 * 10, dim = c(3))), - NULL, f), + NULL, f, guess_dim_names = FALSE), "multiple unnamed dimensions of the same length" ) + expect_warning( + Apply(list(array(1:10, dim = c(10, 3)), + array(1:3 * 10, dim = c(3))), + NULL, f), + "Guessed names for some unnamed dimensions" + ) + expect_equal( + Apply(list(array(1:10, dim = c(10, 3)), + array(1:3 * 10, dim = c(3))), + NULL, f), + list(output1 = array(sapply(c(10, 20, 30), function(x) { + x + rep(sapply(1:10, function(y) { + y:(y + 3) + }), 1) + }), + dim = c(4, 10, 3)), + output2 = array(sapply(c(10, 20, 30), function (x) x + rep(1:10, 1)), + dim = c(10, 3)), + output3 = array(sapply(c(10, 20, 30), function(x) { + x + rep(sapply(1:10, function(y) { + rep(y:(y + 4), 6 * 7) + }), 1) + }), + dim = c(5, 6, 7, 10, 3))) + ) # unnamed input dim # unnamed output # unnamed output dim -- GitLab From 5c7e103d9cfe09dda22aef2f68b152bc0e715894 Mon Sep 17 00:00:00 2001 From: Nicolau Manubens Date: Sun, 11 Nov 2018 16:02:47 +0100 Subject: [PATCH 09/19] Added gitlab-ci.yml to Rbuildignore. --- .Rbuildignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.Rbuildignore b/.Rbuildignore index 834c4fa..97c7dbe 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -1,5 +1,6 @@ .git .gitignore +.gitlab-ci.yml .tar.gz .pdf ./.nc -- GitLab From e1ecbbda64364d3c175f2242bafec40dd7fab8b1 Mon Sep 17 00:00:00 2001 From: Nicolau Manubens Date: Sun, 11 Nov 2018 16:29:44 +0100 Subject: [PATCH 10/19] Simplified GitLab CI pipeline. --- .gitlab-ci.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f72523a..398a915 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,6 +1,5 @@ stages: - build - - report build: stage: build @@ -8,9 +7,4 @@ build: - module load R - R -e 'devtools::build()' - R -e 'devtools::check()' - -report: - stage: report - script: - - module load R - R -e 'covr::package_coverage()' -- GitLab From ab747a6f3e6d9b362a04648afd4c6d576bab7924 Mon Sep 17 00:00:00 2001 From: Nicolau Manubens Date: Sun, 11 Nov 2018 17:59:16 +0100 Subject: [PATCH 11/19] Cleanup and added extra badges. --- DESCRIPTION | 3 +- NAMESPACE | 1 - R/Apply.R | 1 - R/zzz.R | 134 ---------------------------------------------------- README.md | 7 +-- 5 files changed, 2 insertions(+), 144 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 1f13960..22220b5 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,5 +1,5 @@ Package: multiApply -Title: Apply Functions to Multiple Multidimensional Arguments +Title: Apply Functions to Multiple Multidimensional Arrays or Vectors Version: 1.0.0 Authors@R: c( person("BSC-CNS", role = c("aut", "cph")), @@ -9,7 +9,6 @@ Description: The base apply function and its variants, as well as the related fu Depends: R (>= 3.2.0) Imports: - abind, doParallel, foreach, plyr diff --git a/NAMESPACE b/NAMESPACE index aaad7dd..12eb9c5 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,7 +1,6 @@ # Generated by roxygen2: do not edit by hand export(Apply) -importFrom(abind,abind) importFrom(doParallel,registerDoParallel) importFrom(foreach,registerDoSEQ) importFrom(plyr,llply) diff --git a/R/Apply.R b/R/Apply.R index b126918..1ee5703 100644 --- a/R/Apply.R +++ b/R/Apply.R @@ -25,7 +25,6 @@ #' (sum(y > z) / (length(y)))) * 100 #' } #' test <- Apply(data, target = list(3, 3, NULL), test_fun) -#' @importFrom abind abind #' @importFrom foreach registerDoSEQ #' @importFrom doParallel registerDoParallel #' @importFrom plyr splat llply diff --git a/R/zzz.R b/R/zzz.R index 4df33e9..2bf747e 100644 --- a/R/zzz.R +++ b/R/zzz.R @@ -11,137 +11,3 @@ dim(x) <- old_dims[new_order] x } - -# This function is a helper for the function .MergeArrays. -# It expects as inputs two named numeric vectors, and it extends them -# with dimensions of length 1 until an ordered common dimension -# format is reached. -.MergeArrayDims <- function(dims1, dims2) { - new_dims1 <- c() - new_dims2 <- c() - while (length(dims1) > 0) { - if (names(dims1)[1] %in% names(dims2)) { - pos <- which(names(dims2) == names(dims1)[1]) - dims_to_add <- rep(1, pos - 1) - if (length(dims_to_add) > 0) { - names(dims_to_add) <- names(dims2[1:(pos - 1)]) - } - new_dims1 <- c(new_dims1, dims_to_add, dims1[1]) - new_dims2 <- c(new_dims2, dims2[1:pos]) - dims1 <- dims1[-1] - dims2 <- dims2[-c(1:pos)] - } else { - new_dims1 <- c(new_dims1, dims1[1]) - new_dims2 <- c(new_dims2, 1) - names(new_dims2)[length(new_dims2)] <- names(dims1)[1] - dims1 <- dims1[-1] - } - } - if (length(dims2) > 0) { - dims_to_add <- rep(1, length(dims2)) - names(dims_to_add) <- names(dims2) - new_dims1 <- c(new_dims1, dims_to_add) - new_dims2 <- c(new_dims2, dims2) - } - list(new_dims1, new_dims2) -} - -# This function takes two named arrays and merges them, filling with -# NA where needed. -# dim(array1) -# 'b' 'c' 'e' 'f' -# 1 3 7 9 -# dim(array2) -# 'a' 'b' 'd' 'f' 'g' -# 2 3 5 9 11 -# dim(.MergeArrays(array1, array2, 'b')) -# 'a' 'b' 'c' 'e' 'd' 'f' 'g' -# 2 4 3 7 5 9 11 -.MergeArrays <- function(array1, array2, along) { - if (!(is.null(array1) || is.null(array2))) { - if (!(identical(names(dim(array1)), names(dim(array2))) && - identical(dim(array1)[-which(names(dim(array1)) == along)], - dim(array2)[-which(names(dim(array2)) == along)]))) { - new_dims <- .MergeArrayDims(dim(array1), dim(array2)) - dim(array1) <- new_dims[[1]] - dim(array2) <- new_dims[[2]] - for (j in 1:length(dim(array1))) { - if (names(dim(array1))[j] != along) { - if (dim(array1)[j] != dim(array2)[j]) { - if (which.max(c(dim(array1)[j], dim(array2)[j])) == 1) { - na_array_dims <- dim(array2) - na_array_dims[j] <- dim(array1)[j] - dim(array2)[j] - na_array <- array(dim = na_array_dims) - array2 <- abind(array2, na_array, along = j) - names(dim(array2)) <- names(na_array_dims) - } else { - na_array_dims <- dim(array1) - na_array_dims[j] <- dim(array2)[j] - dim(array1)[j] - na_array <- array(dim = na_array_dims) - array1 <- abind(array1, na_array, along = j) - names(dim(array1)) <- names(na_array_dims) - } - } - } - } - } - if (!(along %in% names(dim(array2)))) { - stop("The dimension specified in 'along' is not present in the ", - "provided arrays.") - } - array1 <- abind(array1, array2, along = which(names(dim(array1)) == along)) - names(dim(array1)) <- names(dim(array2)) - } else if (is.null(array1)) { - array1 <- array2 - } - array1 -} - -# Takes as input a list of arrays. The list must have named dimensions. -.MergeArrayOfArrays <- function(array_of_arrays) { - MergeArrays <- .MergeArrays - array_dims <- (dim(array_of_arrays)) - dim_names <- names(array_dims) - - # Merge the chunks. - for (dim_index in 1:length(dim_names)) { - dim_sub_array_of_chunks <- dim_sub_array_of_chunk_indices <- NULL - if (dim_index < length(dim_names)) { - dim_sub_array_of_chunks <- array_dims[(dim_index + 1):length(dim_names)] - names(dim_sub_array_of_chunks) <- dim_names[(dim_index + 1):length(dim_names)] - dim_sub_array_of_chunk_indices <- dim_sub_array_of_chunks - sub_array_of_chunk_indices <- array(1:prod(dim_sub_array_of_chunk_indices), - dim_sub_array_of_chunk_indices) - } else { - sub_array_of_chunk_indices <- NULL - } - sub_array_of_chunks <- vector('list', prod(dim_sub_array_of_chunks)) - dim(sub_array_of_chunks) <- dim_sub_array_of_chunks - for (i in 1:prod(dim_sub_array_of_chunks)) { - if (!is.null(sub_array_of_chunk_indices)) { - chunk_sub_indices <- which(sub_array_of_chunk_indices == i, arr.ind = TRUE)[1, ] - } else { - chunk_sub_indices <- NULL - } - for (j in 1:(array_dims[dim_index])) { - new_chunk <- do.call('[[', c(list(x = array_of_arrays), - as.list(c(j, chunk_sub_indices)))) - if (is.null(new_chunk)) { - stop("Chunks missing.") - } - if (is.null(sub_array_of_chunks[[i]])) { - sub_array_of_chunks[[i]] <- new_chunk - } else { - sub_array_of_chunks[[i]] <- MergeArrays(sub_array_of_chunks[[i]], - new_chunk, - dim_names[dim_index]) - } - } - } - array_of_arrays <- sub_array_of_chunks - rm(sub_array_of_chunks) - gc() - } - - array_of_arrays[[1]] -} diff --git a/README.md b/README.md index 3bdb2c3..082fbd3 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,4 @@ -## multiApply - -[![build status](https://earth.bsc.es/gitlab/ces/multiApply/badges/master/build.svg)](https://earth.bsc.es/gitlab/ces/multiApply/commits/master) -[![coverage report](https://earth.bsc.es/gitlab/ces/multiApply/badges/master/coverage.svg)](https://earth.bsc.es/gitlab/ces/multiApply/commits/master) -[![CRAN version](http://www.r-pkg.org/badges/version/multiApply)](https://cran.r-project.org/package=multiApply) -[![CRAN RStudio Downloads](https://cranlogs.r-pkg.org/badges/multiApply)](https://cran.rstudio.com/web/packages/multiApply/index.html) +## multiApply [![build status](https://earth.bsc.es/gitlab/ces/multiApply/badges/master/build.svg)](https://earth.bsc.es/gitlab/ces/multiApply/commits/master) [![CRAN version](http://www.r-pkg.org/badges/version/multiApply)](https://cran.r-project.org/package=multiApply) [![coverage report](https://earth.bsc.es/gitlab/ces/multiApply/badges/master/coverage.svg)](https://earth.bsc.es/gitlab/ces/multiApply/commits/master) [![dependencies](https://tinyverse.netlify.com/badge/multiApply)](https://cran.r-project.org/package=multiApply) [![License: LGPL v3](https://img.shields.io/badge/License-LGPL%20v3-blue.svg)](https://www.gnu.org/licenses/lgpl-3.0) [![CRAN RStudio Downloads](https://cranlogs.r-pkg.org/badges/multiApply)](https://cran.rstudio.com/web/packages/multiApply/index.html) This package extends the apply and plyr families of functions to applications which involve the use of multiple arrays as input, and is useful to apply a function taking multiple numeric objects as input across multiple multi-dimensional arrays. -- GitLab From e85f11534d73931068558869b32b9562323d77d9 Mon Sep 17 00:00:00 2001 From: Nicolau Manubens Date: Sun, 11 Nov 2018 20:27:33 +0100 Subject: [PATCH 12/19] Attempt at indexed arrays once more. Memory consumption improves but wall-clock time becomes unacceptable. Commit to be undone. --- R/Apply.R | 81 ++++++++++++++++++++++++++++++++++++------------------- README.md | 2 +- 2 files changed, 54 insertions(+), 29 deletions(-) diff --git a/R/Apply.R b/R/Apply.R index 1ee5703..dfa90ee 100644 --- a/R/Apply.R +++ b/R/Apply.R @@ -401,29 +401,48 @@ Apply <- function(data, target_dims = NULL, fun, ..., chunk_sizes <- c(chunk_sizes, total_size %% chunk_size) } - input_margin_weights <- lapply(1:length(data), - function(i) { - marg_sizes <- dim(data[[i]])[margins[[i]]] - sapply(1:length(marg_sizes), - function(k) prod(c(1, marg_sizes)[1:k])) - }) - + # The following few lines have an impact on memory footprint. # Flattening margin dimensions so that the iteration function can access # them easily. - for (i in 1:length(data)) { - if (length(margins[[i]]) > 0) { - dims <- dim(data[[i]]) - margins_inds <- 1:length(margins[[i]]) + length(target_dims[[i]]) - dim(data[[i]]) <- c(dims[-margins_inds], - '_margins_dim_' = prod(dims[margins_inds])) +### for (i in 1:length(data)) { +### if (length(margins[[i]]) > 0) { +### dims <- dim(data[[i]]) +### margins_inds <- 1:length(margins[[i]]) + length(target_dims[[i]]) +### dim(data[[i]]) <- c(dims[-margins_inds], +### '_margins_dim_' = prod(dims[margins_inds])) +### } else { +### dim(data[[i]]) <- c(dim(data[[i]]), '_margins_dim_' = 1) +### } +### } + .isolate <- function(data, margins, drop = FALSE) { + if (length(margins) > 0) { + margin_length <- lapply(dim(data), function(x) 1 : x) + margin_length[- margins] <- "" } else { - dim(data[[i]]) <- c(dim(data[[i]]), '_margins_dim_' = 1) + margin_length <- as.list(rep("", length(dim(data)))) } + margin_length <- expand.grid(margin_length, KEEP.OUT.ATTRS = FALSE, + stringsAsFactors = FALSE) + + eval(dim(environment()$data)) + structure(list(env = environment(), index = margin_length, + drop = drop, subs = as.name("[")), + class = c("indexed_array")) + } + input_margin_weights <- vector('list', length(data)) + iteration_input_dims <- vector('list', length(data)) + flat_data <- vector('list', length(data)) + for (i in 1:length(data)) { + flat_data[[i]] <- .isolate(data[[i]], margins[[i]]) + marg_sizes <- dim(data[[i]])[margins[[i]]] + input_margin_weights[[i]] <- sapply(1:length(marg_sizes), + function(k) prod(c(1, marg_sizes)[1:k])) + iteration_input_dims[[i]] <- dim(data[[i]])[target_dims[[i]]] } + # TODO: need to add progress bar # TODO: IF ONLY ONE INPUT ARRAY, MAKE USE OF apply. splatted_f <- splat(fun) - # For a selected use case, these are the timings: # - total: 17 s # - preparation + post: 1 s @@ -443,11 +462,11 @@ Apply <- function(data, target_dims = NULL, fun, ..., names(first_marg_indices) <- names(mad) sub_arrays_of_results <- list() found_first_sub_result <- FALSE - iteration_indices_to_take <- list() - for (i in 1:length(data)) { - iteration_indices_to_take[[i]] <- as.list(rep(TRUE, length(dim(data[[i]])))) - } - +### iteration_indices_to_take <- list() +### for (i in 1:length(data)) { +### iteration_indices_to_take[[i]] <- as.list(rep(TRUE, length(dim(data[[i]])))) +### } +### add_one_multidim <- function(index, dims) { stop_iterating <- FALSE check_dim <- 1 @@ -476,15 +495,21 @@ Apply <- function(data, target_dims = NULL, fun, ..., input_margin_dim_index <- first_marg_indices[margins_names[[i]]] input_margin_dim_index <- 1 + sum((input_margin_dim_index - 1) * input_margin_weights[[i]]) - iteration_indices_to_take[[i]][[length(dim(data[[i]]))]] <- input_margin_dim_index - iteration_input[[i]] <- do.call('[', c(list(x = data[[i]]), - iteration_indices_to_take[[i]], - list(drop = FALSE))) - num_dims <- length(dim(iteration_input[[i]])) - if (num_dims > 1) { - dim(iteration_input[[i]]) <- dim(iteration_input[[i]])[-length(dim(iteration_input[[i]]))] +### iteration_indices_to_take[[i]][[length(dim(data[[i]]))]] <- input_margin_dim_index +### iteration_input[[i]] <- do.call('[', c(list(x = data[[i]]), +### iteration_indices_to_take[[i]], +### list(drop = FALSE))) +### num_dims <- length(dim(iteration_input[[i]])) +### if (num_dims > 1) { +### dim(iteration_input[[i]]) <- dim(iteration_input[[i]])[-length(dim(iteration_input[[i]]))] +### } else { +### dim(iteration_input[[i]]) <- NULL +### } + if (length(iteration_input_dims[[i]]) > 0) { + iteration_input[[i]] <- array(flat_data[[i]][[input_margin_dim_index]], + dim = iteration_input_dims[[i]]) } else { - dim(iteration_input[[i]]) <- NULL + iteration_input[[i]] <- as.vector(flat_data[[i]][[input_margin_dim_index]]) } } if (!is.null(mad)) { diff --git a/README.md b/README.md index 082fbd3..c3466a5 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -## multiApply [![build status](https://earth.bsc.es/gitlab/ces/multiApply/badges/master/build.svg)](https://earth.bsc.es/gitlab/ces/multiApply/commits/master) [![CRAN version](http://www.r-pkg.org/badges/version/multiApply)](https://cran.r-project.org/package=multiApply) [![coverage report](https://earth.bsc.es/gitlab/ces/multiApply/badges/master/coverage.svg)](https://earth.bsc.es/gitlab/ces/multiApply/commits/master) [![dependencies](https://tinyverse.netlify.com/badge/multiApply)](https://cran.r-project.org/package=multiApply) [![License: LGPL v3](https://img.shields.io/badge/License-LGPL%20v3-blue.svg)](https://www.gnu.org/licenses/lgpl-3.0) [![CRAN RStudio Downloads](https://cranlogs.r-pkg.org/badges/multiApply)](https://cran.rstudio.com/web/packages/multiApply/index.html) +## multiApply [![build status](https://earth.bsc.es/gitlab/ces/multiApply/badges/master/build.svg)](https://earth.bsc.es/gitlab/ces/multiApply/commits/master) [![CRAN version](http://www.r-pkg.org/badges/version/multiApply)](https://cran.r-project.org/package=multiApply) [![coverage report](https://earth.bsc.es/gitlab/ces/multiApply/badges/master/coverage.svg)](https://earth.bsc.es/gitlab/ces/multiApply/commits/master) [![License: LGPL v3](https://img.shields.io/badge/License-LGPL%20v3-blue.svg)](https://www.gnu.org/licenses/lgpl-3.0) [![CRAN RStudio Downloads](https://cranlogs.r-pkg.org/badges/multiApply)](https://cran.rstudio.com/web/packages/multiApply/index.html) This package extends the apply and plyr families of functions to applications which involve the use of multiple arrays as input, and is useful to apply a function taking multiple numeric objects as input across multiple multi-dimensional arrays. -- GitLab From 77d2ad412623c1861d1146f0e544de347ed1b898 Mon Sep 17 00:00:00 2001 From: Nicolau Manubens Date: Sun, 11 Nov 2018 20:38:04 +0100 Subject: [PATCH 13/19] Reverting use of indexed arrays. --- R/Apply.R | 90 +++++++++++++++++++++++++++---------------------------- 1 file changed, 45 insertions(+), 45 deletions(-) diff --git a/R/Apply.R b/R/Apply.R index dfa90ee..07b276d 100644 --- a/R/Apply.R +++ b/R/Apply.R @@ -404,40 +404,40 @@ Apply <- function(data, target_dims = NULL, fun, ..., # The following few lines have an impact on memory footprint. # Flattening margin dimensions so that the iteration function can access # them easily. -### for (i in 1:length(data)) { -### if (length(margins[[i]]) > 0) { -### dims <- dim(data[[i]]) -### margins_inds <- 1:length(margins[[i]]) + length(target_dims[[i]]) -### dim(data[[i]]) <- c(dims[-margins_inds], -### '_margins_dim_' = prod(dims[margins_inds])) -### } else { -### dim(data[[i]]) <- c(dim(data[[i]]), '_margins_dim_' = 1) -### } -### } - .isolate <- function(data, margins, drop = FALSE) { - if (length(margins) > 0) { - margin_length <- lapply(dim(data), function(x) 1 : x) - margin_length[- margins] <- "" + for (i in 1:length(data)) { + if (length(margins[[i]]) > 0) { + dims <- dim(data[[i]]) + margins_inds <- 1:length(margins[[i]]) + length(target_dims[[i]]) + dim(data[[i]]) <- c(dims[-margins_inds], + '_margins_dim_' = prod(dims[margins_inds])) } else { - margin_length <- as.list(rep("", length(dim(data)))) + dim(data[[i]]) <- c(dim(data[[i]]), '_margins_dim_' = 1) } - margin_length <- expand.grid(margin_length, KEEP.OUT.ATTRS = FALSE, - stringsAsFactors = FALSE) - - eval(dim(environment()$data)) - structure(list(env = environment(), index = margin_length, - drop = drop, subs = as.name("[")), - class = c("indexed_array")) } + ###.isolate <- function(data, margins, drop = FALSE) { + ### if (length(margins) > 0) { + ### margin_length <- lapply(dim(data), function(x) 1 : x) + ### margin_length[- margins] <- "" + ### } else { + ### margin_length <- as.list(rep("", length(dim(data)))) + ### } + ### margin_length <- expand.grid(margin_length, KEEP.OUT.ATTRS = FALSE, + ### stringsAsFactors = FALSE) + + ### eval(dim(environment()$data)) + ### structure(list(env = environment(), index = margin_length, + ### drop = drop, subs = as.name("[")), + ### class = c("indexed_array")) + ###} input_margin_weights <- vector('list', length(data)) - iteration_input_dims <- vector('list', length(data)) - flat_data <- vector('list', length(data)) + ###iteration_input_dims <- vector('list', length(data)) + ###flat_data <- vector('list', length(data)) for (i in 1:length(data)) { - flat_data[[i]] <- .isolate(data[[i]], margins[[i]]) marg_sizes <- dim(data[[i]])[margins[[i]]] input_margin_weights[[i]] <- sapply(1:length(marg_sizes), function(k) prod(c(1, marg_sizes)[1:k])) - iteration_input_dims[[i]] <- dim(data[[i]])[target_dims[[i]]] + ###iteration_input_dims[[i]] <- dim(data[[i]])[target_dims[[i]]] + ###flat_data[[i]] <- .isolate(data[[i]], margins[[i]]) } # TODO: need to add progress bar @@ -462,11 +462,11 @@ Apply <- function(data, target_dims = NULL, fun, ..., names(first_marg_indices) <- names(mad) sub_arrays_of_results <- list() found_first_sub_result <- FALSE -### iteration_indices_to_take <- list() -### for (i in 1:length(data)) { -### iteration_indices_to_take[[i]] <- as.list(rep(TRUE, length(dim(data[[i]])))) -### } -### + iteration_indices_to_take <- list() + for (i in 1:length(data)) { + iteration_indices_to_take[[i]] <- as.list(rep(TRUE, length(dim(data[[i]])))) + } + add_one_multidim <- function(index, dims) { stop_iterating <- FALSE check_dim <- 1 @@ -495,22 +495,22 @@ Apply <- function(data, target_dims = NULL, fun, ..., input_margin_dim_index <- first_marg_indices[margins_names[[i]]] input_margin_dim_index <- 1 + sum((input_margin_dim_index - 1) * input_margin_weights[[i]]) -### iteration_indices_to_take[[i]][[length(dim(data[[i]]))]] <- input_margin_dim_index -### iteration_input[[i]] <- do.call('[', c(list(x = data[[i]]), -### iteration_indices_to_take[[i]], -### list(drop = FALSE))) -### num_dims <- length(dim(iteration_input[[i]])) -### if (num_dims > 1) { -### dim(iteration_input[[i]]) <- dim(iteration_input[[i]])[-length(dim(iteration_input[[i]]))] -### } else { -### dim(iteration_input[[i]]) <- NULL -### } - if (length(iteration_input_dims[[i]]) > 0) { - iteration_input[[i]] <- array(flat_data[[i]][[input_margin_dim_index]], - dim = iteration_input_dims[[i]]) + iteration_indices_to_take[[i]][[length(dim(data[[i]]))]] <- input_margin_dim_index + iteration_input[[i]] <- do.call('[', c(list(x = data[[i]]), + iteration_indices_to_take[[i]], + list(drop = FALSE))) + num_dims <- length(dim(iteration_input[[i]])) + if (num_dims > 1) { + dim(iteration_input[[i]]) <- dim(iteration_input[[i]])[-length(dim(iteration_input[[i]]))] } else { - iteration_input[[i]] <- as.vector(flat_data[[i]][[input_margin_dim_index]]) + dim(iteration_input[[i]]) <- NULL } + ###if (length(iteration_input_dims[[i]]) > 0) { + ### iteration_input[[i]] <- array(flat_data[[i]][[input_margin_dim_index]], + ### dim = iteration_input_dims[[i]]) + ###} else { + ### iteration_input[[i]] <- as.vector(flat_data[[i]][[input_margin_dim_index]]) + ###} } if (!is.null(mad)) { first_marg_indices <- add_one_multidim(first_marg_indices, mad) -- GitLab From fb9320b1e4b5bf6a03070ccfb8b1c90153509eca Mon Sep 17 00:00:00 2001 From: Nicolau Manubens Date: Sun, 11 Nov 2018 21:25:26 +0100 Subject: [PATCH 14/19] Bugfix + fix in CI. --- .gitlab-ci.yml | 4 ++-- R/Apply.R | 21 +++++++++++---------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 398a915..a7d3f89 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -5,6 +5,6 @@ build: stage: build script: - module load R - - R -e 'devtools::build()' - - R -e 'devtools::check()' + - R CMD build --resave-data . + - R CMD check --as-cran multiApply_*.tar.gz - R -e 'covr::package_coverage()' diff --git a/R/Apply.R b/R/Apply.R index 07b276d..98bdab0 100644 --- a/R/Apply.R +++ b/R/Apply.R @@ -401,6 +401,17 @@ Apply <- function(data, target_dims = NULL, fun, ..., chunk_sizes <- c(chunk_sizes, total_size %% chunk_size) } + input_margin_weights <- vector('list', length(data)) + ###iteration_input_dims <- vector('list', length(data)) + ###flat_data <- vector('list', length(data)) + for (i in 1:length(data)) { + marg_sizes <- dim(data[[i]])[margins[[i]]] + input_margin_weights[[i]] <- sapply(1:length(marg_sizes), + function(k) prod(c(1, marg_sizes)[1:k])) + ###iteration_input_dims[[i]] <- dim(data[[i]])[target_dims[[i]]] + ###flat_data[[i]] <- .isolate(data[[i]], margins[[i]]) + } + # The following few lines have an impact on memory footprint. # Flattening margin dimensions so that the iteration function can access # them easily. @@ -429,16 +440,6 @@ Apply <- function(data, target_dims = NULL, fun, ..., ### drop = drop, subs = as.name("[")), ### class = c("indexed_array")) ###} - input_margin_weights <- vector('list', length(data)) - ###iteration_input_dims <- vector('list', length(data)) - ###flat_data <- vector('list', length(data)) - for (i in 1:length(data)) { - marg_sizes <- dim(data[[i]])[margins[[i]]] - input_margin_weights[[i]] <- sapply(1:length(marg_sizes), - function(k) prod(c(1, marg_sizes)[1:k])) - ###iteration_input_dims[[i]] <- dim(data[[i]])[target_dims[[i]]] - ###flat_data[[i]] <- .isolate(data[[i]], margins[[i]]) - } # TODO: need to add progress bar # TODO: IF ONLY ONE INPUT ARRAY, MAKE USE OF apply. -- GitLab From 0d874c15065face73b29092ff70446de4cbbad97 Mon Sep 17 00:00:00 2001 From: Nicolau Manubens Date: Sun, 11 Nov 2018 22:05:32 +0100 Subject: [PATCH 15/19] Fixed memory footprint issues. --- R/Apply.R | 49 ++++++++++++++++++++++++++++++------------------- 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/R/Apply.R b/R/Apply.R index 98bdab0..659b6eb 100644 --- a/R/Apply.R +++ b/R/Apply.R @@ -415,16 +415,16 @@ Apply <- function(data, target_dims = NULL, fun, ..., # The following few lines have an impact on memory footprint. # Flattening margin dimensions so that the iteration function can access # them easily. - for (i in 1:length(data)) { - if (length(margins[[i]]) > 0) { - dims <- dim(data[[i]]) - margins_inds <- 1:length(margins[[i]]) + length(target_dims[[i]]) - dim(data[[i]]) <- c(dims[-margins_inds], - '_margins_dim_' = prod(dims[margins_inds])) - } else { - dim(data[[i]]) <- c(dim(data[[i]]), '_margins_dim_' = 1) - } - } + ######for (i in 1:length(data)) { + ###### if (length(margins[[i]]) > 0) { + ###### dims <- dim(data[[i]]) + ###### margins_inds <- 1:length(margins[[i]]) + length(target_dims[[i]]) + ###### dim(data[[i]]) <- c(dims[-margins_inds], + ###### '_margins_dim_' = prod(dims[margins_inds])) + ###### } else { + ###### dim(data[[i]]) <- c(dim(data[[i]]), '_margins_dim_' = 1) + ###### } + ######} ###.isolate <- function(data, margins, drop = FALSE) { ### if (length(margins) > 0) { ### margin_length <- lapply(dim(data), function(x) 1 : x) @@ -442,7 +442,6 @@ Apply <- function(data, target_dims = NULL, fun, ..., ###} # TODO: need to add progress bar - # TODO: IF ONLY ONE INPUT ARRAY, MAKE USE OF apply. splatted_f <- splat(fun) # For a selected use case, these are the timings: # - total: 17 s @@ -466,6 +465,7 @@ Apply <- function(data, target_dims = NULL, fun, ..., iteration_indices_to_take <- list() for (i in 1:length(data)) { iteration_indices_to_take[[i]] <- as.list(rep(TRUE, length(dim(data[[i]])))) + names(iteration_indices_to_take[[i]]) <- names(dim(data[[i]])) } add_one_multidim <- function(index, dims) { @@ -494,17 +494,28 @@ Apply <- function(data, target_dims = NULL, fun, ..., iteration_input <- list() for (i in 1:length(data)) { input_margin_dim_index <- first_marg_indices[margins_names[[i]]] - input_margin_dim_index <- 1 + sum((input_margin_dim_index - 1) * - input_margin_weights[[i]]) - iteration_indices_to_take[[i]][[length(dim(data[[i]]))]] <- input_margin_dim_index + ######input_margin_dim_index <- 1 + sum((input_margin_dim_index - 1) * + ###### input_margin_weights[[i]]) + ######iteration_indices_to_take[[i]][[length(dim(data[[i]]))]] <- input_margin_dim_index + iteration_indices_to_take[[i]][margins_names[[i]]] <- input_margin_dim_index iteration_input[[i]] <- do.call('[', c(list(x = data[[i]]), iteration_indices_to_take[[i]], list(drop = FALSE))) - num_dims <- length(dim(iteration_input[[i]])) - if (num_dims > 1) { - dim(iteration_input[[i]]) <- dim(iteration_input[[i]])[-length(dim(iteration_input[[i]]))] - } else { - dim(iteration_input[[i]]) <- NULL + ######num_dims <- length(dim(iteration_input[[i]])) + ######if (num_dims > 1) { + ###### dim(iteration_input[[i]]) <- dim(iteration_input[[i]])[-length(dim(iteration_input[[i]]))] + ######} else { + ###### dim(iteration_input[[i]]) <- NULL + ######} + num_margins <- length(margins_names[[i]]) + if (num_margins > 0) { + if (num_margins == length(dim(iteration_input[[i]]))) { + dim(iteration_input[[i]]) <- NULL + } else { + dims_to_remove <- 1:num_margins + length(target_dims[[i]]) + dim(iteration_input[[i]]) <- dim(iteration_input[[i]])[-dims_to_remove] + #if only one dim remains, make as.vector + } } ###if (length(iteration_input_dims[[i]]) > 0) { ### iteration_input[[i]] <- array(flat_data[[i]][[input_margin_dim_index]], -- GitLab From 2c48ec35957eb45ae6d7e25a6e11732af9de6fdd Mon Sep 17 00:00:00 2001 From: Nicolau Manubens Date: Sun, 11 Nov 2018 22:07:11 +0100 Subject: [PATCH 16/19] Removed comments with code of less efficient alternatives. --- R/Apply.R | 48 ------------------------------------------------ 1 file changed, 48 deletions(-) diff --git a/R/Apply.R b/R/Apply.R index 659b6eb..5dccf39 100644 --- a/R/Apply.R +++ b/R/Apply.R @@ -402,45 +402,12 @@ Apply <- function(data, target_dims = NULL, fun, ..., } input_margin_weights <- vector('list', length(data)) - ###iteration_input_dims <- vector('list', length(data)) - ###flat_data <- vector('list', length(data)) for (i in 1:length(data)) { marg_sizes <- dim(data[[i]])[margins[[i]]] input_margin_weights[[i]] <- sapply(1:length(marg_sizes), function(k) prod(c(1, marg_sizes)[1:k])) - ###iteration_input_dims[[i]] <- dim(data[[i]])[target_dims[[i]]] - ###flat_data[[i]] <- .isolate(data[[i]], margins[[i]]) } - # The following few lines have an impact on memory footprint. - # Flattening margin dimensions so that the iteration function can access - # them easily. - ######for (i in 1:length(data)) { - ###### if (length(margins[[i]]) > 0) { - ###### dims <- dim(data[[i]]) - ###### margins_inds <- 1:length(margins[[i]]) + length(target_dims[[i]]) - ###### dim(data[[i]]) <- c(dims[-margins_inds], - ###### '_margins_dim_' = prod(dims[margins_inds])) - ###### } else { - ###### dim(data[[i]]) <- c(dim(data[[i]]), '_margins_dim_' = 1) - ###### } - ######} - ###.isolate <- function(data, margins, drop = FALSE) { - ### if (length(margins) > 0) { - ### margin_length <- lapply(dim(data), function(x) 1 : x) - ### margin_length[- margins] <- "" - ### } else { - ### margin_length <- as.list(rep("", length(dim(data)))) - ### } - ### margin_length <- expand.grid(margin_length, KEEP.OUT.ATTRS = FALSE, - ### stringsAsFactors = FALSE) - - ### eval(dim(environment()$data)) - ### structure(list(env = environment(), index = margin_length, - ### drop = drop, subs = as.name("[")), - ### class = c("indexed_array")) - ###} - # TODO: need to add progress bar splatted_f <- splat(fun) # For a selected use case, these are the timings: @@ -494,19 +461,10 @@ Apply <- function(data, target_dims = NULL, fun, ..., iteration_input <- list() for (i in 1:length(data)) { input_margin_dim_index <- first_marg_indices[margins_names[[i]]] - ######input_margin_dim_index <- 1 + sum((input_margin_dim_index - 1) * - ###### input_margin_weights[[i]]) - ######iteration_indices_to_take[[i]][[length(dim(data[[i]]))]] <- input_margin_dim_index iteration_indices_to_take[[i]][margins_names[[i]]] <- input_margin_dim_index iteration_input[[i]] <- do.call('[', c(list(x = data[[i]]), iteration_indices_to_take[[i]], list(drop = FALSE))) - ######num_dims <- length(dim(iteration_input[[i]])) - ######if (num_dims > 1) { - ###### dim(iteration_input[[i]]) <- dim(iteration_input[[i]])[-length(dim(iteration_input[[i]]))] - ######} else { - ###### dim(iteration_input[[i]]) <- NULL - ######} num_margins <- length(margins_names[[i]]) if (num_margins > 0) { if (num_margins == length(dim(iteration_input[[i]]))) { @@ -517,12 +475,6 @@ Apply <- function(data, target_dims = NULL, fun, ..., #if only one dim remains, make as.vector } } - ###if (length(iteration_input_dims[[i]]) > 0) { - ### iteration_input[[i]] <- array(flat_data[[i]][[input_margin_dim_index]], - ### dim = iteration_input_dims[[i]]) - ###} else { - ### iteration_input[[i]] <- as.vector(flat_data[[i]][[input_margin_dim_index]]) - ###} } if (!is.null(mad)) { first_marg_indices <- add_one_multidim(first_marg_indices, mad) -- GitLab From 9551f474f0cd889217d974f6aeea334ea931a8a4 Mon Sep 17 00:00:00 2001 From: Nicolau Manubens Date: Mon, 12 Nov 2018 00:22:21 +0100 Subject: [PATCH 17/19] Updated documentation. --- DESCRIPTION | 7 ++++--- R/Apply.R | 21 +++++++++++---------- README.md | 37 ++++++++++++++++++++++++++++++++++--- man/Apply.Rd | 20 ++++++++++---------- 4 files changed, 59 insertions(+), 26 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 22220b5..d514a06 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,11 +1,12 @@ Package: multiApply Title: Apply Functions to Multiple Multidimensional Arrays or Vectors -Version: 1.0.0 +Version: 2.0.0 Authors@R: c( person("BSC-CNS", role = c("aut", "cph")), person("Nicolau", "Manubens", , "nicolau.manubens@bsc.es", role = "aut"), - person("Alasdair", "Hunter", , "alasdair.hunter@bsc.es", role = c("aut", "cre"))) -Description: The base apply function and its variants, as well as the related functions in the 'plyr' package, typically apply user-defined functions to a single argument (or a list of vectorized arguments in the case of mapply). The 'multiApply' package extends this paradigm to functions taking one or a list of multiple unidimensional or multidimensional arguments (or combinations thereof) as input, which can have different numbers of dimensions as well as different dimension lengths, and returning one or a list of unidimensional or multidimensional arrays as output. In contrast to apply and variants, this package suggests the use of 'target dimensions' as opposite to the 'margins' for specifying the dimensions relevant to the function to be applied. Also, two remarkable differences are the support for functions returning multiple array outputs and the transparent use of multi-core. + person("Alasdair", "Hunter", , "alasdair.hunter@bsc.es", role = "aut"), + person("Nuria", "Perez", , "nuria.perez@bsc.es", role = "cre")) +Description: The base apply function and its variants, as well as the related functions in the 'plyr' package, typically apply user-defined functions to a single argument (or a list of vectorized arguments in the case of mapply). The 'multiApply' package extends this paradigm to efficiently apply functions taking one or a list of multiple unidimensional or multidimensional arguments (or combinations thereof) as input, which can have different numbers of dimensions as well as different dimension lengths, and returning one or a list of unidimensional or multidimensional arrays as output. This saves development time by preventing the R user from writing error-prone and memory-unefficient loops dealing with multiple complex arrays. In contrast to apply and variants, this package suggests the use of 'target dimensions' as opposite to the 'margins' for specifying the dimensions relevant to the function to be applied. Also, two remarkable features of multiApply are the support for functions returning multiple array outputs and the transparent use of multi-core. Depends: R (>= 3.2.0) Imports: diff --git a/R/Apply.R b/R/Apply.R index 5dccf39..4710cb3 100644 --- a/R/Apply.R +++ b/R/Apply.R @@ -1,17 +1,18 @@ -#' Wrapper for Applying Atomic Functions to Arrays. +#' Apply Functions to Multiple Multidimensional Arrays or Vectors #' -#' This wrapper applies a given function, which takes N [multi-dimensional] arrays as inputs (which may have different numbers of dimensions and dimension lengths), and applies it to a list of N [multi-dimensional] arrays with at least as many dimensions as expected by the given function. The user can specify which dimensions of each array (or matrix) the function is to be applied over with the \code{margins} or \code{target_dims} option. A user can apply a function that receives (in addition to other helper parameters) 1 or more arrays as input, each with a different number of dimensions, and returns any number of multidimensional arrays. The target dimensions can be specified by their names. It is recommended to use this wrapper with multidimensional arrays with named dimensions. -#' @param data A single object (vector, matrix or array) or a list of objects. They must be in the same order as expected by fun. -#' @param target_dims List of vectors containing the dimensions to be input into fun for each of the objects in the data. These vectors can contain either integers specifying the dimension position, or characters corresponding to the dimension names. This parameter is mandatory if margins is not specified. If both margins and target_dims are specified, margins takes priority over target_dims. -#' @param fun Function to be applied to the arrays. -#' @param ... Additional arguments to be used in the fun. +#' This function efficiently applies a given function, which takes N vectors or multi-dimensional arrays as inputs (which may have different numbers of dimensions and dimension lengths), and applies it to a list of N vectors or multi-dimensional arrays with at least as many dimensions as expected by the given function. The user can specify which dimensions of each array the function is to be applied over with the \code{margins} or \code{target_dims} parameters. The function to be applied can receive other helper parameters and return any number of numeric vectors or multidimensional arrays. The target dimensions or margins can be specified by their names, as long as the inputs are provided with dimension names (recommended). This function can also use multi-core in a transparent way if requested via the \code{ncores} parameter.\cr\crThe following steps help to understand how \code{Apply} works:\cr\cr - The function receives N arrays with Dn dimensions each.\cr - The user specifies, for each of the arrays, which of its dimensions are 'target' dimensions (dimensions which the function provided in 'fun' operates with) and which are 'margins' (dimensions to be looped over).\cr - \code{Apply} will generate an array with as many dimensions as margins in all of the input arrays. If a margin is repeated across different inputs, it will appear only once in the resulting array.\cr - For each element of this resulting array, the function provided in the parameter'fun' is applied to the corresponding sub-arrays in 'data'.\cr - If the function returns a vector or a multidimensional array, the additional dimensions will be prepended to the resulting array (in left-most positions).\cr - If the provided function returns more than one vector or array, the process above is carried out for each of the outputs, resulting in a list with multiple arrays, each with the combination of all target dimensions (at the right-most positions) and resulting dimensions (at the left-most positions). +#' +#' @param data One or a list of numeric object (vector, matrix or array). They must be in the same order as expected by the function provided in the parameter 'fun'. The dimensions do not necessarily have to be ordered. If the 'target_dims' require a different order than the provided, \code{Apply} will automatically reorder the dimensions as needed. +#' @param target_dims One or a list of vectors (or NULLs) containing the dimensions to be input into fun for each of the objects in the data. If a single vector of target dimensions is specified and multiple inputs are provided in 'data, then the single set of target dimensions is re-used for all of the inputs. These vectors can contain either integers specifying the position of the dimensions, or character strings corresponding to the dimension names. This parameter is mandatory if 'margins' are not specified. If both 'margins' and 'target_dims' are specified, 'margins' takes priority. +#' @param fun Function to be applied to the arrays. Must receive as many inputs as provided in 'data', each with as many dimensions as specified in 'target_dims' or as the total number of dimensions in 'data' minus the ones specified in 'margins'. The function can receive other additional fixed parameters (see parameter '...' of \code{Apply}). The function can return one or a list of numeric vectors or multidimensional arrays, optionally with dimension names which will be propagated to the final result. The returned list can optionally be named, with a name for each output, which will be propagated to the resulting array. The function can optionally be provided with the attributes 'target_dims' and 'output_dims'. In that case, the corresponding parameters of \code{Apply} do not need to be provided. The function can expect named dimensions for each of its inputs, in the same order as specified in 'target_dims' or, if no 'target_dims' have been provided, in the same order as provided in 'data'. +#' @param ... Additional fixed arguments expected by the function provided in the parameter 'fun'. #' @param output_dims Optional list of vectors containing the names of the dimensions to be output from the fun for each of the objects it returns (or a single vector if the function has only one output). -#' @param margins List of vectors containing the margins for the input objects to be split by. Or, if there is a single vector of margins specified and a list of objects in data, then the single set of margins is applied over all objects. These vectors can contain either integers specifying the dimension position, or characters corresponding to the dimension names. If both margins and target_dims are specified, margins takes priority over target_dims. +#' @param margins One or a list of vectors (or NULLs) containing the 'margin' dimensions to be looped over for each input in 'data'. If a single vector of margins is specified and multiple inputs are provided in 'data', then the single set of margins is re-used for all of the inputs. These vectors can contain either integers specifying the position of the margins, or character strings corresponding to the dimension names. If both 'margins' and 'target_dims' are specified, 'margins' takes priority. #' @param guess_dim_names Whether to automatically guess missing dimension names for dimensions of equal length across different inputs in 'data' with a warning (TRUE; default), or to crash whenever unnamed dimensions of equa length are identified across different inputs (FALSE). -#' @param ncores The number of multicore threads to use for parallel computation. +#' @param ncores The number of parallel processes to spawn for the use for parallel computation in multiple cores. #' @param split_factor Factor telling to which degree the input data should be split into smaller pieces to be processed by the available cores. By default (split_factor = 1) the data is split into 4 pieces for each of the cores (as specified in ncores). A split_factor of 2 will result in 8 pieces for each of the cores, and so on. The special value 'greatest' will split the input data into as many pieces as possible. -#' @details When using a single object as input, Apply is almost identical to the apply function. For multiple input objects, the output array will have dimensions equal to the dimensions specified in 'margins'. -#' @return List of arrays or matrices or vectors resulting from applying fun to data. +#' @details When using a single object as input, Apply is almost identical to the apply function (as fast or slightly slower in some cases; with equal or improved -smaller- memory footprint). +#' @return List of arrays or matrices or vectors resulting from applying 'fun' to 'data'. #' @references Wickham, H (2011), The Split-Apply-Combine Strategy for Data Analysis, Journal of Statistical Software. #' @export #' @examples diff --git a/README.md b/README.md index c3466a5..df71716 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,39 @@ ## multiApply [![build status](https://earth.bsc.es/gitlab/ces/multiApply/badges/master/build.svg)](https://earth.bsc.es/gitlab/ces/multiApply/commits/master) [![CRAN version](http://www.r-pkg.org/badges/version/multiApply)](https://cran.r-project.org/package=multiApply) [![coverage report](https://earth.bsc.es/gitlab/ces/multiApply/badges/master/coverage.svg)](https://earth.bsc.es/gitlab/ces/multiApply/commits/master) [![License: LGPL v3](https://img.shields.io/badge/License-LGPL%20v3-blue.svg)](https://www.gnu.org/licenses/lgpl-3.0) [![CRAN RStudio Downloads](https://cranlogs.r-pkg.org/badges/multiApply)](https://cran.rstudio.com/web/packages/multiApply/index.html) -This package extends the apply and plyr families of functions to applications which involve the use of multiple arrays as input, and is useful to apply a function taking multiple numeric objects as input across multiple multi-dimensional arrays. +This package includes the function `Apply` as its only function. It extends the `apply` function to applications in which a function needs to be applied simultaneously over multiple input arrays. Although this can be done manually with for loops and calls to the base apply function, in many cases it can be a challenging task which can very easily result in error-prone or memory-unefficient code. -This is especially useful for climate data related applications, where data is often distributed across multiple arrays with different dimensions (e.g experimental array 1, experimental array 2 and the observations). The `multiApply::Apply` function reduces the need to write loops for every application. +A very simple example follows showing the kind of situation where `Apply` can be useful: imagine you have two arrays, each containing five 2x2 matrices, and you want to perform the multiplication of each of the five pairs of matrices. Next, one of the best ways to do this with base R: + +```r +library(plyr) +library(abind) + +A <- array(1:20, c(5, 2, 2)) +B <- array(1:20, c(5, 2, 2)) + +D <- aaply(X = abind(A, B, along = 4), + MARGINS = 1, + FUN = function(x) x[,,1] %*% x[,,2]) +``` + +Although it is not excessively complex, the choosen example is very simple and the complexity would increase as the function to apply required additional dimensions or inputs, and would be unapplicable if multiple outputs were to be returned. In addition, the function to apply (matrix multiplication) had to be redefined for this particular case (multiplication of the first matrix by the second). + +Next, an example of how to reach the same results using `Apply`: + +```r +library(multiApply) + +A <- array(1:20, c(5, 2, 2)) +B <- array(1:20, c(5, 2, 2)) + +D <- Apply(data = list(A, B), + target_dims = c(2, 3), + fun = "%*%")$output1 +``` + +This solution takes half the time to complete, and is cleaner and extensible to functions receiving any number of inputs with any number of dimensions, or returning any number of outputs. Although the peak RAM usage (as measured with `peakRAM`) of both solutions in this example is about the same, it is challenging to avoid memory duplications when using custom code in more complex applications, and can usually require hours of dedication. `Apply` scales well to large inputs and has been designed to be fast and avoid memory duplications. + +In contrast to `apply` and variants, this package suggests the use of 'target dimensions' as opposite to the 'margins' for specifying the dimensions relevant to the function to be applied. Additionally, it supports functions returning multiple vector or arrays, and can transparently uses multi-core. ### Installation @@ -45,7 +76,7 @@ income <- Apply(data = list(sales_amount, sales_price), target_dims = list(c('item', 'day'), 'item'), income_function) -dim(income[[1]]) +dim(income$output1) # store # 5 ``` diff --git a/man/Apply.Rd b/man/Apply.Rd index 592c6af..5e7b9be 100644 --- a/man/Apply.Rd +++ b/man/Apply.Rd @@ -2,39 +2,39 @@ % Please edit documentation in R/Apply.R \name{Apply} \alias{Apply} -\title{Wrapper for Applying Atomic Functions to Arrays.} +\title{Apply Functions to Multiple Multidimensional Arrays or Vectors} \usage{ Apply(data, target_dims = NULL, fun, ..., output_dims = NULL, margins = NULL, guess_dim_names = TRUE, ncores = NULL, split_factor = 1) } \arguments{ -\item{data}{A single object (vector, matrix or array) or a list of objects. They must be in the same order as expected by fun.} +\item{data}{One or a list of numeric object (vector, matrix or array). They must be in the same order as expected by the function provided in the parameter 'fun'. The dimensions do not necessarily have to be ordered. If the 'target_dims' require a different order than the provided, \code{Apply} will automatically reorder the dimensions as needed.} -\item{target_dims}{List of vectors containing the dimensions to be input into fun for each of the objects in the data. These vectors can contain either integers specifying the dimension position, or characters corresponding to the dimension names. This parameter is mandatory if margins is not specified. If both margins and target_dims are specified, margins takes priority over target_dims.} +\item{target_dims}{One or a list of vectors (or NULLs) containing the dimensions to be input into fun for each of the objects in the data. If a single vector of target dimensions is specified and multiple inputs are provided in 'data, then the single set of target dimensions is re-used for all of the inputs. These vectors can contain either integers specifying the position of the dimensions, or character strings corresponding to the dimension names. This parameter is mandatory if 'margins' are not specified. If both 'margins' and 'target_dims' are specified, 'margins' takes priority.} -\item{fun}{Function to be applied to the arrays.} +\item{fun}{Function to be applied to the arrays. Must receive as many inputs as provided in 'data', each with as many dimensions as specified in 'target_dims' or as the total number of dimensions in 'data' minus the ones specified in 'margins'. The function can receive other additional fixed parameters (see parameter '...' of \code{Apply}). The function can return one or a list of numeric vectors or multidimensional arrays, optionally with dimension names which will be propagated to the final result. The returned list can optionally be named, with a name for each output, which will be propagated to the resulting array. The function can optionally be provided with the attributes 'target_dims' and 'output_dims'. In that case, the corresponding parameters of \code{Apply} do not need to be provided. The function can expect named dimensions for each of its inputs, in the same order as specified in 'target_dims' or, if no 'target_dims' have been provided, in the same order as provided in 'data'.} -\item{...}{Additional arguments to be used in the fun.} +\item{...}{Additional fixed arguments expected by the function provided in the parameter 'fun'.} \item{output_dims}{Optional list of vectors containing the names of the dimensions to be output from the fun for each of the objects it returns (or a single vector if the function has only one output).} -\item{margins}{List of vectors containing the margins for the input objects to be split by. Or, if there is a single vector of margins specified and a list of objects in data, then the single set of margins is applied over all objects. These vectors can contain either integers specifying the dimension position, or characters corresponding to the dimension names. If both margins and target_dims are specified, margins takes priority over target_dims.} +\item{margins}{One or a list of vectors (or NULLs) containing the 'margin' dimensions to be looped over for each input in 'data'. If a single vector of margins is specified and multiple inputs are provided in 'data', then the single set of margins is re-used for all of the inputs. These vectors can contain either integers specifying the position of the margins, or character strings corresponding to the dimension names. If both 'margins' and 'target_dims' are specified, 'margins' takes priority.} \item{guess_dim_names}{Whether to automatically guess missing dimension names for dimensions of equal length across different inputs in 'data' with a warning (TRUE; default), or to crash whenever unnamed dimensions of equa length are identified across different inputs (FALSE).} -\item{ncores}{The number of multicore threads to use for parallel computation.} +\item{ncores}{The number of parallel processes to spawn for the use for parallel computation in multiple cores.} \item{split_factor}{Factor telling to which degree the input data should be split into smaller pieces to be processed by the available cores. By default (split_factor = 1) the data is split into 4 pieces for each of the cores (as specified in ncores). A split_factor of 2 will result in 8 pieces for each of the cores, and so on. The special value 'greatest' will split the input data into as many pieces as possible.} } \value{ -List of arrays or matrices or vectors resulting from applying fun to data. +List of arrays or matrices or vectors resulting from applying 'fun' to 'data'. } \description{ -This wrapper applies a given function, which takes N [multi-dimensional] arrays as inputs (which may have different numbers of dimensions and dimension lengths), and applies it to a list of N [multi-dimensional] arrays with at least as many dimensions as expected by the given function. The user can specify which dimensions of each array (or matrix) the function is to be applied over with the \code{margins} or \code{target_dims} option. A user can apply a function that receives (in addition to other helper parameters) 1 or more arrays as input, each with a different number of dimensions, and returns any number of multidimensional arrays. The target dimensions can be specified by their names. It is recommended to use this wrapper with multidimensional arrays with named dimensions. +This function efficiently applies a given function, which takes N vectors or multi-dimensional arrays as inputs (which may have different numbers of dimensions and dimension lengths), and applies it to a list of N vectors or multi-dimensional arrays with at least as many dimensions as expected by the given function. The user can specify which dimensions of each array the function is to be applied over with the \code{margins} or \code{target_dims} parameters. The function to be applied can receive other helper parameters and return any number of numeric vectors or multidimensional arrays. The target dimensions or margins can be specified by their names, as long as the inputs are provided with dimension names (recommended). This function can also use multi-core in a transparent way if requested via the \code{ncores} parameter.\cr\crThe following steps help to understand how \code{Apply} works:\cr\cr - The function receives N arrays with Dn dimensions each.\cr - The user specifies, for each of the arrays, which of its dimensions are 'target' dimensions (dimensions which the function provided in 'fun' operates with) and which are 'margins' (dimensions to be looped over).\cr - \code{Apply} will generate an array with as many dimensions as margins in all of the input arrays. If a margin is repeated across different inputs, it will appear only once in the resulting array.\cr - For each element of this resulting array, the function provided in the parameter'fun' is applied to the corresponding sub-arrays in 'data'.\cr - If the function returns a vector or a multidimensional array, the additional dimensions will be prepended to the resulting array (in left-most positions).\cr - If the provided function returns more than one vector or array, the process above is carried out for each of the outputs, resulting in a list with multiple arrays, each with the combination of all target dimensions (at the right-most positions) and resulting dimensions (at the left-most positions). } \details{ -When using a single object as input, Apply is almost identical to the apply function. For multiple input objects, the output array will have dimensions equal to the dimensions specified in 'margins'. +When using a single object as input, Apply is almost identical to the apply function (as fast or slightly slower in some cases; with equal or improved -smaller- memory footprint). } \examples{ #Change in the rate of exceedance for two arrays, with different -- GitLab From 8cf2da9a0ccc6c66d14022fc82a69a099c213277 Mon Sep 17 00:00:00 2001 From: Nicolau Manubens Date: Mon, 12 Nov 2018 00:26:26 +0100 Subject: [PATCH 18/19] Small documentation updates. --- R/Apply.R | 2 +- man/Apply.Rd | 2 +- multiApply-manual.pdf | Bin 68100 -> 72043 bytes 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/R/Apply.R b/R/Apply.R index 4710cb3..335d437 100644 --- a/R/Apply.R +++ b/R/Apply.R @@ -1,6 +1,6 @@ #' Apply Functions to Multiple Multidimensional Arrays or Vectors #' -#' This function efficiently applies a given function, which takes N vectors or multi-dimensional arrays as inputs (which may have different numbers of dimensions and dimension lengths), and applies it to a list of N vectors or multi-dimensional arrays with at least as many dimensions as expected by the given function. The user can specify which dimensions of each array the function is to be applied over with the \code{margins} or \code{target_dims} parameters. The function to be applied can receive other helper parameters and return any number of numeric vectors or multidimensional arrays. The target dimensions or margins can be specified by their names, as long as the inputs are provided with dimension names (recommended). This function can also use multi-core in a transparent way if requested via the \code{ncores} parameter.\cr\crThe following steps help to understand how \code{Apply} works:\cr\cr - The function receives N arrays with Dn dimensions each.\cr - The user specifies, for each of the arrays, which of its dimensions are 'target' dimensions (dimensions which the function provided in 'fun' operates with) and which are 'margins' (dimensions to be looped over).\cr - \code{Apply} will generate an array with as many dimensions as margins in all of the input arrays. If a margin is repeated across different inputs, it will appear only once in the resulting array.\cr - For each element of this resulting array, the function provided in the parameter'fun' is applied to the corresponding sub-arrays in 'data'.\cr - If the function returns a vector or a multidimensional array, the additional dimensions will be prepended to the resulting array (in left-most positions).\cr - If the provided function returns more than one vector or array, the process above is carried out for each of the outputs, resulting in a list with multiple arrays, each with the combination of all target dimensions (at the right-most positions) and resulting dimensions (at the left-most positions). +#' This function efficiently applies a given function, which takes N vectors or multi-dimensional arrays as inputs (which may have different numbers of dimensions and dimension lengths), and applies it to a list of N vectors or multi-dimensional arrays with at least as many dimensions as expected by the given function. The user can specify which dimensions of each array the function is to be applied over with the \code{margins} or \code{target_dims} parameters. The function to be applied can receive other helper parameters and return any number of numeric vectors or multidimensional arrays. The target dimensions or margins can be specified by their names, as long as the inputs are provided with dimension names (recommended). This function can also use multi-core in a transparent way if requested via the \code{ncores} parameter.\cr\cr The following steps help to understand how \code{Apply} works:\cr\cr - The function receives N arrays with Dn dimensions each.\cr - The user specifies, for each of the arrays, which of its dimensions are 'target' dimensions (dimensions which the function provided in 'fun' operates with) and which are 'margins' (dimensions to be looped over).\cr - \code{Apply} will generate an array with as many dimensions as margins in all of the input arrays. If a margin is repeated across different inputs, it will appear only once in the resulting array.\cr - For each element of this resulting array, the function provided in the parameter'fun' is applied to the corresponding sub-arrays in 'data'.\cr - If the function returns a vector or a multidimensional array, the additional dimensions will be prepended to the resulting array (in left-most positions).\cr - If the provided function returns more than one vector or array, the process above is carried out for each of the outputs, resulting in a list with multiple arrays, each with the combination of all target dimensions (at the right-most positions) and resulting dimensions (at the left-most positions). #' #' @param data One or a list of numeric object (vector, matrix or array). They must be in the same order as expected by the function provided in the parameter 'fun'. The dimensions do not necessarily have to be ordered. If the 'target_dims' require a different order than the provided, \code{Apply} will automatically reorder the dimensions as needed. #' @param target_dims One or a list of vectors (or NULLs) containing the dimensions to be input into fun for each of the objects in the data. If a single vector of target dimensions is specified and multiple inputs are provided in 'data, then the single set of target dimensions is re-used for all of the inputs. These vectors can contain either integers specifying the position of the dimensions, or character strings corresponding to the dimension names. This parameter is mandatory if 'margins' are not specified. If both 'margins' and 'target_dims' are specified, 'margins' takes priority. diff --git a/man/Apply.Rd b/man/Apply.Rd index 5e7b9be..4fe37e9 100644 --- a/man/Apply.Rd +++ b/man/Apply.Rd @@ -31,7 +31,7 @@ Apply(data, target_dims = NULL, fun, ..., output_dims = NULL, List of arrays or matrices or vectors resulting from applying 'fun' to 'data'. } \description{ -This function efficiently applies a given function, which takes N vectors or multi-dimensional arrays as inputs (which may have different numbers of dimensions and dimension lengths), and applies it to a list of N vectors or multi-dimensional arrays with at least as many dimensions as expected by the given function. The user can specify which dimensions of each array the function is to be applied over with the \code{margins} or \code{target_dims} parameters. The function to be applied can receive other helper parameters and return any number of numeric vectors or multidimensional arrays. The target dimensions or margins can be specified by their names, as long as the inputs are provided with dimension names (recommended). This function can also use multi-core in a transparent way if requested via the \code{ncores} parameter.\cr\crThe following steps help to understand how \code{Apply} works:\cr\cr - The function receives N arrays with Dn dimensions each.\cr - The user specifies, for each of the arrays, which of its dimensions are 'target' dimensions (dimensions which the function provided in 'fun' operates with) and which are 'margins' (dimensions to be looped over).\cr - \code{Apply} will generate an array with as many dimensions as margins in all of the input arrays. If a margin is repeated across different inputs, it will appear only once in the resulting array.\cr - For each element of this resulting array, the function provided in the parameter'fun' is applied to the corresponding sub-arrays in 'data'.\cr - If the function returns a vector or a multidimensional array, the additional dimensions will be prepended to the resulting array (in left-most positions).\cr - If the provided function returns more than one vector or array, the process above is carried out for each of the outputs, resulting in a list with multiple arrays, each with the combination of all target dimensions (at the right-most positions) and resulting dimensions (at the left-most positions). +This function efficiently applies a given function, which takes N vectors or multi-dimensional arrays as inputs (which may have different numbers of dimensions and dimension lengths), and applies it to a list of N vectors or multi-dimensional arrays with at least as many dimensions as expected by the given function. The user can specify which dimensions of each array the function is to be applied over with the \code{margins} or \code{target_dims} parameters. The function to be applied can receive other helper parameters and return any number of numeric vectors or multidimensional arrays. The target dimensions or margins can be specified by their names, as long as the inputs are provided with dimension names (recommended). This function can also use multi-core in a transparent way if requested via the \code{ncores} parameter.\cr\cr The following steps help to understand how \code{Apply} works:\cr\cr - The function receives N arrays with Dn dimensions each.\cr - The user specifies, for each of the arrays, which of its dimensions are 'target' dimensions (dimensions which the function provided in 'fun' operates with) and which are 'margins' (dimensions to be looped over).\cr - \code{Apply} will generate an array with as many dimensions as margins in all of the input arrays. If a margin is repeated across different inputs, it will appear only once in the resulting array.\cr - For each element of this resulting array, the function provided in the parameter'fun' is applied to the corresponding sub-arrays in 'data'.\cr - If the function returns a vector or a multidimensional array, the additional dimensions will be prepended to the resulting array (in left-most positions).\cr - If the provided function returns more than one vector or array, the process above is carried out for each of the outputs, resulting in a list with multiple arrays, each with the combination of all target dimensions (at the right-most positions) and resulting dimensions (at the left-most positions). } \details{ When using a single object as input, Apply is almost identical to the apply function (as fast or slightly slower in some cases; with equal or improved -smaller- memory footprint). diff --git a/multiApply-manual.pdf b/multiApply-manual.pdf index bf1c86e977e001ee0b102d7b749e031b158ac0d0..a1f26396885bcaa03cdf279c9f38176bab2d08b2 100644 GIT binary patch delta 62223 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

Ai`d0dQ00h-PVF+wh=44z_n@CK5gPK+bnedXh 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 C?5Mr~ delta 58329 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

y+5qs|BEP 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 -- GitLab From 8a6682c5bc42dab876e9285d237a117c4463d6e2 Mon Sep 17 00:00:00 2001 From: Nicolau Manubens Date: Mon, 19 Nov 2018 17:29:25 +0100 Subject: [PATCH 19/19] Updated documentation. --- DESCRIPTION | 2 +- R/Apply.R | 9 +++-- README.md | 19 ++++++--- multiApply-manual.pdf | Bin 72043 -> 72238 bytes tests/testthat/test-use-cases.R | 68 ++++++++++++++++++++++++++++++++ 5 files changed, 89 insertions(+), 9 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index d514a06..31dcd94 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -6,7 +6,7 @@ Authors@R: c( person("Nicolau", "Manubens", , "nicolau.manubens@bsc.es", role = "aut"), person("Alasdair", "Hunter", , "alasdair.hunter@bsc.es", role = "aut"), person("Nuria", "Perez", , "nuria.perez@bsc.es", role = "cre")) -Description: The base apply function and its variants, as well as the related functions in the 'plyr' package, typically apply user-defined functions to a single argument (or a list of vectorized arguments in the case of mapply). The 'multiApply' package extends this paradigm to efficiently apply functions taking one or a list of multiple unidimensional or multidimensional arguments (or combinations thereof) as input, which can have different numbers of dimensions as well as different dimension lengths, and returning one or a list of unidimensional or multidimensional arrays as output. This saves development time by preventing the R user from writing error-prone and memory-unefficient loops dealing with multiple complex arrays. In contrast to apply and variants, this package suggests the use of 'target dimensions' as opposite to the 'margins' for specifying the dimensions relevant to the function to be applied. Also, two remarkable features of multiApply are the support for functions returning multiple array outputs and the transparent use of multi-core. +Description: The base apply function and its variants, as well as the related functions in the 'plyr' package, typically apply user-defined functions to a single argument (or a list of vectorized arguments in the case of mapply). The 'multiApply' package extends this paradigm with its only function, Apply, which efficiently applies functions taking one or a list of multiple unidimensional or multidimensional numeric arrays (or combinations thereof) as input. The input arrays can have different numbers of dimensions as well as different dimension lengths, and the applied function can return one or a list of unidimensional or multidimensional arrays as output. This saves development time by preventing the R user from writing often error-prone and memory-unefficient loops dealing with multiple complex arrays. Also, a remarkable feature of Apply is the transparent use of multi-core through its parameter 'ncores'. In contrast to the base apply function, this package suggests the use of 'target dimensions' as opposite to the 'margins' for specifying the dimensions relevant to the function to be applied. Depends: R (>= 3.2.0) Imports: diff --git a/R/Apply.R b/R/Apply.R index 335d437..876c8f4 100644 --- a/R/Apply.R +++ b/R/Apply.R @@ -101,14 +101,17 @@ Apply <- function(data, target_dims = NULL, fun, ..., "found across different inputs in 'data'. Please check ", "carefully the assumed names below are correct, or provide ", "dimension names for safety, or disable the parameter ", - "'guess_dimension_names'.", dim_names_string) + "'guess_dim_names'.", dim_names_string) } # Check fun if (is.character(fun)) { - try({fun <- get(fun)}, silent = TRUE) + fun_name <- fun + err <- try({ + fun <- get(fun) + }, silent = TRUE) if (!is.function(fun)) { - stop("Could not find the function '", fun, "'.") + stop("Could not find the function '", fun_name, "'.") } } if (!is.function(fun)) { diff --git a/README.md b/README.md index df71716..99d3eab 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ ## multiApply [![build status](https://earth.bsc.es/gitlab/ces/multiApply/badges/master/build.svg)](https://earth.bsc.es/gitlab/ces/multiApply/commits/master) [![CRAN version](http://www.r-pkg.org/badges/version/multiApply)](https://cran.r-project.org/package=multiApply) [![coverage report](https://earth.bsc.es/gitlab/ces/multiApply/badges/master/coverage.svg)](https://earth.bsc.es/gitlab/ces/multiApply/commits/master) [![License: LGPL v3](https://img.shields.io/badge/License-LGPL%20v3-blue.svg)](https://www.gnu.org/licenses/lgpl-3.0) [![CRAN RStudio Downloads](https://cranlogs.r-pkg.org/badges/multiApply)](https://cran.rstudio.com/web/packages/multiApply/index.html) -This package includes the function `Apply` as its only function. It extends the `apply` function to applications in which a function needs to be applied simultaneously over multiple input arrays. Although this can be done manually with for loops and calls to the base apply function, in many cases it can be a challenging task which can very easily result in error-prone or memory-unefficient code. +This package includes the function `Apply` as its only function. It extends the `apply` function to applications in which a function needs to be applied simultaneously over multiple input arrays. Although this can be done manually with for loops and calls to the base `apply` function, it can often be a challenging task which can easily result in error-prone or memory-unefficient code. -A very simple example follows showing the kind of situation where `Apply` can be useful: imagine you have two arrays, each containing five 2x2 matrices, and you want to perform the multiplication of each of the five pairs of matrices. Next, one of the best ways to do this with base R: +A very simple example follows showing the kind of situation where `Apply` can be useful: imagine you have two arrays, each containing five 2x2 matrices, and you want to perform the multiplication of each of the five pairs of matrices. Next, one of the best ways to do this with base R (plus some helper libraries): ```r library(plyr) @@ -16,7 +16,7 @@ D <- aaply(X = abind(A, B, along = 4), FUN = function(x) x[,,1] %*% x[,,2]) ``` -Although it is not excessively complex, the choosen example is very simple and the complexity would increase as the function to apply required additional dimensions or inputs, and would be unapplicable if multiple outputs were to be returned. In addition, the function to apply (matrix multiplication) had to be redefined for this particular case (multiplication of the first matrix by the second). +Since the choosen use case is very simple, this solution is not excessively complex, but the complexity would increase as the function to apply required additional dimensions or inputs, and would be unapplicable if multiple outputs were to be returned. In addition, the function to apply (matrix multiplication) had to be redefined for this particular case (multiplication of the first matrix along the third dimension by the second along the third dimension). Next, an example of how to reach the same results using `Apply`: @@ -31,7 +31,16 @@ D <- Apply(data = list(A, B), fun = "%*%")$output1 ``` -This solution takes half the time to complete, and is cleaner and extensible to functions receiving any number of inputs with any number of dimensions, or returning any number of outputs. Although the peak RAM usage (as measured with `peakRAM`) of both solutions in this example is about the same, it is challenging to avoid memory duplications when using custom code in more complex applications, and can usually require hours of dedication. `Apply` scales well to large inputs and has been designed to be fast and avoid memory duplications. +This solution takes half the time to complete (as measured with `microbenchmark` with inputs of different sizes), and is cleaner and extensible to functions receiving any number of inputs with any number of dimensions, or returning any number of outputs. Although the peak RAM usage (as measured with `peakRAM`) of both solutions in this example is about the same, it is challenging to avoid memory duplications when using custom code in more complex applications, and can usually require hours of dedication. `Apply` scales well to large inputs and has been designed to be fast and avoid memory duplications. + +Additionally, multi-code computation can be enabled via the `ncores` parameter, as shown next. Although in this minimalist example using multi-core would make the execution slower, in applications where the inputs are larger the wall-clock time is reduced dramatically. + +```r +D <- Apply(data = list(A, B), + target_dims = c(2, 3), + fun = "%*%", + ncores = 4)$output1 +``` In contrast to `apply` and variants, this package suggests the use of 'target dimensions' as opposite to the 'margins' for specifying the dimensions relevant to the function to be applied. Additionally, it supports functions returning multiple vector or arrays, and can transparently uses multi-core. @@ -44,7 +53,7 @@ install.packages('multiApply') library(multiApply) ``` -Also, you can install the latest stable version from this GitHub repository as follows: +Also, you can install the latest stable version from the GitHub repository as follows: ```r devtools::install_git('https://earth.bsc.es/gitlab/ces/multiApply') diff --git a/multiApply-manual.pdf b/multiApply-manual.pdf index a1f26396885bcaa03cdf279c9f38176bab2d08b2..c1ab41cb14670fb7d596548390f3070814cc0963 100644 GIT binary patch delta 21752 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 delta 21532 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

Z+V16L&40N7+h;Ylq!BcmC(4;q4p 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%0A3)ntd{H~yzpa6M8Hx}J z{{Mdkr~z00Am^@n5tGPdsJmQ5t2Fa5Z-XHsI`B$nN4p|m3zY?*bv;Pe>^XUs0=FdT zMRr%!vC`=Pv)|7oP$vtwU%KC6sr@N%!;6?SFBeW}7n_+dDp6aQR+Qwt{8R$UbbXqP zPOK$iH}qPz41x57?JSsFqw?#UC+P~3;_-jm-Oge2O@w;>!*J0v=;Dfwb#>u3bCIfF zkO4gP19QoVwr@{d!(%|3sWtAVFPO%#$I8+qbZHYjbVa>6L!siHk>l912dQlM2CYFA zuQ|w}KYdP}IMY5x<69*cNZN8U&Akp%ISB)Q{s_;FMmip^#1WPg6?mHffD5&(Pdk5@ z+Hr=K+p_w07~D7`;s4x498kw=zN@~F9eDG9{+!WYOn|LRIce#b@Bu{{vSXWnML^;G zMutH?{hgXQ*&ZyCf4~KT&!z@V#h(`G)$(%IJAU&-XUosiBqOcTWVRbb=ayw#LM^!< z+ThDTfVqO#;U9lu{P50<}+x zD4He&fF^Q#Z`pXbWJFRuO1AG}TL2QUdfW}R$-BNq(`T9Lsy0j#^abq+4vKxe`i?XUtW}eI{NiCPm4gt`A zR&f$Tr5zc~tcVmaFl!($4YLmLBn^R@Qep^nQy3y*NB(ijgJO@vEo>%E*mh9zU*;z;=jZGj)BQ1g`J|AJ8IVK>Jc3VWgmb2oYoTtgWya_If zagY}LIx8p11&8kZb}=cl%IrY;gJ2CT1&qQ44zAWF3gt6T(QYQr2}OMX8#)q{Kya4Cim=`S>)2zm~&*8^peU62sU>-hX)P zFBMhc#o^*hp5|2$5~py0GguzsIj|p`|^hc600U zA9SYzp>#lJK?^MfmKHG%f7sw; zBsSd8V0$Asc2ifhO+?BBI#!z`{T^Lg_ucQBGi;2^U)CW(O4hWgPM4yiJI*3>-{7Pg zq1}~jN)v|ue$)1H)Q>*86rGuIoTXAjhU95(KMohI(Uc_D^i!RGuBPABW4jm4+05U} zK^UpTbv`=tsL<0z-k@!?q#0WDb=|MlXjNOFYn@o@wod&tkJpl{X5&`Nu69k?)xOcP zsk@E^S}kjgUF&qLQE7?)7VZCE|CI~YZul*%^By7=Gz_4tX1S#;o9;E9Cz)`~AKe!b zm|$x5giDW8T)EDF{4I;HLL*P5T*07qSIa)k(OiTxmV*5cL{6O}?pV}?KWx!<9v3bz z^v5b}Ez)XF)+Ni-=n1Juh&tVCEk*4{uC{J>9_$!lZ_=(w>44fxxyDu|t;=qIJEpEj zb-lUYj-j;Mxp&8;%N>|Dto^hY&$C&X&#MjWIJw2053i1YUL5WH<2bv$SQM}F>CyH{ zdcA~5w3y;1nbSV`gzxgF{24#u$NYr9D}I%IN%=Luo~H?B z;Wzx|+w3Nt@{CXTBrWh{&Zm63xV=c{MRq;qInSplpYa);bNn*CRxLBba``2n^MV(X zq`2WFFK^~)%F91)I)T7w!8b= z>1nOgNq&2r$4A?A=v<=~9=o^c3Bm_iY{L%s;;^fhZ!Xme<27mt9d^iPMW1|opx-_S zcnAuAz5)r~fQF}@i0?tghG)dEmRmO<(JVp2aKzK1XFc-} z>%DmT@!8?Cf%Wbo1z8V==GuBvcdX~4p7o6Ak-~033T?vG#yjB8{jvUKZN%3-Gk$;} zt2q9YmTe@XzBZP9|1zR0!(ZXSWp!l#(Q(Ir_oMl9*G;}!WTbNy=IL!#!C4#XU)Be^ zdx&da9(~+DeYwK5+ozLcTK*g7oWI%0+wn$Tp1`2rdV0nW(ewH9564HV^duick zJGltOP(vH{G^FNo1BwT^ZCjW|MY2q*q8?;{1JJgLcj8~x3GOD2dj@bTvih^>IKKpc zkpL&Vx=O)1ddfbNATLv*OEjiTX9r>`?hUdjH-S3N4xYg0ctft*}MT9eFmqE@V*^P4+i*A_X4AzTuY*O7gTBc1( z592z=R_XPReu8biu#CO-Sipodouk2j`CVFe-bYX;T)&qTseiIxe<6=%8O)0|d|*bej+Xmitvv_~VCf;ONOoXRqe0~!0o8q5 z`2d)`W3RYpuO%`YJ_54)w#fmqXoyU%0cPSy0QSA)*9YhB(9HH}CYI>TBcR)VYzrD- z3^&JUH^4|QDhqm3xd0-}^jmgWv|nZFH~aOQ=KAGo_3~~I=&u*$B%9Jq9vG`5gK)9h z<<%0h9hV_XvD(SItj1cu?{xM1(l_(;3+toy!%QYsul=I#Fxm^D*OUExynR|G z^Ky%R8$jwTKP_)Dr3PK}wYlzp8{Z@|q~>3!OICJSerV^3x$3*0v-kO6 z)hviI$hj4#$fpU?MSeZ1qNRNs-;jREJ8v=wU*(r6e^;c-_;@`_r`7jt`ZY_v z)PDd^wdh=z!3+Ttmtz97f;oH9g4j?EZ$|Qr}01hZ9%535SOkDv9xtXzMR2frlh{RR63FWlOSEuUT z>f}k%Zy*ISi%mrBKo1N8ID#&$z$%0w3=vp^b=Wc~?x!|j6Qa