From d038362897d34157b3efadb735dfc4804828ccf4 Mon Sep 17 00:00:00 2001 From: ahunter Date: Tue, 26 Sep 2017 10:49:02 +0200 Subject: [PATCH 01/29] Add functionality to replicate margins --- R/Apply.R | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/R/Apply.R b/R/Apply.R index cafa3b5..f3a688d 100644 --- a/R/Apply.R +++ b/R/Apply.R @@ -73,6 +73,21 @@ Apply <- function(data, margins = NULL, AtomicFun, ..., inverse_margins = NULL, structure(list(env = environment(), index = margin_length, subs = as.name("[")), class = c("indexed_array")) } + #Check margins match for input objects + all_dims <- c(unlist(lapply(1 : length(data), + function(x) sum(dim(data[[x]])[margins[[x]]])))) + pos_dim <- min(which(all_dims == max(all_dims))) + dim_template <- dim(data[[pos_dim]])[margins[[pos_dim]]] + for (i in 1 : length(data)) { + if (identical(dim(data[[i]])[margins[[i]]], dim_template) == FALSE) { + for (j in 1 : (length(dim(data[[i]])[margins[[i]]]))) { + if (c(dim(data[[i]])[margins[[i]]])[j] != dim_template[j]) { + data[[i]] <- InsertDim(data[[i]], posdim = margins[[i]][j], lendim = dim_template[j]) + data[[i]] <- adrop(data[[i]], drop = (margins[[i]][j] + 1)) + } + } + } + } for (i in 1 : length(data)) { margin_length <- lapply(dim(data[[i]]), function(x) 1 : x) margin_length[-margins[[i]]] <- "" -- GitLab From 556c811fba87b45558cbb07c74c07a0c90145561 Mon Sep 17 00:00:00 2001 From: ahunter Date: Tue, 26 Sep 2017 10:53:52 +0200 Subject: [PATCH 02/29] Add s2dv dependency for InsertDim function --- DESCRIPTION | 3 ++- NAMESPACE | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/DESCRIPTION b/DESCRIPTION index e31050c..131c6d2 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -13,7 +13,8 @@ Imports: plyr, doParallel, future, - foreach + foreach, + s2dverification License: LGPL-3 URL: https://earth.bsc.es/gitlab/ces/multiApply BugReports: https://earth.bsc.es/gitlab/ces/multiApply/issues diff --git a/NAMESPACE b/NAMESPACE index 3b439b3..a6a7711 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -4,4 +4,5 @@ importFrom(abind, abind) importFrom(future, availableCores) importFrom(doParallel, registerDoParallel) importFrom(foreach, registerDoSEQ) +importFrom(s2dverification, InsertDim) export(Apply) -- GitLab From 4289df9b9d633485abdcef61f2c1a069bd32acc6 Mon Sep 17 00:00:00 2001 From: ahunter Date: Tue, 26 Sep 2017 12:32:54 +0200 Subject: [PATCH 03/29] Small change to allow functions to be provided directly --- R/Apply.R | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/R/Apply.R b/R/Apply.R index f3a688d..879ecb3 100644 --- a/R/Apply.R +++ b/R/Apply.R @@ -66,7 +66,11 @@ Apply <- function(data, margins = NULL, AtomicFun, ..., inverse_margins = NULL, } names <- names(dim(data[[1]]))[margins[[1]]] input <- list() - splatted_f <- splat(get(AtomicFun)) + if (is.charcter(AtomicFun)) { + splatted_f <- splat(get(AtomicFun)) + } else { + splatted_f <- splat(AtomicFun) + } if (!is.null(margins)) { .isolate <- function(data, margin_length, drop = TRUE) { eval(dim(environment()$data)) -- GitLab From 3f603641cf928d57644cf9322892da5d3aa6fac6 Mon Sep 17 00:00:00 2001 From: ahunter Date: Tue, 26 Sep 2017 13:36:00 +0200 Subject: [PATCH 04/29] Small bugfix for the previous commit --- R/Apply.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/Apply.R b/R/Apply.R index 879ecb3..c5255f9 100644 --- a/R/Apply.R +++ b/R/Apply.R @@ -66,7 +66,7 @@ Apply <- function(data, margins = NULL, AtomicFun, ..., inverse_margins = NULL, } names <- names(dim(data[[1]]))[margins[[1]]] input <- list() - if (is.charcter(AtomicFun)) { + if (is.character(AtomicFun)) { splatted_f <- splat(get(AtomicFun)) } else { splatted_f <- splat(AtomicFun) -- GitLab From 180a6efbd46d91c67edaac839baf8b4de6641cea Mon Sep 17 00:00:00 2001 From: ahunter Date: Tue, 26 Sep 2017 13:42:19 +0200 Subject: [PATCH 05/29] Reverse prioritization of margins and inverse_marginas --- R/Apply.R | 7 ++++--- man/Apply.Rd | 7 +++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/R/Apply.R b/R/Apply.R index c5255f9..6f5032a 100644 --- a/R/Apply.R +++ b/R/Apply.R @@ -2,10 +2,11 @@ #' #' The Apply function is an extension of the mapply function, which instead of taking lists of unidimensional objects as input, takes lists of multidimensional objects as input, which may have different numbers of dimensions and dimension lengths. The user can specify which dimensions of each array (or matrix) the function is to be applied over with the margins option. #' @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 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 inverse_margins are specified, margins takes priority over inverse_margins. +#' @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 inverse_margins are specified, inverse_margins takes priority over margins. +#' @param inverse_margins 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. If both margins and inverse_margins are specified, margins takes priority over inverse_margins. #' @param AtomicFun Function to be applied to the arrays. #' @param ... Additional arguments to be used in the AtomicFun. -#' @param inverse_margins 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. If both margins and inverse_margins are specified, margins takes priority over inverse_margins. +#' @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 inverse_margins are specified, inverse_margins takes priority over margins. #' @param parallel Logical, should the function be applied in parallel. #' @param ncores The number of cores to use for parallel computation. #' @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'. @@ -21,7 +22,7 @@ #' (sum(y > z) / (length(y)))) * 100} #' margins = list(c(1, 2), c(1, 2), c(1,2)) #' test <- Apply(data, margins, AtomicFun = "test_fun") -Apply <- function(data, margins = NULL, AtomicFun, ..., inverse_margins = NULL, parallel = FALSE, ncores = NULL) { +Apply <- function(data, inverse_margins = NULL, AtomicFun, ..., margins = NULL, parallel = FALSE, ncores = NULL) { if (!is.list(data)) { data <- list(data) } diff --git a/man/Apply.Rd b/man/Apply.Rd index 4a09caf..e884db1 100644 --- a/man/Apply.Rd +++ b/man/Apply.Rd @@ -4,20 +4,19 @@ \alias{Apply} \title{Wrapper for Applying Atomic Functions to Arrays.} \usage{ -Apply(data, margins = NULL, AtomicFun, ..., inverse_margins = NULL, parallel = FALSE, +Apply(data, inverse_margins = NULL, AtomicFun, ..., margins = NULL, parallel = FALSE, ncores = NULL) } \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{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 inverse_margins are specified, margins takes priority over inverse_margins.} +\item{inverse_margins}{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. If both margins and inverse_margins are specified, inverse_margins takes priority over margins.} \item{AtomicFun}{Function to be applied to the arrays.} \item{...}{Additional arguments to be used in the AtomicFun.} -\item{inverse_margins}{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. If both margins and inverse_margins are specified, margins takes priority over inverse_margins.} - +\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 inverse_margins are specified, inverse_margins takes priority over margins.} \item{parallel}{Logical, should the function be applied in parallel.} \item{ncores}{The number of cores to use for parallel computation.} -- GitLab From b20cd33be42708b1c8f3bcece54d7068a05b8b2c Mon Sep 17 00:00:00 2001 From: ahunter Date: Thu, 28 Sep 2017 16:20:59 +0200 Subject: [PATCH 06/29] Change inverse_margins to target_dims --- R/Apply.R | 24 ++++++++++++------------ man/Apply.Rd | 6 +++--- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/R/Apply.R b/R/Apply.R index 6f5032a..0fb2966 100644 --- a/R/Apply.R +++ b/R/Apply.R @@ -2,11 +2,11 @@ #' #' The Apply function is an extension of the mapply function, which instead of taking lists of unidimensional objects as input, takes lists of multidimensional objects as input, which may have different numbers of dimensions and dimension lengths. The user can specify which dimensions of each array (or matrix) the function is to be applied over with the margins option. #' @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 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 inverse_margins are specified, inverse_margins takes priority over margins. -#' @param inverse_margins 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. If both margins and inverse_margins are specified, margins takes priority over inverse_margins. +#' @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, target_dims takes priority over margins. +#' @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. 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 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 inverse_margins are specified, inverse_margins takes priority over margins. +#' @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, target_dims takes priority over margins. #' @param parallel Logical, should the function be applied in parallel. #' @param ncores The number of cores to use for parallel computation. #' @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'. @@ -22,29 +22,29 @@ #' (sum(y > z) / (length(y)))) * 100} #' margins = list(c(1, 2), c(1, 2), c(1,2)) #' test <- Apply(data, margins, AtomicFun = "test_fun") -Apply <- function(data, inverse_margins = NULL, AtomicFun, ..., margins = NULL, parallel = FALSE, ncores = NULL) { +Apply <- function(data, target_dims = NULL, AtomicFun, ..., margins = NULL, parallel = FALSE, ncores = NULL) { if (!is.list(data)) { data <- list(data) } if (!is.null(margins)) { - inverse_margins <- NULL + target_dims <- NULL } - if (!is.null(inverse_margins)) { - if (!is.list(inverse_margins)) { - inverse_margins <- rep(list(inverse_margins), length(data)) + if (!is.null(target_dims)) { + if (!is.list(target_dims)) { + target_dims <- rep(list(target_dims), length(data)) } - if (is.character(unlist(inverse_margins[1]))) { - margins2 <- inverse_margins + if (is.character(unlist(target_dims[1]))) { + margins2 <- target_dims for (i in 1 : length(data)) { margins_new <- c() for (j in 1 : length(margins2[[i]])) { margins_new[j] <- which(names(dim(data[[i]])) == margins2[[i]][[j]]) } - inverse_margins[[i]] <- c(margins_new) + target_dims[[i]] <- c(margins_new) } } for (i in 1 : length(data)) { - margins[[i]] <- c(1 :length(dim(data[[i]])))[-c(inverse_margins[[i]])] + margins[[i]] <- c(1 :length(dim(data[[i]])))[-c(target_dims[[i]])] } } if (!is.null(margins)) { diff --git a/man/Apply.Rd b/man/Apply.Rd index e884db1..699facd 100644 --- a/man/Apply.Rd +++ b/man/Apply.Rd @@ -4,19 +4,19 @@ \alias{Apply} \title{Wrapper for Applying Atomic Functions to Arrays.} \usage{ -Apply(data, inverse_margins = NULL, AtomicFun, ..., margins = NULL, parallel = FALSE, +Apply(data, target_dims = NULL, AtomicFun, ..., margins = NULL, parallel = FALSE, ncores = NULL) } \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{inverse_margins}{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. If both margins and inverse_margins are specified, inverse_margins takes priority over margins.} +\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. If both margins and target_dims are specified, target_dims takes priority over margins.} \item{AtomicFun}{Function to be applied to the arrays.} \item{...}{Additional arguments to be used in the AtomicFun.} -\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 inverse_margins are specified, inverse_margins takes priority over margins.} +\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, target_dims takes priority over margins.} \item{parallel}{Logical, should the function be applied in parallel.} \item{ncores}{The number of cores to use for parallel computation.} -- GitLab From be6bf905519f558863ccf8a1371802cca951b915 Mon Sep 17 00:00:00 2001 From: ahunter Date: Mon, 2 Oct 2017 08:55:15 +0200 Subject: [PATCH 07/29] I've introduced a bug somewhere --- R/Apply.R | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/R/Apply.R b/R/Apply.R index 0fb2966..8c9eaba 100644 --- a/R/Apply.R +++ b/R/Apply.R @@ -62,6 +62,10 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., margins = NULL, para margins[[i]] <- c(margins_new) } } + # if (is.unsorted(margins)) { + # unordered_dims <- margins + # margins <- sort(margins) + # } if (!is.logical(parallel)) { stop("parallel must be logical") } @@ -89,6 +93,7 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., margins = NULL, para if (c(dim(data[[i]])[margins[[i]]])[j] != dim_template[j]) { data[[i]] <- InsertDim(data[[i]], posdim = margins[[i]][j], lendim = dim_template[j]) data[[i]] <- adrop(data[[i]], drop = (margins[[i]][j] + 1)) + print("OK") } } } @@ -129,6 +134,7 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., margins = NULL, para } if (!is.null(dim(WrapperFun))) { names(dim(WrapperFun)) <- c(AtomicFun, names) + print(dim(WrapperFun)) } out <- WrapperFun } -- GitLab From 2e14ddb1fef4c70d591c491b963fa8db930b68ea Mon Sep 17 00:00:00 2001 From: ahunter Date: Mon, 2 Oct 2017 09:10:24 +0200 Subject: [PATCH 08/29] Fixed output dim names when AtomicFun returns object with more than one dim --- R/Apply.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/Apply.R b/R/Apply.R index cafa3b5..33b553a 100644 --- a/R/Apply.R +++ b/R/Apply.R @@ -108,7 +108,7 @@ Apply <- function(data, margins = NULL, AtomicFun, ..., inverse_margins = NULL, WrapperFun <- splatted_f(data, ...) } if (!is.null(dim(WrapperFun))) { - names(dim(WrapperFun)) <- c(AtomicFun, names) + names(dim(WrapperFun))[(length(dim(WrapperFun)) - length(names) + 1) : length(dim(WrapperFun))] <- c(names) } out <- WrapperFun } -- GitLab From f2b49ee68f552e8d346c42992857d5231129af4f Mon Sep 17 00:00:00 2001 From: ahunter Date: Mon, 2 Oct 2017 09:15:22 +0200 Subject: [PATCH 09/29] Change inverse_margins to target_dims --- R/Apply.R | 22 +++++++++++----------- man/Apply.Rd | 6 +++--- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/R/Apply.R b/R/Apply.R index 33b553a..3359837 100644 --- a/R/Apply.R +++ b/R/Apply.R @@ -2,10 +2,10 @@ #' #' The Apply function is an extension of the mapply function, which instead of taking lists of unidimensional objects as input, takes lists of multidimensional objects as input, which may have different numbers of dimensions and dimension lengths. The user can specify which dimensions of each array (or matrix) the function is to be applied over with the margins option. #' @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 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 inverse_margins are specified, margins takes priority over inverse_margins. +#' @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 AtomicFun Function to be applied to the arrays. #' @param ... Additional arguments to be used in the AtomicFun. -#' @param inverse_margins 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. If both margins and inverse_margins are specified, margins takes priority over inverse_margins. +#' @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. If both margins and target_dims are specified, margins takes priority over target_dims. #' @param parallel Logical, should the function be applied in parallel. #' @param ncores The number of cores to use for parallel computation. #' @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'. @@ -21,29 +21,29 @@ #' (sum(y > z) / (length(y)))) * 100} #' margins = list(c(1, 2), c(1, 2), c(1,2)) #' test <- Apply(data, margins, AtomicFun = "test_fun") -Apply <- function(data, margins = NULL, AtomicFun, ..., inverse_margins = NULL, parallel = FALSE, ncores = NULL) { +Apply <- function(data, margins = NULL, AtomicFun, ..., target_dims = NULL, parallel = FALSE, ncores = NULL) { if (!is.list(data)) { data <- list(data) } if (!is.null(margins)) { - inverse_margins <- NULL + target_dims <- NULL } - if (!is.null(inverse_margins)) { - if (!is.list(inverse_margins)) { - inverse_margins <- rep(list(inverse_margins), length(data)) + if (!is.null(target_dims)) { + if (!is.list(target_dims)) { + target_dims <- rep(list(target_dims), length(data)) } - if (is.character(unlist(inverse_margins[1]))) { - margins2 <- inverse_margins + if (is.character(unlist(target_dims[1]))) { + margins2 <- target_dims for (i in 1 : length(data)) { margins_new <- c() for (j in 1 : length(margins2[[i]])) { margins_new[j] <- which(names(dim(data[[i]])) == margins2[[i]][[j]]) } - inverse_margins[[i]] <- c(margins_new) + target_dims[[i]] <- c(margins_new) } } for (i in 1 : length(data)) { - margins[[i]] <- c(1 :length(dim(data[[i]])))[-c(inverse_margins[[i]])] + margins[[i]] <- c(1 :length(dim(data[[i]])))[-c(target_dims[[i]])] } } if (!is.null(margins)) { diff --git a/man/Apply.Rd b/man/Apply.Rd index 4a09caf..7cf39f2 100644 --- a/man/Apply.Rd +++ b/man/Apply.Rd @@ -4,19 +4,19 @@ \alias{Apply} \title{Wrapper for Applying Atomic Functions to Arrays.} \usage{ -Apply(data, margins = NULL, AtomicFun, ..., inverse_margins = NULL, parallel = FALSE, +Apply(data, margins = NULL, AtomicFun, ..., target_dims = NULL, parallel = FALSE, ncores = NULL) } \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{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 inverse_margins are specified, margins takes priority over inverse_margins.} +\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{AtomicFun}{Function to be applied to the arrays.} \item{...}{Additional arguments to be used in the AtomicFun.} -\item{inverse_margins}{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. If both margins and inverse_margins are specified, margins takes priority over inverse_margins.} +\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. If both margins and target_dims are specified, margins takes priority over target_dims.} \item{parallel}{Logical, should the function be applied in parallel.} -- GitLab From 410fcafc64e3f0c36b637ea802793e39cd7cf4a7 Mon Sep 17 00:00:00 2001 From: ahunter Date: Mon, 2 Oct 2017 09:40:17 +0200 Subject: [PATCH 10/29] Automatic sorting of target_dims --- R/Apply.R | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/R/Apply.R b/R/Apply.R index 3359837..c9d35ee 100644 --- a/R/Apply.R +++ b/R/Apply.R @@ -42,7 +42,13 @@ Apply <- function(data, margins = NULL, AtomicFun, ..., target_dims = NULL, para target_dims[[i]] <- c(margins_new) } } - for (i in 1 : length(data)) { + for (i in 1 : length(data)) { + if (is.unsorted(target_dims[[i]])) { + targ_dims <- sort(target_dims[[i]]) + marg_dims <- c(1 : length(dim(data[[i]])))[- target_dims[[i]]] + data[[i]] <- aperm(data[[i]], c(targ_dims, marg_dims)) + target_dims[[i]] <- 1 : length(targ_dims) + } margins[[i]] <- c(1 :length(dim(data[[i]])))[-c(target_dims[[i]])] } } -- GitLab From 224579be0874decda5fb5fc40671669b85c2004e Mon Sep 17 00:00:00 2001 From: ahunter Date: Mon, 2 Oct 2017 15:29:21 +0200 Subject: [PATCH 11/29] Bugfixed, but still can't handle POSXIct objects when replicating margins --- R/Apply.R | 75 ++++++++++++++++++++++++++++++------------------------- 1 file changed, 41 insertions(+), 34 deletions(-) diff --git a/R/Apply.R b/R/Apply.R index 8c9eaba..679cd8d 100644 --- a/R/Apply.R +++ b/R/Apply.R @@ -2,11 +2,10 @@ #' #' The Apply function is an extension of the mapply function, which instead of taking lists of unidimensional objects as input, takes lists of multidimensional objects as input, which may have different numbers of dimensions and dimension lengths. The user can specify which dimensions of each array (or matrix) the function is to be applied over with the margins option. #' @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 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, target_dims takes priority over margins. -#' @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. If both margins and target_dims are specified, margins takes priority over target_dims. +#' @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 AtomicFun Function to be applied to the arrays. #' @param ... Additional arguments to be used in the AtomicFun. -#' @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, target_dims takes priority over margins. +#' @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. If both margins and target_dims are specified, margins takes priority over target_dims. #' @param parallel Logical, should the function be applied in parallel. #' @param ncores The number of cores to use for parallel computation. #' @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'. @@ -22,7 +21,7 @@ #' (sum(y > z) / (length(y)))) * 100} #' margins = list(c(1, 2), c(1, 2), c(1,2)) #' test <- Apply(data, margins, AtomicFun = "test_fun") -Apply <- function(data, target_dims = NULL, AtomicFun, ..., margins = NULL, parallel = FALSE, ncores = NULL) { +Apply <- function(data, margins = NULL, AtomicFun, ..., target_dims = NULL, parallel = FALSE, ncores = NULL) { if (!is.list(data)) { data <- list(data) } @@ -43,7 +42,13 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., margins = NULL, para target_dims[[i]] <- c(margins_new) } } - for (i in 1 : length(data)) { + for (i in 1 : length(data)) { + if (is.unsorted(target_dims[[i]])) { + targ_dims <- sort(target_dims[[i]]) + marg_dims <- c(1 : length(dim(data[[i]])))[- target_dims[[i]]] + data[[i]] <- aperm(data[[i]], c(targ_dims, marg_dims)) + target_dims[[i]] <- 1 : length(targ_dims) + } margins[[i]] <- c(1 :length(dim(data[[i]])))[-c(target_dims[[i]])] } } @@ -62,42 +67,48 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., margins = NULL, para margins[[i]] <- c(margins_new) } } - # if (is.unsorted(margins)) { - # unordered_dims <- margins - # margins <- sort(margins) - # } if (!is.logical(parallel)) { stop("parallel must be logical") } names <- names(dim(data[[1]]))[margins[[1]]] input <- list() - if (is.character(AtomicFun)) { - splatted_f <- splat(get(AtomicFun)) - } else { + if (is.function(AtomicFun)) { splatted_f <- splat(AtomicFun) + } else { + splatted_f <- splat(get(AtomicFun)) } + + splatted_f <- splat(get(AtomicFun)) + if (!is.null(margins)) { + #Check margins match for input objects + all_dims <- c(unlist(lapply(1 : length(data), + function(x) sum(dim(data[[x]])[margins[[x]]])))) + print(all_dims) + pos_dim <- min(which(all_dims == max(all_dims))) + dim_template <- dim(data[[pos_dim]])[margins[[pos_dim]]] + print(pos_dim) + print(dim_template) + for (i in 1 : length(data)) { + if (identical(dim(data[[i]])[margins[[i]]], dim_template) == FALSE) { + for (j in 1 : (length(dim(data[[i]])[margins[[i]]]))) { + print("OK") + print(c(dim(data[[i]])[margins[[i]]])[j] != dim_template[j]) + if (c(dim(data[[i]])[margins[[i]]])[j] != dim_template[j]) { + print(class(data[[i]])) + data[[i]] <- InsertDim(data[[i]], posdim = margins[[i]][j], lendim = dim_template[j]) + data[[i]] <- adrop(data[[i]], drop = (margins[[i]][j] + 1)) + print(class(data[[i]])) + } + } + } + } + print(dim(data)[[2]]) .isolate <- function(data, margin_length, drop = TRUE) { eval(dim(environment()$data)) structure(list(env = environment(), index = margin_length, subs = as.name("[")), class = c("indexed_array")) } - #Check margins match for input objects - all_dims <- c(unlist(lapply(1 : length(data), - function(x) sum(dim(data[[x]])[margins[[x]]])))) - pos_dim <- min(which(all_dims == max(all_dims))) - dim_template <- dim(data[[pos_dim]])[margins[[pos_dim]]] - for (i in 1 : length(data)) { - if (identical(dim(data[[i]])[margins[[i]]], dim_template) == FALSE) { - for (j in 1 : (length(dim(data[[i]])[margins[[i]]]))) { - if (c(dim(data[[i]])[margins[[i]]])[j] != dim_template[j]) { - data[[i]] <- InsertDim(data[[i]], posdim = margins[[i]][j], lendim = dim_template[j]) - data[[i]] <- adrop(data[[i]], drop = (margins[[i]][j] + 1)) - print("OK") - } - } - } - } for (i in 1 : length(data)) { margin_length <- lapply(dim(data[[i]]), function(x) 1 : x) margin_length[-margins[[i]]] <- "" @@ -133,11 +144,7 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., margins = NULL, para WrapperFun <- splatted_f(data, ...) } if (!is.null(dim(WrapperFun))) { - names(dim(WrapperFun)) <- c(AtomicFun, names) - print(dim(WrapperFun)) + # names(dim(WrapperFun))[(length(dim(WrapperFun)) - length(names) + 1) : length(dim(WrapperFun))] <- c(names) } out <- WrapperFun -} - - - +} \ No newline at end of file -- GitLab From f43f566d1df7cc05cc97e28a5dc1cbf8b811ed01 Mon Sep 17 00:00:00 2001 From: Nicolau Manubens Date: Mon, 16 Oct 2017 14:54:00 +0200 Subject: [PATCH 12/29] Some enhancements ongoing. --- R/Apply.R | 186 ++++++++++++++++++++++++++++++++++++------------------ R/Utils.R | 9 +++ 2 files changed, 132 insertions(+), 63 deletions(-) create mode 100644 R/Utils.R diff --git a/R/Apply.R b/R/Apply.R index 3cbe6d1..5cb9e22 100644 --- a/R/Apply.R +++ b/R/Apply.R @@ -22,88 +22,153 @@ #' margins = list(c(1, 2), c(1, 2), c(1,2)) #' test <- Apply(data, margins, AtomicFun = "test_fun") Apply <- function(data, target_dims = NULL, AtomicFun, ..., margins = NULL, parallel = FALSE, ncores = NULL) { + # Check data if (!is.list(data)) { data <- list(data) } + if (any(!sapply(data, is.numeric))) { + stop("Parameter 'data' must be one or a list of numeric objects.") + } + # Check target_dims and margins + if (is.null(margins) && is.null(target_dims)) { + stop("One of 'margins' or 'target_dims' must be specified.") + } if (!is.null(margins)) { target_dims <- NULL } - if (!is.null(target_dims)) { - if (!is.list(target_dims)) { - target_dims <- rep(list(target_dims), length(data)) + if (!is.null(margins)) { + # Check margins and build target_dims accordingly + if (!is.list(margins)) { + margins <- rep(list(margins), length(data)) + } + if (any(!sapply(margins, + function(x) is.character(x) || is.numeric(x)))) { + 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 (is.character(unlist(target_dims[1]))) { - margins2 <- target_dims - for (i in 1 : length(data)) { - margins_new <- c() - for (j in 1 : length(margins2[[i]])) { - margins_new[j] <- which(names(dim(data[[i]])) == margins2[[i]][[j]]) + duplicate_dim_specs <- sapply(margins, + function(x) { + length(unique(x)) != length(x) + }) + if (any(duplicate_dim_specs)) { + stop("Parameter 'margins' must not contain duplicated dimension ", + "specifications.") + } + target_dims <- vector('list', length(data)) + for (i in 1 : length(data)) { + if (is.character(unlist(margins[i]))) { + margins_new <- margins[[i]] + for (j in 1 : length(margins_new)) { + margins_new[j] <- which(names(dim(data[[i]])) == margins_new[j]) } - target_dims[[i]] <- c(margins_new) + margins[[i]] <- margins_new } + target_dims[[i]] <- c(1 : length(dim(data[[i]])))[-c(margins[[i]])] + } + } else { + # Check target_dims and build margins accordingly + if (!is.list(target_dims)) { + target_dims <- rep(list(target_dims), length(data)) + } + if (any(!sapply(target_dims, + function(x) is.character(x) || is.numeric(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.") } + duplicate_dim_specs <- sapply(target_dims, + function(x) { + length(unique(x)) != length(x) + }) + if (any(duplicate_dim_specs)) { + stop("Parameter 'target_dims' must not contain duplicated dimension ", + "specifications.") + } + margins <- vector('list', length(data)) for (i in 1 : length(data)) { - if (is.unsorted(target_dims[[i]])) { - targ_dims <- sort(target_dims[[i]]) - marg_dims <- c(1 : length(dim(data[[i]])))[- target_dims[[i]]] - data[[i]] <- aperm(data[[i]], c(targ_dims, marg_dims)) - target_dims[[i]] <- 1 : length(targ_dims) + if (is.character(unlist(target_dims[i]))) { + margins2 <- target_dims[[i]] + margins2_new <- c() + for (j in 1 : length(margins2)) { + margins2_new[j] <- which(names(dim(data[[i]])) == margins2[j]) + } + target_dims[[i]] <- margins2_new } - margins[[i]] <- c(1 :length(dim(data[[i]])))[-c(target_dims[[i]])] + margins[[i]] <- c(1 : length(dim(data[[i]]))[-c(target_dims[[i]])]) } } - if (!is.null(margins)) { - if (!is.list(margins)) { - margins <- rep(list(margins), length(data)) + # Reorder dimensions of input data for target dims to be left-most + for (i in 1 : length(data)) { + if (is.unsorted(target_dims[[i]]) || + (max(target_dims[[i]]) > length(target_dims[[i]]))) { + targ_dims <- sort(target_dims[[i]]) + marg_dims <- (1 : length(dim(data[[i]])))[- target_dims[[i]]] + data[[i]] <- .aperm2(data[[i]], c(targ_dims, marg_dims)) + target_dims[[i]] <- 1 : length(targ_dims) + margins[[i]] <- c(1 : length(dim(data[[i]])))[-c(target_dims[[i]])] } } - if (is.character(unlist(margins[1])) && !is.null(margins)) { - margins2 <- margins - for (i in 1 : length(data)) { - margins_new <- c() - for (j in 1 : length(margins2[[i]])) { - margins_new[j] <- which(names(dim(data[[i]])) == margins2[[i]][[j]]) - } - margins[[i]] <- c(margins_new) + # Check AtomicFun + if (is.character(AtomicFun)) { + try({AtomicFun <- get(AtomicFun)}, silent = TRUE) + if (!is.function(AtomicFun)) { + stop("Could not find the function '", AtomicFun, "'.") } } + if (!is.function(AtomicFun)) { + stop("Parameter 'AtomicFun' must be a function or a character string ", + "with the name of a function.") + } + # Check parallel if (!is.logical(parallel)) { - stop("parallel must be logical") + stop("Parameter 'parallel' must be logical.") } + # Check ncores + if (parallel) { + if (is.null(ncores)) { + ncores <- availableCores() - 1 + } + if (!is.numeric(ncores)) { + stop("Parameter 'ncores' must be numeric.") + } + ncores <- round(ncores) + ncores <- min(availableCores() - 1, ncores) + } + names <- names(dim(data[[1]]))[margins[[1]]] input <- list() - if (is.function(AtomicFun)) { - splatted_f <- splat(AtomicFun) - } else { - splatted_f <- splat(get(AtomicFun)) - } - - splatted_f <- splat(get(AtomicFun)) - + + splatted_f <- splat(AtomicFun) + if (!is.null(margins)) { #Check margins match for input objects - all_dims <- c(unlist(lapply(1 : length(data), - function(x) sum(dim(data[[x]])[margins[[x]]])))) - print(all_dims) - pos_dim <- min(which(all_dims == max(all_dims))) - dim_template <- dim(data[[pos_dim]])[margins[[pos_dim]]] - print(pos_dim) - print(dim_template) - for (i in 1 : length(data)) { - if (identical(dim(data[[i]])[margins[[i]]], dim_template) == FALSE) { - for (j in 1 : (length(dim(data[[i]])[margins[[i]]]))) { - print("OK") - print(c(dim(data[[i]])[margins[[i]]])[j] != dim_template[j]) - if (c(dim(data[[i]])[margins[[i]]])[j] != dim_template[j]) { - print(class(data[[i]])) - data[[i]] <- InsertDim(data[[i]], posdim = margins[[i]][j], lendim = dim_template[j]) - data[[i]] <- adrop(data[[i]], drop = (margins[[i]][j] + 1)) - print(class(data[[i]])) - } - } - } - } - print(dim(data)[[2]]) + all_dims <- c(unlist(lapply(1 : length(data), + function(x) sum(dim(data[[x]])[margins[[x]]])))) + print(all_dims) + pos_dim <- min(which(all_dims == max(all_dims))) + dim_template <- dim(data[[pos_dim]])[margins[[pos_dim]]] + print(pos_dim) + print(dim_template) + for (i in 1 : length(data)) { + if (identical(dim(data[[i]])[margins[[i]]], dim_template) == FALSE) { + for (j in 1 : (length(dim(data[[i]])[margins[[i]]]))) { + print("OK") + print(c(dim(data[[i]])[margins[[i]]])[j] != dim_template[j]) + if (c(dim(data[[i]])[margins[[i]]])[j] != dim_template[j]) { + print(class(data[[i]])) + data[[i]] <- InsertDim(data[[i]], posdim = margins[[i]][j], lendim = dim_template[j]) + data[[i]] <- adrop(data[[i]], drop = (margins[[i]][j] + 1)) + print(class(data[[i]])) + } + } + } + } + print(dim(data)[[2]]) .isolate <- function(data, margin_length, drop = TRUE) { eval(dim(environment()$data)) structure(list(env = environment(), index = margin_length, subs = as.name("[")), @@ -120,11 +185,6 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., margins = NULL, para i_max <- length(input[[1]])[1] / dims[[1]] k <- length(input[[1]]) / i_max if (parallel == TRUE) { - if (is.null(ncores)) { - ncores <- availableCores() - 1 - } else { - ncores <- min(availableCores() - 1, ncores) - } registerDoParallel(ncores) } WrapperFun <- llply(1 : i_max, function(i) diff --git a/R/Utils.R b/R/Utils.R new file mode 100644 index 0000000..4404d32 --- /dev/null +++ b/R/Utils.R @@ -0,0 +1,9 @@ +# 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)] + dim(x) <- old_dims[new_order] + x +} -- GitLab From 79bdaf3f98d92e0b0739bf9d65083a795e324eb1 Mon Sep 17 00:00:00 2001 From: Nicolau Manubens Date: Mon, 16 Oct 2017 21:46:12 +0200 Subject: [PATCH 13/29] Some progress. --- R/Apply.R | 88 ++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 65 insertions(+), 23 deletions(-) diff --git a/R/Apply.R b/R/Apply.R index 5cb9e22..cffc871 100644 --- a/R/Apply.R +++ b/R/Apply.R @@ -29,6 +29,13 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., margins = NULL, para if (any(!sapply(data, is.numeric))) { stop("Parameter 'data' must be one or a list of numeric objects.") } + is_vector <- rep(FALSE, length(data)) + for (i in 1 : length(data)) { + if (is.null(dim(data[[i]]))) { + is_vector[i] <- TRUE + dim(data[[i]]) <- length(data[[i]]) + } + } # Check target_dims and margins if (is.null(margins) && is.null(target_dims)) { stop("One of 'margins' or 'target_dims' must be specified.") @@ -36,19 +43,18 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., margins = NULL, para if (!is.null(margins)) { target_dims <- NULL } + margins_names <- vector('list', length(data)) + target_dims_names <- vector('list', length(data)) if (!is.null(margins)) { # Check margins and build target_dims accordingly if (!is.list(margins)) { margins <- rep(list(margins), length(data)) } if (any(!sapply(margins, - function(x) is.character(x) || is.numeric(x)))) { + function(x) is.character(x) || is.numeric(x) || is.null(x)))) { 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) @@ -59,14 +65,35 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., margins = NULL, para } target_dims <- vector('list', length(data)) for (i in 1 : length(data)) { - if (is.character(unlist(margins[i]))) { - margins_new <- margins[[i]] - for (j in 1 : length(margins_new)) { - margins_new[j] <- which(names(dim(data[[i]])) == margins_new[j]) + if (length(margins[[i]]) > 0) { + if (is.character(unlist(margins[i]))) { + if (is.null(names(dim(data[[i]])))) { + stop("Parameter 'margins' contains dimension names, but ", + "some of the corresponding objects in 'data' do not have ", + "dimension names.") + } + margins_new <- margins[[i]] + for (j in 1 : length(margins_new)) { + matches <- which(names(dim(data[[i]])) == margins_new[j]) + if (length(matches) < 1) { + stop("Could not find dimension '", margins_new[j], "' in ", i, + "th object provided in 'data'.") + } + margins_new[j] <- matches[1] + } + margins_names[[i]] <- margins[[i]] + margins[[i]] <- margins_new + } + if (!is.null(names(dim(data[[i]])))) { + target_dims_names[[i]] <- names(dim(data[[i]]))[- margins[[i]]] + } + target_dims[[i]] <- c(1 : length(dim(data[[i]])))[- margins[[i]]] + } else { + target_dims[[i]] <- 1 : length(dim(data[[i]])) + if (!is.null(names(dim(data[[i]])))) { + target_dims_names[[i]] <- names(dim(data[[i]])) } - margins[[i]] <- margins_new } - target_dims[[i]] <- c(1 : length(dim(data[[i]])))[-c(margins[[i]])] } } else { # Check target_dims and build margins accordingly @@ -92,25 +119,39 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., margins = NULL, para margins <- vector('list', length(data)) for (i in 1 : length(data)) { if (is.character(unlist(target_dims[i]))) { - margins2 <- target_dims[[i]] - margins2_new <- c() - for (j in 1 : length(margins2)) { - margins2_new[j] <- which(names(dim(data[[i]])) == margins2[j]) + 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.") + } + margins2_new <- target_dims[[i]] + for (j in 1 : length(margins2_new)) { + matches <- which(names(dim(data[[i]])) == margins2_new[j]) + if (length(matches) < 1) { + stop("Could not find dimension '", margins2_new[j], "' in ", i, + "th object provided in 'data'.") + } + margins2_new[j] <- matches[1] } + target_dims_names[[i]] <- target_dims[[i]] target_dims[[i]] <- margins2_new } - margins[[i]] <- c(1 : length(dim(data[[i]]))[-c(target_dims[[i]])]) + if (!is.null(names(dim(data[[i]])))) { + margins_names[[i]] <- names(dim(data[[i]]))[- target_dims[[i]]] + } + margins[[i]] <- c(1 : length(dim(data[[i]]))[- target_dims[[i]]]) + } } # Reorder dimensions of input data for target dims to be left-most + # and in the required order. for (i in 1 : length(data)) { if (is.unsorted(target_dims[[i]]) || (max(target_dims[[i]]) > length(target_dims[[i]]))) { - targ_dims <- sort(target_dims[[i]]) marg_dims <- (1 : length(dim(data[[i]])))[- target_dims[[i]]] - data[[i]] <- .aperm2(data[[i]], c(targ_dims, marg_dims)) - target_dims[[i]] <- 1 : length(targ_dims) - margins[[i]] <- c(1 : length(dim(data[[i]])))[-c(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]])) } } # Check AtomicFun @@ -145,12 +186,12 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., margins = NULL, para splatted_f <- splat(AtomicFun) - if (!is.null(margins)) { + if (any(!sapply(margins, is.null))) { #Check margins match for input objects - all_dims <- c(unlist(lapply(1 : length(data), - function(x) sum(dim(data[[x]])[margins[[x]]])))) + all_dims <- c(sapply(1 : length(data), + function(x) sum(dim(data[[x]])[margins[[x]]]))) print(all_dims) - pos_dim <- min(which(all_dims == max(all_dims))) + pos_dim <- which.max(all_dims)[1] dim_template <- dim(data[[pos_dim]])[margins[[pos_dim]]] print(pos_dim) print(dim_template) @@ -206,5 +247,6 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., margins = NULL, para if (!is.null(dim(WrapperFun))) { names(dim(WrapperFun))[(length(dim(WrapperFun)) - length(names) + 1) : length(dim(WrapperFun))] <- c(names) } + out <- WrapperFun } -- GitLab From ebe226fe66b5800683c74f0cd2aab7d97d35f456 Mon Sep 17 00:00:00 2001 From: Nicolau Manubens Date: Tue, 17 Oct 2017 11:36:55 +0200 Subject: [PATCH 14/29] Some progress. --- R/Apply.R | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/R/Apply.R b/R/Apply.R index cffc871..7c5fac7 100644 --- a/R/Apply.R +++ b/R/Apply.R @@ -181,6 +181,36 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., margins = NULL, para ncores <- min(availableCores() - 1, ncores) } + # Consistency checks of margins of all arrays +# accumulated_found_margins <- list() +# steps: +# 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 +# then need to check which margins are common for all the data arrays. Those will be used by llply. +# For the margins that are not common, we will need to iterate manually across them, and use data arrays repeatedly as needed +# Extra: take into account in this process that there can be empty margin vectors +# +# for (i in 1 : length(data)) { +# if (!is.null(names(dim(data[[i]])))) { +# matches <- which(names(dim(data[[i]])) %in% names(accumulated_found_sizes)) +# no_match_names <- names(dim(data[[i]])) +# if (length(matches) > 0) { +# match_names <- names(dim(data[[i]]))[matches] +# if (any(unlist(accumulated_found_sizes[match_names]) != dim(data[[i]])[match_names])) { +# stop("Found dimensions with the same name and different size in 'data'.") +# } +# no_match_names <- no_match_names[- matches] +# } +# accumulated_found_sizes <- c(accumulated_found_sizes, as.list(dim(data[[i]])[no_match_names])) +# } +# + names <- names(dim(data[[1]]))[margins[[1]]] input <- list() -- GitLab From af732610045ec806bd80b3bef2a95f97d3280fa3 Mon Sep 17 00:00:00 2001 From: Nicolau Manubens Date: Wed, 18 Oct 2017 20:55:28 +0200 Subject: [PATCH 15/29] Progress with checks. --- R/Apply.R | 286 ++++++++++++++++++++++++++----------------- R/{Utils.R => zzz.R} | 0 2 files changed, 177 insertions(+), 109 deletions(-) rename R/{Utils.R => zzz.R} (100%) diff --git a/R/Apply.R b/R/Apply.R index 7c5fac7..8c7dd7c 100644 --- a/R/Apply.R +++ b/R/Apply.R @@ -46,6 +46,7 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., margins = NULL, para margins_names <- vector('list', length(data)) target_dims_names <- vector('list', length(data)) if (!is.null(margins)) { + # Check margins and build target_dims accordingly if (!is.list(margins)) { margins <- rep(list(margins), length(data)) @@ -72,22 +73,23 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., margins = NULL, para "some of the corresponding objects in 'data' do not have ", "dimension names.") } - margins_new <- margins[[i]] - for (j in 1 : length(margins_new)) { - matches <- which(names(dim(data[[i]])) == margins_new[j]) + margins2 <- margins[[i]] + margins2_new_num <- c() + for (j in 1 : length(margins2)) { + matches <- which(names(dim(data[[i]])) == margins2[j]) if (length(matches) < 1) { - stop("Could not find dimension '", margins_new[j], "' in ", i, + stop("Could not find dimension '", margins2[j], "' in ", i, "th object provided in 'data'.") } - margins_new[j] <- matches[1] + margins2_new_num[j] <- matches[1] } margins_names[[i]] <- margins[[i]] - margins[[i]] <- margins_new + margins[[i]] <- margins2_new_num } if (!is.null(names(dim(data[[i]])))) { target_dims_names[[i]] <- names(dim(data[[i]]))[- margins[[i]]] } - target_dims[[i]] <- c(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]])))) { @@ -124,25 +126,26 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., margins = NULL, para "some of the corresponding objects in 'data' do not have ", "dimension names.") } - margins2_new <- target_dims[[i]] - for (j in 1 : length(margins2_new)) { - matches <- which(names(dim(data[[i]])) == margins2_new[j]) + 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 '", margins2_new[j], "' in ", i, + stop("Could not find dimension '", targs2[j], "' in ", i, "th object provided in 'data'.") } - margins2_new[j] <- matches[1] + targs2_new_num[j] <- matches[1] } target_dims_names[[i]] <- target_dims[[i]] - target_dims[[i]] <- margins2_new + 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]] <- c(1 : length(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 # and in the required order. for (i in 1 : length(data)) { @@ -154,6 +157,7 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., margins = NULL, para margins[[i]] <- (length(target_dims[[i]]) + 1) : length(dim(data[[i]])) } } + # Check AtomicFun if (is.character(AtomicFun)) { try({AtomicFun <- get(AtomicFun)}, silent = TRUE) @@ -165,10 +169,12 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., margins = NULL, para stop("Parameter 'AtomicFun' must be a function or a character string ", "with the name of a function.") } + # Check parallel if (!is.logical(parallel)) { stop("Parameter 'parallel' must be logical.") } + # Check ncores if (parallel) { if (is.null(ncores)) { @@ -181,102 +187,164 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., margins = NULL, para ncores <- min(availableCores() - 1, ncores) } - # Consistency checks of margins of all arrays -# accumulated_found_margins <- list() -# steps: -# 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 -# then need to check which margins are common for all the data arrays. Those will be used by llply. -# For the margins that are not common, we will need to iterate manually across them, and use data arrays repeatedly as needed -# Extra: take into account in this process that there can be empty margin vectors -# -# for (i in 1 : length(data)) { -# if (!is.null(names(dim(data[[i]])))) { -# matches <- which(names(dim(data[[i]])) %in% names(accumulated_found_sizes)) -# no_match_names <- names(dim(data[[i]])) -# if (length(matches) > 0) { -# match_names <- names(dim(data[[i]]))[matches] -# if (any(unlist(accumulated_found_sizes[match_names]) != dim(data[[i]])[match_names])) { -# stop("Found dimensions with the same name and different size in 'data'.") -# } -# no_match_names <- no_match_names[- matches] -# } -# accumulated_found_sizes <- c(accumulated_found_sizes, as.list(dim(data[[i]])[no_match_names])) -# } -# - - names <- names(dim(data[[1]]))[margins[[1]]] - input <- list() - - splatted_f <- splat(AtomicFun) - - if (any(!sapply(margins, is.null))) { - #Check margins match for input objects - all_dims <- c(sapply(1 : length(data), - function(x) sum(dim(data[[x]])[margins[[x]]]))) - print(all_dims) - pos_dim <- which.max(all_dims)[1] - dim_template <- dim(data[[pos_dim]])[margins[[pos_dim]]] - print(pos_dim) - print(dim_template) - for (i in 1 : length(data)) { - if (identical(dim(data[[i]])[margins[[i]]], dim_template) == FALSE) { - for (j in 1 : (length(dim(data[[i]])[margins[[i]]]))) { - print("OK") - print(c(dim(data[[i]])[margins[[i]]])[j] != dim_template[j]) - if (c(dim(data[[i]])[margins[[i]]])[j] != dim_template[j]) { - print(class(data[[i]])) - data[[i]] <- InsertDim(data[[i]], posdim = margins[[i]][j], lendim = dim_template[j]) - data[[i]] <- adrop(data[[i]], drop = (margins[[i]][j] + 1)) - print(class(data[[i]])) - } - } - } - } - print(dim(data)[[2]]) - .isolate <- function(data, margin_length, drop = TRUE) { - eval(dim(environment()$data)) - structure(list(env = environment(), index = margin_length, subs = as.name("[")), - class = c("indexed_array")) - } - for (i in 1 : length(data)) { - margin_length <- lapply(dim(data[[i]]), function(x) 1 : x) - margin_length[-margins[[i]]] <- "" - margin_length <- expand.grid(margin_length, KEEP.OUT.ATTRS = FALSE, - stringsAsFactors = FALSE) - input[[i]] <- .isolate(data[[i]], margin_length) - } - dims <- dim(data[[1]])[margins[[1]]] - i_max <- length(input[[1]])[1] / dims[[1]] - k <- length(input[[1]]) / i_max - if (parallel == TRUE) { - registerDoParallel(ncores) - } - WrapperFun <- llply(1 : i_max, function(i) - sapply((k * i - (k - 1)) : (k * i), function(x) splatted_f(lapply(input, `[[`, x),...), simplify = FALSE), - .parallel = parallel) - if (parallel == TRUE) { - registerDoSEQ() - } - if (is.null(dim(WrapperFun[[1]][[1]]))) { - WrapperFun <- array(as.numeric(unlist(WrapperFun)), dim=c(c(length((WrapperFun[[1]])[[1]])), - dim(data[[1]])[margins[[1]]])) + # 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 + accumulated_found_margins <- afm <- list() + for (i in 1:length(data)) { + if (!is.null(margins_names[[i]])) { + if (length(afm) > 0) { + matches <- which(margins_names[[i]] %in% names(afm)) + if (length(matches) > 0) { + margs_to_add <- as.list(dim(data[[i]])[margins[[i]]][- matches]) + if (any(dim(data[[i]])[margins[[i]][matches]] != unlist(afm[margins_names[[i]][matches]]))) { + stop("Found one or more margin dimensions with the same name and ", + "different length in some of the input objects in 'data'.") + } + } else { + margs_to_add <- as.list(dim(data[[i]])[margins[[i]]]) + } + unnamed_margins <- which(sapply(names(afm), nchar) == 0) + if (length(unnamed_margins) > 0) { + stop_with_error <- FALSE + if (length(unnamed_margins) <= length(margs_to_add)) { + if (any(unlist(afm[unnamed_margins]) != unlist(margs_to_add[1:length(unnamed_margins)]))) { + stop_with_error <- TRUE + } + names(afm)[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(afm[unnamed_margins[1:length(margs_to_add)]]) != unlist(margs_to_add))) { + stop_with_error <- TRUE + } + names(afm)[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.") + } + } + afm <- c(afm, margs_to_add) + } else { + afm <- as.list(dim(data[[i]])[margins[[i]]]) + } } else { - WrapperFun <- array(as.numeric(unlist(WrapperFun)), dim=c(c(dim(WrapperFun[[1]][[1]])), - dim(data[[1]])[margins[[1]]])) + margs_to_add <- as.list(dim(data[[i]])[margins[[i]]]) + names(margs_to_add) <- rep('', length(margs_to_add)) + if (length(afm) > 0) { + stop_with_error <- FALSE + if (length(afm) >= length(margs_to_add)) { + if (any(unlist(margs_to_add) != unlist(afm[1:length(margs_to_add)]))) { + stop_with_error <- TRUE + } + } else { + if (any(unlist(margs_to_add)[1:length(afm)] != unlist(afm))) { + stop_with_error <- TRUE + } + margs_to_add <- margs_to_add[- (1:length(afm))] + afm <- c(afm, 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 { + afm <- margs_to_add + } } - } else { - WrapperFun <- splatted_f(data, ...) - } - if (!is.null(dim(WrapperFun))) { - names(dim(WrapperFun))[(length(dim(WrapperFun)) - length(names) + 1) : length(dim(WrapperFun))] <- c(names) } - out <- WrapperFun + # Now need to check which margins are common for all the data arrays. + # Those will be used by llply. + # For the margins that are not common, we will need to iterate manually + # across them, and use data arrays repeatedly as needed. + common_margs <- c() + +# names <- names(dim(data[[1]]))[margins[[1]]] +# input <- list() +# +# splatted_f <- splat(AtomicFun) +# +# if (any(!sapply(margins, is.null))) { +# #Check margins match for input objects +# all_dims <- c(sapply(1 : length(data), +# function(x) sum(dim(data[[x]])[margins[[x]]]))) +# print(all_dims) +# pos_dim <- which.max(all_dims)[1] +# dim_template <- dim(data[[pos_dim]])[margins[[pos_dim]]] +# print(pos_dim) +# print(dim_template) +# for (i in 1 : length(data)) { +# if (identical(dim(data[[i]])[margins[[i]]], dim_template) == FALSE) { +# for (j in 1 : (length(dim(data[[i]])[margins[[i]]]))) { +# print("OK") +# print(c(dim(data[[i]])[margins[[i]]])[j] != dim_template[j]) +# if (c(dim(data[[i]])[margins[[i]]])[j] != dim_template[j]) { +# print(class(data[[i]])) +# data[[i]] <- InsertDim(data[[i]], posdim = margins[[i]][j], lendim = dim_template[j]) +# data[[i]] <- adrop(data[[i]], drop = (margins[[i]][j] + 1)) +# print(class(data[[i]])) +# } +# } +# } +# } +# print(dim(data)[[2]]) +# .isolate <- function(data, margin_length, drop = TRUE) { +# eval(dim(environment()$data)) +# structure(list(env = environment(), index = margin_length, subs = as.name("[")), +# class = c("indexed_array")) +# } +# for (i in 1 : length(data)) { +# margin_length <- lapply(dim(data[[i]]), function(x) 1 : x) +# margin_length[-margins[[i]]] <- "" +# margin_length <- expand.grid(margin_length, KEEP.OUT.ATTRS = FALSE, +# stringsAsFactors = FALSE) +# input[[i]] <- .isolate(data[[i]], margin_length) +# } +# dims <- dim(data[[1]])[margins[[1]]] +# i_max <- length(input[[1]])[1] / dims[[1]] +# k <- length(input[[1]]) / i_max +# if (parallel == TRUE) { +# registerDoParallel(ncores) +# } +# WrapperFun <- llply(1 : i_max, function(i) +# sapply((k * i - (k - 1)) : (k * i), function(x) splatted_f(lapply(input, `[[`, x),...), simplify = FALSE), +# .parallel = parallel) +# if (parallel == TRUE) { +# registerDoSEQ() +# } +# if (is.null(dim(WrapperFun[[1]][[1]]))) { +# WrapperFun <- array(as.numeric(unlist(WrapperFun)), dim=c(c(length((WrapperFun[[1]])[[1]])), +# dim(data[[1]])[margins[[1]]])) +# } else { +# WrapperFun <- array(as.numeric(unlist(WrapperFun)), dim=c(c(dim(WrapperFun[[1]][[1]])), +# dim(data[[1]])[margins[[1]]])) +# } +# } else { +# WrapperFun <- splatted_f(data, ...) +# } +# if (!is.null(dim(WrapperFun))) { +# names(dim(WrapperFun))[(length(dim(WrapperFun)) - length(names) + 1) : length(dim(WrapperFun))] <- c(names) +# } +# +# out <- WrapperFun } diff --git a/R/Utils.R b/R/zzz.R similarity index 100% rename from R/Utils.R rename to R/zzz.R -- GitLab From 0dce49e9c63e5d0e01073f91219e6b621f52fcf4 Mon Sep 17 00:00:00 2001 From: Nicolau Manubens Date: Thu, 19 Oct 2017 01:51:06 +0200 Subject: [PATCH 16/29] Progress. Needs debugging. --- R/Apply.R | 213 ++++++++++++++++++++++++++++++++---------------------- R/zzz.R | 57 +++++++++++++++ 2 files changed, 182 insertions(+), 88 deletions(-) diff --git a/R/Apply.R b/R/Apply.R index 8c7dd7c..f7e29e9 100644 --- a/R/Apply.R +++ b/R/Apply.R @@ -196,34 +196,34 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., margins = NULL, para # 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 - accumulated_found_margins <- afm <- list() + all_found_margins_lengths <- afml <- list() for (i in 1:length(data)) { if (!is.null(margins_names[[i]])) { - if (length(afm) > 0) { - matches <- which(margins_names[[i]] %in% names(afm)) + if (length(afml) > 0) { + matches <- which(margins_names[[i]] %in% names(afml)) if (length(matches) > 0) { margs_to_add <- as.list(dim(data[[i]])[margins[[i]]][- matches]) - if (any(dim(data[[i]])[margins[[i]][matches]] != unlist(afm[margins_names[[i]][matches]]))) { + if (any(dim(data[[i]])[margins[[i]][matches]] != unlist(afml[margins_names[[i]][matches]]))) { stop("Found one or more margin dimensions with the same name and ", "different length in some of the input objects in 'data'.") } } else { margs_to_add <- as.list(dim(data[[i]])[margins[[i]]]) } - unnamed_margins <- which(sapply(names(afm), nchar) == 0) + 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(afm[unnamed_margins]) != unlist(margs_to_add[1:length(unnamed_margins)]))) { + if (any(unlist(afml[unnamed_margins]) != unlist(margs_to_add[1:length(unnamed_margins)]))) { stop_with_error <- TRUE } - names(afm)[unnamed_margins] <- names(margs_to_add)[1:length(unnamed_margins)] + 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(afm[unnamed_margins[1:length(margs_to_add)]]) != unlist(margs_to_add))) { + if (any(unlist(afml[unnamed_margins[1:length(margs_to_add)]]) != unlist(margs_to_add))) { stop_with_error <- TRUE } - names(afm)[unnamed_margins[1:length(margs_to_add)]] <- names(margs_to_add) + names(afml)[unnamed_margins[1:length(margs_to_add)]] <- names(margs_to_add) margs_to_add <- list() } if (stop_with_error) { @@ -237,25 +237,25 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., margins = NULL, para "in a consistent order.") } } - afm <- c(afm, margs_to_add) + afml <- c(afml, margs_to_add) } else { - afm <- as.list(dim(data[[i]])[margins[[i]]]) + 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(afm) > 0) { + if (length(afml) > 0) { stop_with_error <- FALSE - if (length(afm) >= length(margs_to_add)) { - if (any(unlist(margs_to_add) != unlist(afm[1:length(margs_to_add)]))) { + 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(afm)] != unlist(afm))) { + if (any(unlist(margs_to_add)[1:length(afml)] != unlist(afml))) { stop_with_error <- TRUE } - margs_to_add <- margs_to_add[- (1:length(afm))] - afm <- c(afm, margs_to_add) + 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 ", @@ -268,83 +268,120 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., margins = NULL, para "in a consistent order.") } } else { - afm <- margs_to_add + afml <- margs_to_add } } } + # 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 missing, + # e.g. ''. # Now need to check which margins are common for all the data arrays. # Those will be used by llply. # For the margins that are not common, we will need to iterate manually # across them, and use data arrays repeatedly as needed. - common_margs <- c() - -# names <- names(dim(data[[1]]))[margins[[1]]] -# input <- list() -# -# splatted_f <- splat(AtomicFun) -# -# if (any(!sapply(margins, is.null))) { -# #Check margins match for input objects -# all_dims <- c(sapply(1 : length(data), -# function(x) sum(dim(data[[x]])[margins[[x]]]))) -# print(all_dims) -# pos_dim <- which.max(all_dims)[1] -# dim_template <- dim(data[[pos_dim]])[margins[[pos_dim]]] -# print(pos_dim) -# print(dim_template) -# for (i in 1 : length(data)) { -# if (identical(dim(data[[i]])[margins[[i]]], dim_template) == FALSE) { -# for (j in 1 : (length(dim(data[[i]])[margins[[i]]]))) { -# print("OK") -# print(c(dim(data[[i]])[margins[[i]]])[j] != dim_template[j]) -# if (c(dim(data[[i]])[margins[[i]]])[j] != dim_template[j]) { -# print(class(data[[i]])) -# data[[i]] <- InsertDim(data[[i]], posdim = margins[[i]][j], lendim = dim_template[j]) -# data[[i]] <- adrop(data[[i]], drop = (margins[[i]][j] + 1)) -# print(class(data[[i]])) -# } -# } -# } -# } -# print(dim(data)[[2]]) -# .isolate <- function(data, margin_length, drop = TRUE) { -# eval(dim(environment()$data)) -# structure(list(env = environment(), index = margin_length, subs = as.name("[")), -# class = c("indexed_array")) -# } -# for (i in 1 : length(data)) { -# margin_length <- lapply(dim(data[[i]]), function(x) 1 : x) -# margin_length[-margins[[i]]] <- "" -# margin_length <- expand.grid(margin_length, KEEP.OUT.ATTRS = FALSE, -# stringsAsFactors = FALSE) -# input[[i]] <- .isolate(data[[i]], margin_length) -# } -# dims <- dim(data[[1]])[margins[[1]]] -# i_max <- length(input[[1]])[1] / dims[[1]] -# k <- length(input[[1]]) / i_max -# if (parallel == TRUE) { -# registerDoParallel(ncores) -# } -# WrapperFun <- llply(1 : i_max, function(i) -# sapply((k * i - (k - 1)) : (k * i), function(x) splatted_f(lapply(input, `[[`, x),...), simplify = FALSE), -# .parallel = parallel) -# if (parallel == TRUE) { -# registerDoSEQ() -# } -# if (is.null(dim(WrapperFun[[1]][[1]]))) { -# WrapperFun <- array(as.numeric(unlist(WrapperFun)), dim=c(c(length((WrapperFun[[1]])[[1]])), -# dim(data[[1]])[margins[[1]]])) -# } else { -# WrapperFun <- array(as.numeric(unlist(WrapperFun)), dim=c(c(dim(WrapperFun[[1]][[1]])), -# dim(data[[1]])[margins[[1]]])) -# } -# } else { -# WrapperFun <- splatted_f(data, ...) -# } -# if (!is.null(dim(WrapperFun))) { -# names(dim(WrapperFun))[(length(dim(WrapperFun)) - length(names) + 1) : length(dim(WrapperFun))] <- c(names) -# } -# -# out <- WrapperFun + margins_afml <- margins + for (i in 1:length(dat)) { + if (!is.null(margins_names[[i]])) { + margins_afml[[i]] <- sapply(margins_names[[i]], + function(x) { + sapply(x, + function(y) { + which(names(afml) == y) + } + ) + } + ) + } + } + common_margs <- margins_afml[[1]] + if (length(margins_afml) > 1) { + for (i in 2:length(margins_afml)) { + non_matches <- which(!(margins_afml[[i]] %in% common_margs)) + if (length(non_matches) > 0) { + common_margs <- common_margs[- non_matches] + } + } + } + non_common_margs <- 1:length(afml) + if (length(common_margs) > 0) { + non_common_margs <- non_common_margs[- common_args] + } + # 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) + + .isolate <- function(data, margin_length, drop = TRUE) { + eval(dim(environment()$data)) + structure(list(env = environment(), index = margin_length, subs = as.name("[")), + class = c("indexed_array")) + } + splatted_f <- splat(AtomicFun) + + # Iterate along all non-common margins + non_common_margins_array <- ncma <- array(1:prod(unlist(afml[non_common_margs])), + dim = unlist(afml[non_common_margs])) + array_of_results <- vector('list', length(ncma)) + dim(array_of_results) <- dim(ncma) + for (j in 1:length(ncma)) { + marg_indices <- which(non_common_margins_array == j, arr.ind = TRUE)[1, ] + names(marg_indices) <- names(dim(ncma)) + input <- list() + for (i in 1:length(data)) { + indices_to_take <- as.list(rep(TRUE, length(dim(data[[i]])))) + inds_to_modify <- which(names(dim(data[[i]])) %in% names(marg_indices)) + if (length(inds_to_modify) > 0) { + indices_to_take[inds_to_modify] <- as.list(marg_indices[names(dim(data[[i]]))[inds_to_modify]]) + input[[i]] <- do.call('[', c(list(x = data[[i]]), indices_to_take, list(drop = FALSE))) + } else { + input[[i]] <- data[[i]] + } + } + # Each iteration of j, 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. + if (length(common_margs) > 0) { + max_size <- 0 + for (i in 1 : length(input)) { + margin_length <- lapply(dim(input[[i]]), function(x) 1 : x) + margin_length[-margins[[i]]] <- "" + margin_length <- expand.grid(margin_length, KEEP.OUT.ATTRS = FALSE, + stringsAsFactors = FALSE) + input[[i]] <- .isolate(input[[i]], margin_length) + if (prod(dim(input[[i]])) > max_size) { + max_size <- prod(dim(input[[i]])) + } + } + dims <- unlist(afml[common_margs]) + i_max <- max_size / dims[1] + k <- max_size / i_max + if (parallel == TRUE) { + registerDoParallel(ncores) + } + array_of_results[[j]] <- llply(1 : i_max, function(i) + sapply((k * i - (k - 1)) : (k * i), function(x) splatted_f(lapply(input, `[[`, x), ...), simplify = FALSE), + .parallel = parallel) + if (parallel == TRUE) { + registerDoSEQ() + } + #if (is.null(dim(WrapperFun[[1]][[1]]))) { + # WrapperFun <- array(as.numeric(unlist(WrapperFun)), dim=c(c(length((WrapperFun[[1]])[[1]])), + # dim(data[[1]])[margins[[1]]])) + #} else { + # WrapperFun <- array(as.numeric(unlist(WrapperFun)), dim=c(c(dim(WrapperFun[[1]][[1]])), + # dim(data[[1]])[margins[[1]]])) + #} + } else { + array_of_results[[j]] <- splatted_f(input, ...) + } + + for (component in 1:length(array_of_results[[j]])) { + names(dim(array_of_results[[j]][[component]])) <- c(rep('', length(dim(array_of_results[[j]][[component]])) - legth(afml)), + names(afml)) + } + } + + # Merge results + .MergeArrayOfArrays(array_of_results) } diff --git a/R/zzz.R b/R/zzz.R index 4404d32..d942a35 100644 --- a/R/zzz.R +++ b/R/zzz.R @@ -7,3 +7,60 @@ dim(x) <- old_dims[new_order] x } + +# Takes as input a list of arrays. The list must have named dimensions. +.MergeArrayOfArrays <- function(array_of_arrays) { + MergeArrays <- startR:::.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)))) + #do.call('[[<-', c(list(x = array_of_chunks), + # as.list(c(j, chunk_sub_indices)), + # list(value = NULL))) + if (is.null(new_chunk)) { + stop("Chunks missing.") + } + if (is.null(sub_array_of_chunks[[i]])) { + sub_array_of_chunks[[i]] <- new_chunk + } else { + #if (length(new_chunk) != length(sub_array_of_chunks[[i]])) { + # stop("Missing components for some chunks.") + #} + #for (component in 1:length(new_chunk)) { + 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]] +} -- GitLab From ee856d8d7696fd6d1c49f7bf83b6fb4c239d8714 Mon Sep 17 00:00:00 2001 From: Nicolau Manubens Date: Thu, 19 Oct 2017 02:50:41 +0200 Subject: [PATCH 17/29] Debug progress. --- R/Apply.R | 53 +++++++++++++++++++++------------ R/zzz.R | 87 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 121 insertions(+), 19 deletions(-) diff --git a/R/Apply.R b/R/Apply.R index f7e29e9..15a8412 100644 --- a/R/Apply.R +++ b/R/Apply.R @@ -281,7 +281,7 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., margins = NULL, para # For the margins that are not common, we will need to iterate manually # across them, and use data arrays repeatedly as needed. margins_afml <- margins - for (i in 1:length(dat)) { + for (i in 1:length(data)) { if (!is.null(margins_names[[i]])) { margins_afml[[i]] <- sapply(margins_names[[i]], function(x) { @@ -305,7 +305,7 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., margins = NULL, para } non_common_margs <- 1:length(afml) if (length(common_margs) > 0) { - non_common_margs <- non_common_margs[- common_args] + non_common_margs <- non_common_margs[- common_margs] } # common_margs is now a numeric vector with the indices of the common # margins (i.e. their position in afml) @@ -320,12 +320,16 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., margins = NULL, para splatted_f <- splat(AtomicFun) # Iterate along all non-common margins - non_common_margins_array <- ncma <- array(1:prod(unlist(afml[non_common_margs])), - dim = unlist(afml[non_common_margs])) + if (length(non_common_margs) > 0) { + non_common_margins_array <- ncma <- array(1:prod(unlist(afml[non_common_margs])), + dim = unlist(afml[non_common_margs])) + } else { + ncma <- array(1) + } array_of_results <- vector('list', length(ncma)) dim(array_of_results) <- dim(ncma) for (j in 1:length(ncma)) { - marg_indices <- which(non_common_margins_array == j, arr.ind = TRUE)[1, ] + marg_indices <- which(ncma == j, arr.ind = TRUE)[1, ] names(marg_indices) <- names(dim(ncma)) input <- list() for (i in 1:length(data)) { @@ -365,23 +369,36 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., margins = NULL, para if (parallel == TRUE) { registerDoSEQ() } - #if (is.null(dim(WrapperFun[[1]][[1]]))) { - # WrapperFun <- array(as.numeric(unlist(WrapperFun)), dim=c(c(length((WrapperFun[[1]])[[1]])), - # dim(data[[1]])[margins[[1]]])) - #} else { - # WrapperFun <- array(as.numeric(unlist(WrapperFun)), dim=c(c(dim(WrapperFun[[1]][[1]])), - # dim(data[[1]])[margins[[1]]])) - #} + for (component in 1:length(array_of_results[[j]])) { + print("AA") + print(length(afml)) + print(str(array_of_results[[j]])) + #if (length(common_margs) > 0) { + if (is.null(dim(array_of_results[[j]][[component]][[1]]))) { + array_of_results[[j]][[component]] <- array(unlist(array_of_results[[j]][[component]]), + dim = c(dim(array_of_results[[j]][[component]][[1]]), + setNames(rep(1, length(dim(ncma))), names(dim(ncma))), + unlist(afml[common_margs]))) + } else { + array_of_results[[j]][[component]] <- array(unlist(array_of_results[[j]][[component]]), + dim = c(length(array_of_results[[j]][[component]][[1]]), + setNames(rep(1, length(dim(ncma))), names(dim(ncma))), + unlist(afml[common_margs]))) + } + + #} + #names(dim(array_of_results[[j]][[component]])) <- c(rep('', length(dim(array_of_results[[j]][[component]])) - length(afml)), + # names(afml)) + } } else { array_of_results[[j]] <- splatted_f(input, ...) } - - for (component in 1:length(array_of_results[[j]])) { - names(dim(array_of_results[[j]][[component]])) <- c(rep('', length(dim(array_of_results[[j]][[component]])) - legth(afml)), - names(afml)) - } } # Merge results - .MergeArrayOfArrays(array_of_results) + if (length(array_of_results) > 1) { + .MergeArrayOfArrays(array_of_results) + } else { + array_of_results[[1]] + } } diff --git a/R/zzz.R b/R/zzz.R index d942a35..32fe628 100644 --- a/R/zzz.R +++ b/R/zzz.R @@ -8,9 +8,94 @@ 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 <- startR:::.MergeArrays + MergeArrays <- .MergeArrays array_dims <- (dim(array_of_arrays)) dim_names <- names(array_dims) -- GitLab From a79f50c6ae1ffc327dc7eff3ada236c0be9cc205 Mon Sep 17 00:00:00 2001 From: Nicolau Manubens Date: Thu, 19 Oct 2017 03:07:53 +0200 Subject: [PATCH 18/29] More debug. --- R/Apply.R | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/R/Apply.R b/R/Apply.R index 15a8412..b10caab 100644 --- a/R/Apply.R +++ b/R/Apply.R @@ -370,22 +370,19 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., margins = NULL, para registerDoSEQ() } for (component in 1:length(array_of_results[[j]])) { - print("AA") - print(length(afml)) - print(str(array_of_results[[j]])) #if (length(common_margs) > 0) { + component_dims <- c() if (is.null(dim(array_of_results[[j]][[component]][[1]]))) { - array_of_results[[j]][[component]] <- array(unlist(array_of_results[[j]][[component]]), - dim = c(dim(array_of_results[[j]][[component]][[1]]), - setNames(rep(1, length(dim(ncma))), names(dim(ncma))), - unlist(afml[common_margs]))) + component_dims <- length(array_of_results[[j]][[component]][[1]]) } else { - array_of_results[[j]][[component]] <- array(unlist(array_of_results[[j]][[component]]), - dim = c(length(array_of_results[[j]][[component]][[1]]), - setNames(rep(1, length(dim(ncma))), names(dim(ncma))), - unlist(afml[common_margs]))) + component_dims <- dim(array_of_results[[j]][[component]][[1]]) } - + if (length(non_common_margs) > 0) { + component_dims <- c(component_dims, setNames(rep(1, length(dim(ncma))), names(dim(ncma)))) + } + component_dims <- c(component_dims, unlist(afml[common_margs])) + array_of_results[[j]][[component]] <- array(unlist(array_of_results[[j]][[component]]), + dim = component_dims) #} #names(dim(array_of_results[[j]][[component]])) <- c(rep('', length(dim(array_of_results[[j]][[component]])) - length(afml)), # names(afml)) -- GitLab From b5d797f7b078fc700130cce0c57126175ae9ea03 Mon Sep 17 00:00:00 2001 From: Nicolau Manubens Date: Fri, 20 Oct 2017 04:30:57 +0200 Subject: [PATCH 19/29] Debug progress. --- R/Apply.R | 181 +++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 130 insertions(+), 51 deletions(-) diff --git a/R/Apply.R b/R/Apply.R index b10caab..5e10eaf 100644 --- a/R/Apply.R +++ b/R/Apply.R @@ -21,7 +21,8 @@ #' (sum(y > z) / (length(y)))) * 100} #' margins = list(c(1, 2), c(1, 2), c(1,2)) #' test <- Apply(data, margins, AtomicFun = "test_fun") -Apply <- function(data, target_dims = NULL, AtomicFun, ..., margins = NULL, parallel = FALSE, ncores = NULL) { +Apply <- function(data, target_dims = NULL, AtomicFun, ..., output_dims = NULL, + margins = NULL, parallel = FALSE, ncores = NULL) { # Check data if (!is.list(data)) { data <- list(data) @@ -36,6 +37,28 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., margins = NULL, para dim(data[[i]]) <- length(data[[i]]) } } + + # Check AtomicFun + if (is.character(AtomicFun)) { + try({AtomicFun <- get(AtomicFun)}, silent = TRUE) + if (!is.function(AtomicFun)) { + stop("Could not find the function '", AtomicFun, "'.") + } + } + if (!is.function(AtomicFun)) { + stop("Parameter 'AtomicFun' must be a function or a character string ", + "with the name of a function.") + } + output_dims <- NULL + if ('startR_step' %in% class(AtomicFun)) { + if (is.null(target_dims)) { + target_dims <- attr(AtomicFun, 'target_dims') + } + if (is.null(output_dims)) { + output_dims <- attr(AtomicFun, 'target_dims') + } + } + # Check target_dims and margins if (is.null(margins) && is.null(target_dims)) { stop("One of 'margins' or 'target_dims' must be specified.") @@ -158,16 +181,21 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., margins = NULL, para } } - # Check AtomicFun - if (is.character(AtomicFun)) { - try({AtomicFun <- get(AtomicFun)}, silent = TRUE) - if (!is.function(AtomicFun)) { - stop("Could not find the function '", AtomicFun, "'.") + # Check output_dims + if (!is.null(output_dims)) { + if (!is.list(output_dims)) { + output_dims <- list(output1 = output_dims) + } + if (any(sapply(output_dims, function(x) !is.character(x)))) { + stop("Parameter 'output_dims' must be one or a list of vectors of character strings.") + } + if (is.null(names(output_dims))) { + names(output_dims) <- rep('', length(output_dims)) + } + missing_output_names <- which(sapply(names(output_dims), nchar) == 0) + if (length(missing_output_names) > 0) { + names(output_dims)[missing_output_names] <- paste0('output', missing_output_names) } - } - if (!is.function(AtomicFun)) { - stop("Parameter 'AtomicFun' must be a function or a character string ", - "with the name of a function.") } # Check parallel @@ -312,11 +340,11 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., margins = NULL, para # 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 = TRUE) { - eval(dim(environment()$data)) - structure(list(env = environment(), index = margin_length, subs = as.name("[")), - class = c("indexed_array")) - } + #.isolate <- function(data, margin_length, drop = TRUE) { + # eval(dim(environment()$data)) + # structure(list(env = environment(), index = margin_length, subs = as.name("[")), + # class = c("indexed_array")) + #} splatted_f <- splat(AtomicFun) # Iterate along all non-common margins @@ -326,18 +354,25 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., margins = NULL, para } else { ncma <- array(1) } - array_of_results <- vector('list', length(ncma)) - dim(array_of_results) <- dim(ncma) + arrays_of_results <- NULL + found_first_result <- FALSE +print("AAA") +print(afml[common_margs]) +print("BBB") +print(afml[non_common_margs]) for (j in 1:length(ncma)) { marg_indices <- which(ncma == j, arr.ind = TRUE)[1, ] names(marg_indices) <- names(dim(ncma)) input <- list() + atomic_fun_out_dims <- list() for (i in 1:length(data)) { indices_to_take <- as.list(rep(TRUE, length(dim(data[[i]])))) inds_to_modify <- which(names(dim(data[[i]])) %in% names(marg_indices)) if (length(inds_to_modify) > 0) { indices_to_take[inds_to_modify] <- as.list(marg_indices[names(dim(data[[i]]))[inds_to_modify]]) input[[i]] <- do.call('[', c(list(x = data[[i]]), indices_to_take, list(drop = FALSE))) + names(dim(input[[i]])) <- names(dim(data[[i]])) + dim(input[[i]]) <- dim(input[[i]])[- inds_to_modify] } else { input[[i]] <- data[[i]] } @@ -346,56 +381,100 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., margins = NULL, para # each object in data (if possible). For each set of 'input's, the # splatted_f is applied in parallel if possible. if (length(common_margs) > 0) { - max_size <- 0 +print("EEEPPP") + #max_size <- 0 for (i in 1 : length(input)) { - margin_length <- lapply(dim(input[[i]]), function(x) 1 : x) - margin_length[-margins[[i]]] <- "" - margin_length <- expand.grid(margin_length, KEEP.OUT.ATTRS = FALSE, - stringsAsFactors = FALSE) - input[[i]] <- .isolate(input[[i]], margin_length) - if (prod(dim(input[[i]])) > max_size) { - max_size <- prod(dim(input[[i]])) - } + #margin_length <- lapply(dim(input[[i]]), function(x) 1 : x) + #margin_length[-margins[[i]]] <- "" + #margin_length <- expand.grid(margin_length, KEEP.OUT.ATTRS = FALSE, + # stringsAsFactors = FALSE) +print(str(input[[i]])) + #input[[i]] <- .isolate(input[[i]], margin_length) +print(str(input[[i]])) +stop() + #if (prod(dim(input[[i]])) > max_size) { + # max_size <- prod(dim(input[[i]])) + #} + dimnames(input[[i]]) <- Map(paste0, letters[seq_along(dim(input[[i]]))], + lapply(dim(input[[i]]), seq)) + input[[i]] <- alply(input[[i]], margins[[i]], .dims = TRUE) } dims <- unlist(afml[common_margs]) - i_max <- max_size / dims[1] + #i_max <- max_size / dims[1] + #k <- max_size / i_max + max_size <- prod(dims) + i_max <- max_size / max(dims)[1] k <- max_size / i_max if (parallel == TRUE) { registerDoParallel(ncores) } - array_of_results[[j]] <- llply(1 : i_max, function(i) +print(i_max) + result <- llply(1 : i_max, function(i) sapply((k * i - (k - 1)) : (k * i), function(x) splatted_f(lapply(input, `[[`, x), ...), simplify = FALSE), .parallel = parallel) if (parallel == TRUE) { registerDoSEQ() } - for (component in 1:length(array_of_results[[j]])) { - #if (length(common_margs) > 0) { - component_dims <- c() - if (is.null(dim(array_of_results[[j]][[component]][[1]]))) { - component_dims <- length(array_of_results[[j]][[component]][[1]]) - } else { - component_dims <- dim(array_of_results[[j]][[component]][[1]]) - } - if (length(non_common_margs) > 0) { - component_dims <- c(component_dims, setNames(rep(1, length(dim(ncma))), names(dim(ncma)))) - } - component_dims <- c(component_dims, unlist(afml[common_margs])) - array_of_results[[j]][[component]] <- array(unlist(array_of_results[[j]][[component]]), - dim = component_dims) - #} - #names(dim(array_of_results[[j]][[component]])) <- c(rep('', length(dim(array_of_results[[j]][[component]])) - length(afml)), - # names(afml)) - } + result <- list(array(unlist(result), dim = c(dim(result[[1]][[1]]), unlist(afml[common_margs])))) } else { - array_of_results[[j]] <- splatted_f(input, ...) + result <- list(splatted_f(input, ...)) + } + if (!found_first_result) { + array_of_results <- vector('list', length(ncma)) + dim(array_of_results) <- dim(ncma) + arrays_of_results <- replicate(length(result), array_of_results, simplify = FALSE) + if (!is.null(output_dims)) { + # Check number of outputs is correct. + if (length(output_dims) != length(arrays_of_results)) { + stop("The 'AtomicFun' returns ", length(arrays_of_results), " elements, but ", + length(output_dims), " elements were expected.") + } + names(arrays_of_results) <- names(output_dims) + } else { + names(arrays_of_results) <- paste0('output', 1:length(result)) + } + rm(array_of_results) + found_first_result <- TRUE + } + for (component in 1:length(result)) { + if (is.null(dim(result[[component]]))) { + component_dims <- length(result[[component]]) + } else { + component_dims <- dim(result[[component]]) + if (length(common_margs) > 0) { + component_dims <- component_dims[1:(length(component_dims) - length(common_margs))] + } + } + atomic_fun_out_dims[[component]] <- component_dims + if (length(non_common_margs) > 0) { + component_dims <- c(component_dims, setNames(rep(1, length(dim(ncma))), names(dim(ncma)))) + } + component_dims <- c(component_dims, unlist(afml[common_margs])) + dim(result[[component]]) <- component_dims + arrays_of_results[[component]][[j]] <- result[[component]] + } + if (!is.null(output_dims)) { + # Check number of output dimensions is correct. + for (component in 1:length(atomic_fun_out_dims)) { + if (length(atomic_fun_out_dims[[component]]) != output_dims[[component]]) { + stop("Expected ", component, "st returned element by 'AtomicFun'", + "to have ", length(output_dims[[component]]), " dimensions, ", + "but ", length(atomic_fun_out_dims[[component]]), " found.") + } + if (!is.null(names(atomic_fun_out_dims[[component]]))) { + # check component_dims match names of output_dims[[component]], and reorder if needed + } + } } } # Merge results - if (length(array_of_results) > 1) { - .MergeArrayOfArrays(array_of_results) - } else { - array_of_results[[1]] + for (component in 1:length(arrays_of_results)) { + if (length(arrays_of_results[[component]]) > 1) { + arrays_of_results[[component]] <- .MergeArrayOfArrays(arrays_of_results[[component]]) + } else { + arrays_of_results[[component]] <- arrays_of_results[[component]][[1]] + } } + arrays_of_results } -- GitLab From a52750818c82af9df1d127f9eecff033cd390b23 Mon Sep 17 00:00:00 2001 From: Nicolau Manubens Date: Mon, 23 Oct 2017 03:51:48 +0200 Subject: [PATCH 20/29] Progress. --- R/Apply.R | 179 +++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 123 insertions(+), 56 deletions(-) diff --git a/R/Apply.R b/R/Apply.R index 5e10eaf..55eb638 100644 --- a/R/Apply.R +++ b/R/Apply.R @@ -69,7 +69,6 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., output_dims = NULL, margins_names <- vector('list', length(data)) target_dims_names <- vector('list', length(data)) if (!is.null(margins)) { - # Check margins and build target_dims accordingly if (!is.list(margins)) { margins <- rep(list(margins), length(data)) @@ -325,7 +324,7 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., output_dims = NULL, common_margs <- margins_afml[[1]] if (length(margins_afml) > 1) { for (i in 2:length(margins_afml)) { - non_matches <- which(!(margins_afml[[i]] %in% common_margs)) + non_matches <- which(!(common_margs %in% margins_afml[[i]])) if (length(non_matches) > 0) { common_margs <- common_margs[- non_matches] } @@ -340,11 +339,43 @@ 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 = TRUE) { - # eval(dim(environment()$data)) - # structure(list(env = environment(), index = margin_length, subs = as.name("[")), - # class = c("indexed_array")) - #} + .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_dimnames) { + lapply(1:length(subsets), + function(x) { + dims <- dim(subsets[[x]]) + names(dims) <- dimnames[[x]] + dims <- dims[out_dimnames[[x]]] + array(subsets[[x]], dim = dims) + }) + } + + data_indexed <- vector('list', length(data)) + data_indexed_indices <- vector('list', length(data)) + for (i in 1 : length(data)) { + non_common_margs_i <- which(names(dim(data[[i]])) %in% names(afml[non_common_margs])) + if (length(non_common_margs_i) > 0) { + margin_length <- lapply(dim(data[[i]]), function(x) 1 : x) + margin_length[- non_common_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(non_common_margs_i) > 0) { + data_indexed_indices[[i]] <- array(1:prod(dim(data[[i]])[non_common_margs_i]), + dim = dim(data[[i]])[non_common_margs_i]) + } else { + data_indexed_indices[[i]] <- array(1, dim = 1) + } + } + splatted_f <- splat(AtomicFun) # Iterate along all non-common margins @@ -356,73 +387,99 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., output_dims = NULL, } arrays_of_results <- NULL found_first_result <- FALSE -print("AAA") -print(afml[common_margs]) -print("BBB") -print(afml[non_common_margs]) +# need to parallelize this loop if no common margins or small common margins +# need to add progress bar +# need to use indexed arrays instead of arrays for (j in 1:length(ncma)) { - marg_indices <- which(ncma == j, arr.ind = TRUE)[1, ] +if (j %% 1000 == 0) { + print(j) +} + marg_indices <- arrayInd(j, dim(ncma)) + #marg_indices <- which(ncma == j, arr.ind = TRUE)[1, ] names(marg_indices) <- names(dim(ncma)) input <- list() atomic_fun_out_dims <- list() - for (i in 1:length(data)) { - indices_to_take <- as.list(rep(TRUE, length(dim(data[[i]])))) - inds_to_modify <- which(names(dim(data[[i]])) %in% names(marg_indices)) - if (length(inds_to_modify) > 0) { - indices_to_take[inds_to_modify] <- as.list(marg_indices[names(dim(data[[i]]))[inds_to_modify]]) - input[[i]] <- do.call('[', c(list(x = data[[i]]), indices_to_take, list(drop = FALSE))) - names(dim(input[[i]])) <- names(dim(data[[i]])) - dim(input[[i]]) <- dim(input[[i]])[- inds_to_modify] - } else { - input[[i]] <- data[[i]] - } - } # Each iteration of j, 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. if (length(common_margs) > 0) { -print("EEEPPP") - #max_size <- 0 - for (i in 1 : length(input)) { - #margin_length <- lapply(dim(input[[i]]), function(x) 1 : x) - #margin_length[-margins[[i]]] <- "" - #margin_length <- expand.grid(margin_length, KEEP.OUT.ATTRS = FALSE, - # stringsAsFactors = FALSE) -print(str(input[[i]])) - #input[[i]] <- .isolate(input[[i]], margin_length) -print(str(input[[i]])) -stop() - #if (prod(dim(input[[i]])) > max_size) { - # max_size <- prod(dim(input[[i]])) - #} - dimnames(input[[i]]) <- Map(paste0, letters[seq_along(dim(input[[i]]))], - lapply(dim(input[[i]]), seq)) - input[[i]] <- alply(input[[i]], margins[[i]], .dims = TRUE) + input_indexed <- vector('list', length(data)) + input_indexed_indices <- vector('list', length(data)) + 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) { + input[[i]] <- data_indexed[[i]][[do.call('[', c(list(x = data_indexed_indices[[i]]), + marg_indices[inds_to_take], + list(drop = TRUE)))]] + } else { + input[[i]] <- data_indexed[[i]][[1]] + } + ## + common_margs_i <- which(names(dim(data[[i]])) %in% names(afml[common_margs])) + if (length(common_margs_i) > 0) { + margin_length <- lapply(dim(data[[i]]), function(x) 1 : x) + margin_length[- common_margs_i] <- "" + } else { + margin_length <- as.list(rep("", length(dim(data[[i]])))) + } + margin_length <- expand.grid(margin_length, KEEP.OUT.ATTRS = FALSE, + stringsAsFactors = FALSE) + input_indexed[[i]] <- .isolate(input[[i]], margin_length) + ## + if (length(common_margs_i) > 0) { + input_indexed_indices[[i]] <- array(1:prod(dim(data[[i]])[common_margs_i]), + dim = dim(data[[i]])[common_margs_i]) + } else { + input_indexed_indices[[i]] <- array(1, dim = 1) + } } dims <- unlist(afml[common_margs]) - #i_max <- max_size / dims[1] - #k <- max_size / i_max + selected_dim <- which(dims != 1) + if (length(selected_dim) > 0) { + selected_dim <- selected_dim[1] + } else { + selected_dim <- 1 + } max_size <- prod(dims) - i_max <- max_size / max(dims)[1] + i_max <- max_size / dims[selected_dim] k <- max_size / i_max if (parallel == TRUE) { - registerDoParallel(ncores) + registerDoParallel(ncores) } -print(i_max) - result <- llply(1 : i_max, function(i) - sapply((k * i - (k - 1)) : (k * i), function(x) splatted_f(lapply(input, `[[`, x), ...), simplify = FALSE), - .parallel = parallel) + result <- llply(1 : i_max, + function(i) { + sapply((k * i - (k - 1)) : (k * i), + function(x) { + splatted_f(.consolidate(lapply(input_indexed, `[[`, x), + lapply(lapply(data, dim), names), + target_dims_names), + ...) + }, simplify = FALSE) + }, .parallel = parallel) if (parallel == TRUE) { registerDoSEQ() } - result <- list(array(unlist(result), dim = c(dim(result[[1]][[1]]), unlist(afml[common_margs])))) + result <- list(array(unlist(result), + dim = c(dim(result[[1]][[1]]), unlist(afml[common_margs])))) } else { - result <- list(splatted_f(input, ...)) + 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) { + input[[i]] <- data_indexed[[i]][[do.call('[', c(list(x = data_indexed_indices[[i]]), + marg_indices[inds_to_take], + list(drop = TRUE)))]] + } else { + input[[i]] <- data_indexed[[i]][[1]] + } + } + result <- splatted_f(.consolidate(input, lapply(lapply(data, dim), names), target_dims_names), ...) + if (!is.list(result)) { + result <- list(result) + } } if (!found_first_result) { - array_of_results <- vector('list', length(ncma)) - dim(array_of_results) <- dim(ncma) - arrays_of_results <- replicate(length(result), array_of_results, simplify = FALSE) + arrays_of_results <- vector('list', length(result)) if (!is.null(output_dims)) { # Check number of outputs is correct. if (length(output_dims) != length(arrays_of_results)) { @@ -430,11 +487,11 @@ print(i_max) length(output_dims), " elements were expected.") } names(arrays_of_results) <- names(output_dims) + } else if (!is.null(names(result))) { + names(arrays_of_results) <- names(result) } else { names(arrays_of_results) <- paste0('output', 1:length(result)) } - rm(array_of_results) - found_first_result <- TRUE } for (component in 1:length(result)) { if (is.null(dim(result[[component]]))) { @@ -451,8 +508,18 @@ print(i_max) } component_dims <- c(component_dims, unlist(afml[common_margs])) dim(result[[component]]) <- component_dims + if (!found_first_result) { + component_array <- array(dim = dim(result[[component]])) + array_of_results <- replicate(length(ncma), component_array, simplify = FALSE) + dim(array_of_results) <- dim(ncma) + arrays_of_results[[component]] <- array_of_results + rm(array_of_results) + } arrays_of_results[[component]][[j]] <- result[[component]] } + if (!found_first_result) { + found_first_result <- TRUE + } if (!is.null(output_dims)) { # Check number of output dimensions is correct. for (component in 1:length(atomic_fun_out_dims)) { -- GitLab From a8a65f18b94eab1dc8c97e644405d29cc56e9837 Mon Sep 17 00:00:00 2001 From: Nicolau Manubens Date: Mon, 23 Oct 2017 15:04:32 +0200 Subject: [PATCH 21/29] Adapted to support multiple outputs and to work with non-common margins, without duplicating inputs. Output dimension names are preserved. --- R/Apply.R | 155 +++++++++++++++--------------------------------------- 1 file changed, 42 insertions(+), 113 deletions(-) diff --git a/R/Apply.R b/R/Apply.R index 55eb638..bc110f5 100644 --- a/R/Apply.R +++ b/R/Apply.R @@ -358,19 +358,19 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., output_dims = NULL, data_indexed <- vector('list', length(data)) data_indexed_indices <- vector('list', length(data)) for (i in 1 : length(data)) { - non_common_margs_i <- which(names(dim(data[[i]])) %in% names(afml[non_common_margs])) - if (length(non_common_margs_i) > 0) { + margs_i <- which(names(dim(data[[i]])) %in% names(afml[c(non_common_margs, common_margs)])) + if (length(margs_i) > 0) { margin_length <- lapply(dim(data[[i]]), function(x) 1 : x) - margin_length[- non_common_margs_i] <- "" + 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(non_common_margs_i) > 0) { - data_indexed_indices[[i]] <- array(1:prod(dim(data[[i]])[non_common_margs_i]), - dim = dim(data[[i]])[non_common_margs_i]) + 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) } @@ -379,109 +379,42 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., output_dims = NULL, splatted_f <- splat(AtomicFun) # Iterate along all non-common margins - if (length(non_common_margs) > 0) { - non_common_margins_array <- ncma <- array(1:prod(unlist(afml[non_common_margs])), - dim = unlist(afml[non_common_margs])) + 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])) } else { - ncma <- array(1) + ma <- array(1) } arrays_of_results <- NULL found_first_result <- FALSE -# need to parallelize this loop if no common margins or small common margins # need to add progress bar -# need to use indexed arrays instead of arrays - for (j in 1:length(ncma)) { -if (j %% 1000 == 0) { - print(j) -} - marg_indices <- arrayInd(j, dim(ncma)) - #marg_indices <- which(ncma == j, arr.ind = TRUE)[1, ] - names(marg_indices) <- names(dim(ncma)) + iteration <- function(j) { + marg_indices <- arrayInd(j, dim(ma)) + names(marg_indices) <- names(dim(ma)) input <- list() atomic_fun_out_dims <- list() # Each iteration of j, 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. - if (length(common_margs) > 0) { - input_indexed <- vector('list', length(data)) - input_indexed_indices <- vector('list', length(data)) - 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) { - input[[i]] <- data_indexed[[i]][[do.call('[', c(list(x = data_indexed_indices[[i]]), - marg_indices[inds_to_take], - list(drop = TRUE)))]] - } else { - input[[i]] <- data_indexed[[i]][[1]] - } - ## - common_margs_i <- which(names(dim(data[[i]])) %in% names(afml[common_margs])) - if (length(common_margs_i) > 0) { - margin_length <- lapply(dim(data[[i]]), function(x) 1 : x) - margin_length[- common_margs_i] <- "" - } else { - margin_length <- as.list(rep("", length(dim(data[[i]])))) - } - margin_length <- expand.grid(margin_length, KEEP.OUT.ATTRS = FALSE, - stringsAsFactors = FALSE) - input_indexed[[i]] <- .isolate(input[[i]], margin_length) - ## - if (length(common_margs_i) > 0) { - input_indexed_indices[[i]] <- array(1:prod(dim(data[[i]])[common_margs_i]), - dim = dim(data[[i]])[common_margs_i]) - } else { - input_indexed_indices[[i]] <- array(1, dim = 1) - } - } - dims <- unlist(afml[common_margs]) - selected_dim <- which(dims != 1) - if (length(selected_dim) > 0) { - selected_dim <- selected_dim[1] + 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) { + input[[i]] <- data_indexed[[i]][[do.call('[', c(list(x = data_indexed_indices[[i]]), + marg_indices[inds_to_take], + list(drop = TRUE)))]] } else { - selected_dim <- 1 - } - max_size <- prod(dims) - i_max <- max_size / dims[selected_dim] - k <- max_size / i_max - if (parallel == TRUE) { - registerDoParallel(ncores) - } - result <- llply(1 : i_max, - function(i) { - sapply((k * i - (k - 1)) : (k * i), - function(x) { - splatted_f(.consolidate(lapply(input_indexed, `[[`, x), - lapply(lapply(data, dim), names), - target_dims_names), - ...) - }, simplify = FALSE) - }, .parallel = parallel) - if (parallel == TRUE) { - registerDoSEQ() - } - result <- list(array(unlist(result), - dim = c(dim(result[[1]][[1]]), unlist(afml[common_margs])))) - } else { - 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) { - input[[i]] <- data_indexed[[i]][[do.call('[', c(list(x = data_indexed_indices[[i]]), - marg_indices[inds_to_take], - list(drop = TRUE)))]] - } else { - input[[i]] <- data_indexed[[i]][[1]] - } - } - result <- splatted_f(.consolidate(input, lapply(lapply(data, dim), names), target_dims_names), ...) - if (!is.list(result)) { - result <- list(result) + input[[i]] <- data_indexed[[i]][[1]] } } + result <- splatted_f(.consolidate(input, lapply(lapply(data, dim), names), + target_dims_names), ...) + if (!is.list(result)) { + result <- list(result) + } if (!found_first_result) { arrays_of_results <- vector('list', length(result)) if (!is.null(output_dims)) { - # Check number of outputs is correct. if (length(output_dims) != length(arrays_of_results)) { stop("The 'AtomicFun' returns ", length(arrays_of_results), " elements, but ", length(output_dims), " elements were expected.") @@ -498,24 +431,15 @@ if (j %% 1000 == 0) { component_dims <- length(result[[component]]) } else { component_dims <- dim(result[[component]]) - if (length(common_margs) > 0) { - component_dims <- component_dims[1:(length(component_dims) - length(common_margs))] - } } atomic_fun_out_dims[[component]] <- component_dims - if (length(non_common_margs) > 0) { - component_dims <- c(component_dims, setNames(rep(1, length(dim(ncma))), names(dim(ncma)))) - } - component_dims <- c(component_dims, unlist(afml[common_margs])) + component_dims <- c(component_dims, setNames(rep(1, length(dim(ma))), names(dim(ma)))) dim(result[[component]]) <- component_dims if (!found_first_result) { - component_array <- array(dim = dim(result[[component]])) - array_of_results <- replicate(length(ncma), component_array, simplify = FALSE) - dim(array_of_results) <- dim(ncma) - arrays_of_results[[component]] <- array_of_results - rm(array_of_results) + arrays_of_results[[component]] <- array(dim = c(dim(result[[component]]), dim(ma))) } - arrays_of_results[[component]][[j]] <- result[[component]] + arrays_of_results[[component]][(1:prod(component_dims)) + + (j - 1) * prod(component_dims)] <- result[[component]] } if (!found_first_result) { found_first_result <- TRUE @@ -533,15 +457,20 @@ if (j %% 1000 == 0) { } } } + TRUE } - # Merge results - for (component in 1:length(arrays_of_results)) { - if (length(arrays_of_results[[component]]) > 1) { - arrays_of_results[[component]] <- .MergeArrayOfArrays(arrays_of_results[[component]]) - } else { - arrays_of_results[[component]] <- arrays_of_results[[component]][[1]] - } + # Execute in parallel if needed + if (parallel) { + registerDoParallel(ncores) + j <- seq(length(ma)) + fe <- eval(as.call(list(quote(foreach::foreach), j = j))) + info <- foreach::`%dopar%`(fe, iteration(j)) + registerDoSEQ() + } else { + info <- sapply(seq(length(ma)), iteration) } + + # Return arrays_of_results } -- GitLab From 2ef7190a1eb5110c668fe6ae64b699e54687d9f8 Mon Sep 17 00:00:00 2001 From: Nicolau Manubens Date: Mon, 23 Oct 2017 23:07:28 +0200 Subject: [PATCH 22/29] Mostly working. --- R/Apply.R | 234 ++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 165 insertions(+), 69 deletions(-) diff --git a/R/Apply.R b/R/Apply.R index bc110f5..5fca345 100644 --- a/R/Apply.R +++ b/R/Apply.R @@ -31,11 +31,20 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., output_dims = NULL, stop("Parameter 'data' must be one or a list of numeric objects.") } is_vector <- rep(FALSE, length(data)) + is_unnamed <- rep(FALSE, length(data)) for (i in 1 : length(data)) { if (is.null(dim(data[[i]]))) { is_vector[i] <- TRUE dim(data[[i]]) <- length(data[[i]]) } + if (!is.null(names(dim(data)))) { + if (any(sapply(names(dim(data)), nchar) == 0)) { + stop("Dimension names of arrays in 'data' must be at least ", + "one character long.") + } + } else { + is_unnamed[i] <- TRUE + } } # Check AtomicFun @@ -299,9 +308,14 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., output_dims = NULL, } } } + 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 missing, - # e.g. ''. + # appears once only. If some names are not provided, they are set automatically + # to 'unnamed_dim_1', 'unamed_dim_2', ... # Now need to check which margins are common for all the data arrays. # Those will be used by llply. @@ -310,6 +324,7 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., output_dims = NULL, margins_afml <- margins for (i in 1:length(data)) { if (!is.null(margins_names[[i]])) { + margins_afml[[i]] <- sapply(margins_names[[i]], function(x) { sapply(x, @@ -319,14 +334,23 @@ 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) { for (i in 2:length(margins_afml)) { - non_matches <- which(!(common_margs %in% margins_afml[[i]])) - if (length(non_matches) > 0) { - common_margs <- common_margs[- non_matches] + margs_a <- unlist(afml[common_margs]) + margs_b <- unlist(afml[margins_afml[[i]]]) + matches <- which(names(margs_a) %in% names(margs_b)) + if (length(matches) > 0) { + common_margs <- common_margs[matches] + } else { + common_margs <- NULL } } } @@ -345,12 +369,14 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., output_dims = NULL, drop = drop, subs = as.name("[")), class = c("indexed_array")) } - .consolidate <- function(subsets, dimnames, out_dimnames) { + .consolidate <- function(subsets, dimnames, out_dims) { lapply(1:length(subsets), function(x) { dims <- dim(subsets[[x]]) - names(dims) <- dimnames[[x]] - dims <- dims[out_dimnames[[x]]] + if (!is_unnamed[x]) { + names(dims) <- dimnames[[x]] + } + dims <- dims[out_dims[[x]]] array(subsets[[x]], dim = dims) }) } @@ -388,87 +414,157 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., output_dims = NULL, } arrays_of_results <- NULL found_first_result <- FALSE + + total_size <- prod(dim(ma)) + if (!is.null(ncores)) { + chunk_size <- round(total_size / (ncores * 4)) + } else { + chunk_size <- 4 + } + if (chunk_size < 1) { + chunk_size <- 1 + } + nchunks <- floor(total_size / chunk_size) + chunk_sizes <- rep(chunk_size, nchunks) + if (total_size %% chunk_size != 0) { + chunk_sizes <- c(chunk_sizes, total_size %% chunk_size) + } + # need to add progress bar - iteration <- function(j) { - marg_indices <- arrayInd(j, dim(ma)) - names(marg_indices) <- names(dim(ma)) - input <- list() - atomic_fun_out_dims <- list() - # Each iteration of j, 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) { - input[[i]] <- data_indexed[[i]][[do.call('[', c(list(x = data_indexed_indices[[i]]), - marg_indices[inds_to_take], - list(drop = TRUE)))]] - } else { - input[[i]] <- data_indexed[[i]][[1]] + iteration <- function(m) { + sub_arrays_of_results <- list() + 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 + j <- n + (m - 1) * chunk_size + marg_indices <- arrayInd(j, dim(ma)) + names(marg_indices) <- names(dim(ma)) + input <- list() + atomic_fun_out_dims <- 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) { + input[[i]] <- data_indexed[[i]][[do.call('[', c(list(x = data_indexed_indices[[i]]), + marg_indices[inds_to_take], + list(drop = TRUE)))]] + } else { + input[[i]] <- data_indexed[[i]][[1]] + } + } + result <- splatted_f(.consolidate(input, lapply(lapply(data, dim), names), + target_dims), ...) + if (!is.list(result)) { + result <- list(result) + } + if (!found_first_sub_result) { + 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), + " elements, but ", length(output_dims), + " elements were expected.") + } + names(sub_arrays_of_results) <- names(output_dims) + } else if (!is.null(names(result))) { + names(sub_arrays_of_results) <- names(result) + } else { + names(sub_arrays_of_results) <- paste0('output', 1:length(result)) + } + } + for (component in 1:length(result)) { + if (is.null(dim(result[[component]]))) { + if (length(result[[component]] == 1)) { + component_dims <- NULL + } else { + component_dims <- length(result[[component]]) + } + } else { + component_dims <- dim(result[[component]]) + } + if (!found_first_sub_result) { + sub_arrays_of_results[[component]] <- array(dim = c(component_dims, chunk_sizes[m])) + } + atomic_fun_out_dims[[component]] <- component_dims + sub_arrays_of_results[[component]][(1:prod(component_dims)) + + (n - 1) * prod(component_dims)] <- result[[component]] + } + if (!found_first_sub_result) { + found_first_sub_result <- TRUE + } + if (!is.null(output_dims)) { + # Check number of output dimensions is correct. + for (component in 1:length(atomic_fun_out_dims)) { + if (length(atomic_fun_out_dims[[component]]) != output_dims[[component]]) { + stop("Expected ", component, "st returned element by 'AtomicFun'", + "to have ", length(output_dims[[component]]), " dimensions, ", + "but ", length(atomic_fun_out_dims[[component]]), " found.") + } + } } } - result <- splatted_f(.consolidate(input, lapply(lapply(data, dim), names), - target_dims_names), ...) - if (!is.list(result)) { - result <- list(result) - } + sub_arrays_of_results + } + + # Execute in parallel if needed + if (parallel) registerDoParallel(ncores) + result <- llply(1:length(chunk_sizes), iteration, .parallel = parallel) + if (parallel) registerDoSEQ() + + # Merge the results + chunk_length <- NULL + fun_out_dims <- NULL + for (m in 1:length(result)) { if (!found_first_result) { - arrays_of_results <- vector('list', length(result)) + 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 ", length(output_dims), " elements were expected.") } names(arrays_of_results) <- names(output_dims) - } else if (!is.null(names(result))) { - names(arrays_of_results) <- names(result) + } else if (!is.null(names(result[[1]]))) { + names(arrays_of_results) <- names(result[[1]]) } else { - names(arrays_of_results) <- paste0('output', 1:length(result)) + names(arrays_of_results) <- paste0('output', 1:length(result[[1]])) } } - for (component in 1:length(result)) { - if (is.null(dim(result[[component]]))) { - component_dims <- length(result[[component]]) - } else { - component_dims <- dim(result[[component]]) - } - atomic_fun_out_dims[[component]] <- component_dims - component_dims <- c(component_dims, setNames(rep(1, length(dim(ma))), names(dim(ma)))) - dim(result[[component]]) <- component_dims + for (component in 1:length(result[[m]])) { + component_dims <- dim(result[[m]][[component]]) if (!found_first_result) { - arrays_of_results[[component]] <- array(dim = c(dim(result[[component]]), dim(ma))) + if (length(component_dims) > 0) { + fun_out_dims[[component]] <- component_dims[- length(component_dims)] + } else { + fun_out_dims[[component]] <- NULL + } + arrays_of_results[[component]] <- array(dim = c(fun_out_dims[[component]], + dim(ma))) + 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 + } + chunk_length <- prod(component_dims) } arrays_of_results[[component]][(1:prod(component_dims)) + - (j - 1) * prod(component_dims)] <- result[[component]] + (m - 1) * chunk_length] <- result[[m]][[component]] } if (!found_first_result) { found_first_result <- TRUE } - if (!is.null(output_dims)) { - # Check number of output dimensions is correct. - for (component in 1:length(atomic_fun_out_dims)) { - if (length(atomic_fun_out_dims[[component]]) != output_dims[[component]]) { - stop("Expected ", component, "st returned element by 'AtomicFun'", - "to have ", length(output_dims[[component]]), " dimensions, ", - "but ", length(atomic_fun_out_dims[[component]]), " found.") - } - if (!is.null(names(atomic_fun_out_dims[[component]]))) { - # check component_dims match names of output_dims[[component]], and reorder if needed - } - } - } - TRUE - } - - # Execute in parallel if needed - if (parallel) { - registerDoParallel(ncores) - j <- seq(length(ma)) - fe <- eval(as.call(list(quote(foreach::foreach), j = j))) - info <- foreach::`%dopar%`(fe, iteration(j)) - registerDoSEQ() - } else { - info <- sapply(seq(length(ma)), iteration) + #if (!is.null(output_dims)) { + # # Check number of output dimensions is correct. + # for (component in 1:length(atomic_fun_out_dims)) { + # if (!is.null(names(fun_out_dims[[component]]))) { + # # check component_dims match names of output_dims[[component]], and reorder if needed + # } + # } + #} } # Return -- GitLab From eb7f099edeb20f255a86808f523aebd81d603e80 Mon Sep 17 00:00:00 2001 From: Nicolau Manubens Date: Tue, 24 Oct 2017 00:09:03 +0200 Subject: [PATCH 23/29] Small fix. --- R/Apply.R | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/R/Apply.R b/R/Apply.R index 5fca345..ee3449f 100644 --- a/R/Apply.R +++ b/R/Apply.R @@ -20,7 +20,7 @@ #' 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, AtomicFun = "test_fun") +#' test <- Apply(data, margins = margins, AtomicFun = "test_fun") Apply <- function(data, target_dims = NULL, AtomicFun, ..., output_dims = NULL, margins = NULL, parallel = FALSE, ncores = NULL) { # Check data @@ -176,16 +176,17 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., output_dims = NULL, margins[[i]] <- (1 : length(dim(data[[i]])))[- target_dims[[i]]] } } - # Reorder dimensions of input data for target dims to be left-most # and in the required order. for (i in 1 : length(data)) { - if (is.unsorted(target_dims[[i]]) || - (max(target_dims[[i]]) > length(target_dims[[i]]))) { - 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]])) + if (length(target_dims[[i]]) > 0) { + if (is.unsorted(target_dims[[i]]) || + (max(target_dims[[i]]) > length(target_dims[[i]]))) { + 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]])) + } } } @@ -372,12 +373,16 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., output_dims = NULL, .consolidate <- function(subsets, dimnames, out_dims) { lapply(1:length(subsets), function(x) { - dims <- dim(subsets[[x]]) - if (!is_unnamed[x]) { - names(dims) <- dimnames[[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]]) } - dims <- dims[out_dims[[x]]] - array(subsets[[x]], dim = dims) }) } -- GitLab From 78b4e51e7f12b710b0e67571e4691e81073b6cb3 Mon Sep 17 00:00:00 2001 From: Nicolau Manubens Date: Tue, 24 Oct 2017 22:56:34 +0200 Subject: [PATCH 24/29] Fixes. --- R/Apply.R | 35 ++++++++++++++++++++--------------- R/zzz.R | 14 +++----------- 2 files changed, 23 insertions(+), 26 deletions(-) diff --git a/R/Apply.R b/R/Apply.R index ee3449f..984d1a4 100644 --- a/R/Apply.R +++ b/R/Apply.R @@ -35,10 +35,11 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., output_dims = NULL, for (i in 1 : length(data)) { if (is.null(dim(data[[i]]))) { is_vector[i] <- TRUE + is_unnamed[i] <- TRUE dim(data[[i]]) <- length(data[[i]]) } - if (!is.null(names(dim(data)))) { - if (any(sapply(names(dim(data)), nchar) == 0)) { + if (!is.null(names(dim(data[[i]])))) { + if (any(sapply(names(dim(data[[i]])), nchar) == 0)) { stop("Dimension names of arrays in 'data' must be at least ", "one character long.") } @@ -58,13 +59,12 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., output_dims = NULL, stop("Parameter 'AtomicFun' must be a function or a character string ", "with the name of a function.") } - output_dims <- NULL if ('startR_step' %in% class(AtomicFun)) { if (is.null(target_dims)) { target_dims <- attr(AtomicFun, 'target_dims') } if (is.null(output_dims)) { - output_dims <- attr(AtomicFun, 'target_dims') + output_dims <- attr(AtomicFun, 'output_dims') } } @@ -195,8 +195,8 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., output_dims = NULL, if (!is.list(output_dims)) { output_dims <- list(output1 = output_dims) } - if (any(sapply(output_dims, function(x) !is.character(x)))) { - stop("Parameter 'output_dims' must be one or a list of vectors of character strings.") + if (any(sapply(output_dims, function(x) !(is.character(x) || is.null(x))))) { + stop("Parameter 'output_dims' must be one or a list of vectors of character strings (or NULLs).") } if (is.null(names(output_dims))) { names(output_dims) <- rep('', length(output_dims)) @@ -445,7 +445,6 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., output_dims = NULL, marg_indices <- arrayInd(j, dim(ma)) names(marg_indices) <- names(dim(ma)) input <- list() - atomic_fun_out_dims <- 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. @@ -479,6 +478,7 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., output_dims = NULL, names(sub_arrays_of_results) <- paste0('output', 1:length(result)) } } + atomic_fun_out_dims <- vector('list', length(result)) for (component in 1:length(result)) { if (is.null(dim(result[[component]]))) { if (length(result[[component]] == 1)) { @@ -492,7 +492,9 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., output_dims = NULL, if (!found_first_sub_result) { sub_arrays_of_results[[component]] <- array(dim = c(component_dims, chunk_sizes[m])) } - atomic_fun_out_dims[[component]] <- component_dims + if (!is.null(component_dims)) { + atomic_fun_out_dims[[component]] <- component_dims + } sub_arrays_of_results[[component]][(1:prod(component_dims)) + (n - 1) * prod(component_dims)] <- result[[component]] } @@ -500,10 +502,15 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., output_dims = NULL, found_first_sub_result <- TRUE } if (!is.null(output_dims)) { + # Check number of outputs. + if (length(output_dims) != length(result)) { + stop("Expected AtomicFun to return ", length(output_dims), " components, ", + "but ", length(result), " found.") + } # Check number of output dimensions is correct. - for (component in 1:length(atomic_fun_out_dims)) { - if (length(atomic_fun_out_dims[[component]]) != output_dims[[component]]) { - stop("Expected ", component, "st returned element by 'AtomicFun'", + 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' ", "to have ", length(output_dims[[component]]), " dimensions, ", "but ", length(atomic_fun_out_dims[[component]]), " found.") } @@ -520,7 +527,7 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., output_dims = NULL, # Merge the results chunk_length <- NULL - fun_out_dims <- NULL + fun_out_dims <- vector('list', length(result[[1]])) for (m in 1:length(result)) { if (!found_first_result) { arrays_of_results <- vector('list', length(result[[1]])) @@ -539,10 +546,8 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., output_dims = NULL, for (component in 1:length(result[[m]])) { component_dims <- dim(result[[m]][[component]]) if (!found_first_result) { - if (length(component_dims) > 0) { + if (length(component_dims) > 1) { fun_out_dims[[component]] <- component_dims[- length(component_dims)] - } else { - fun_out_dims[[component]] <- NULL } arrays_of_results[[component]] <- array(dim = c(fun_out_dims[[component]], dim(ma))) diff --git a/R/zzz.R b/R/zzz.R index 32fe628..3e04077 100644 --- a/R/zzz.R +++ b/R/zzz.R @@ -122,23 +122,15 @@ 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)))) - #do.call('[[<-', c(list(x = array_of_chunks), - # as.list(c(j, chunk_sub_indices)), - # list(value = NULL))) if (is.null(new_chunk)) { stop("Chunks missing.") } if (is.null(sub_array_of_chunks[[i]])) { sub_array_of_chunks[[i]] <- new_chunk } else { - #if (length(new_chunk) != length(sub_array_of_chunks[[i]])) { - # stop("Missing components for some chunks.") - #} - #for (component in 1:length(new_chunk)) { - sub_array_of_chunks[[i]] <- MergeArrays(sub_array_of_chunks[[i]], - new_chunk, - dim_names[dim_index]) - #} + sub_array_of_chunks[[i]] <- MergeArrays(sub_array_of_chunks[[i]], + new_chunk, + dim_names[dim_index]) } } } -- GitLab From 82ce19f4970caebfbbb59c5b24695993c9bba212 Mon Sep 17 00:00:00 2001 From: Nicolau Manubens Date: Fri, 10 Nov 2017 19:57:20 +0100 Subject: [PATCH 25/29] Fix. --- R/Apply.R | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/R/Apply.R b/R/Apply.R index 984d1a4..dd7447d 100644 --- a/R/Apply.R +++ b/R/Apply.R @@ -451,9 +451,10 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., output_dims = NULL, 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) { - input[[i]] <- data_indexed[[i]][[do.call('[', c(list(x = data_indexed_indices[[i]]), - marg_indices[inds_to_take], - list(drop = TRUE)))]] + 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)))]] } else { input[[i]] <- data_indexed[[i]][[1]] } -- GitLab From bc290aefc2662d707dec1d97c418e70e13a04edb Mon Sep 17 00:00:00 2001 From: Nicolau Manubens Date: Wed, 29 Nov 2017 21:55:19 +0100 Subject: [PATCH 26/29] Small fix. --- R/Apply.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/Apply.R b/R/Apply.R index dd7447d..91267a3 100644 --- a/R/Apply.R +++ b/R/Apply.R @@ -482,7 +482,7 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., output_dims = NULL, atomic_fun_out_dims <- vector('list', length(result)) for (component in 1:length(result)) { if (is.null(dim(result[[component]]))) { - if (length(result[[component]] == 1)) { + if (length(result[[component]]) == 1) { component_dims <- NULL } else { component_dims <- length(result[[component]]) -- GitLab From b5d492cfdabba48e0a6ddf6df3cfb8b84f9a42ba Mon Sep 17 00:00:00 2001 From: Nicolau Manubens Date: Thu, 30 Nov 2017 02:02:05 +0100 Subject: [PATCH 27/29] Fixes. --- DESCRIPTION | 1 - NAMESPACE | 1 - R/Apply.R | 38 ++++++++++++++++++++------------------ 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 131c6d2..5e23363 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -12,7 +12,6 @@ Imports: abind, plyr, doParallel, - future, foreach, s2dverification License: LGPL-3 diff --git a/NAMESPACE b/NAMESPACE index a6a7711..0ef43f7 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,7 +1,6 @@ # Generated by roxygen2: do not edit by hand importFrom(plyr, llply, splat) importFrom(abind, abind) -importFrom(future, availableCores) importFrom(doParallel, registerDoParallel) importFrom(foreach, registerDoSEQ) importFrom(s2dverification, InsertDim) diff --git a/R/Apply.R b/R/Apply.R index 91267a3..717ab8d 100644 --- a/R/Apply.R +++ b/R/Apply.R @@ -6,8 +6,7 @@ #' @param AtomicFun Function to be applied to the arrays. #' @param ... Additional arguments to be used in the AtomicFun. #' @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 parallel Logical, should the function be applied in parallel. -#' @param ncores The number of cores to use for parallel computation. +#' @param ncores The number multicore threads to use for parallel computation. #' @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 Array or matrix or vector resulting from AtomicFun. #' @references Wickham, H (2011), The Split-Apply-Combine Strategy for Data Analysis, Journal of Statistical Software. @@ -22,7 +21,7 @@ #' 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, parallel = FALSE, ncores = NULL) { + margins = NULL, ncores = NULL) { # Check data if (!is.list(data)) { data <- list(data) @@ -207,22 +206,14 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., output_dims = NULL, } } - # Check parallel - if (!is.logical(parallel)) { - stop("Parameter 'parallel' must be logical.") - } - # Check ncores - if (parallel) { - if (is.null(ncores)) { - ncores <- availableCores() - 1 - } - if (!is.numeric(ncores)) { - stop("Parameter 'ncores' must be numeric.") - } - ncores <- round(ncores) - ncores <- min(availableCores() - 1, ncores) + if (is.null(ncores)) { + ncores <- 1 + } + if (!is.numeric(ncores)) { + stop("Parameter 'ncores' must be numeric.") } + ncores <- round(ncores) # Consistency checks of margins of all input objects # for each data array, add its margins to the list if not present. @@ -371,7 +362,7 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., output_dims = NULL, class = c("indexed_array")) } .consolidate <- function(subsets, dimnames, out_dims) { - lapply(1:length(subsets), + lapply(setNames(1:length(subsets), names(subsets)), function(x) { if (length(out_dims[[x]]) > 0) { dims <- dim(subsets[[x]]) @@ -390,6 +381,8 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., output_dims = NULL, 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] <- "" @@ -522,6 +515,7 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., output_dims = NULL, } # Execute in parallel if needed + parallel <- ncores > 1 if (parallel) registerDoParallel(ncores) result <- llply(1:length(chunk_sizes), iteration, .parallel = parallel) if (parallel) registerDoSEQ() @@ -577,6 +571,14 @@ Apply <- function(data, target_dims = NULL, AtomicFun, ..., output_dims = NULL, # } #} } + # Assign 'output_dims' as dimension names if possible + if (!is.null(output_dims)) { + for (component in 1:length(output_dims)) { + if (length(output_dims[[component]]) > 0) { + names(dim(arrays_of_results[[component]]))[1:length(output_dims[[component]])] <- output_dims[[component]] + } + } + } # Return arrays_of_results -- GitLab From 68892a786873c2f1804dafd3e1eeb714a080b06a Mon Sep 17 00:00:00 2001 From: Nicolau Manubens Date: Mon, 5 Feb 2018 13:32:47 +0100 Subject: [PATCH 28/29] Updating documentation. --- DESCRIPTION | 5 ++--- NAMESPACE | 6 +++--- R/Apply.R | 9 +++++---- man/Apply.Rd | 34 ++++++++++++++-------------------- 4 files changed, 24 insertions(+), 30 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 5e23363..3e7559a 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -9,11 +9,10 @@ Description: The base apply function and its variants, as well as the related fu Depends: R (>= 3.2.0) Imports: - abind, - plyr, + abind, doParallel, foreach, - s2dverification + plyr License: LGPL-3 URL: https://earth.bsc.es/gitlab/ces/multiApply BugReports: https://earth.bsc.es/gitlab/ces/multiApply/issues diff --git a/NAMESPACE b/NAMESPACE index 0ef43f7..213333a 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,7 +1,7 @@ # Generated by roxygen2: do not edit by hand -importFrom(plyr, llply, splat) importFrom(abind, abind) -importFrom(doParallel, registerDoParallel) importFrom(foreach, registerDoSEQ) -importFrom(s2dverification, InsertDim) +importFrom(doParallel, registerDoParallel) +importFrom(plyr, splat) +importFrom(plyr, llply) export(Apply) diff --git a/R/Apply.R b/R/Apply.R index 717ab8d..b49b11d 100644 --- a/R/Apply.R +++ b/R/Apply.R @@ -1,14 +1,15 @@ #' Wrapper for Applying Atomic Functions to Arrays. #' -#' The Apply function is an extension of the mapply function, which instead of taking lists of unidimensional objects as input, takes lists of multidimensional objects as input, which may have different numbers of dimensions and dimension lengths. The user can specify which dimensions of each array (or matrix) the function is to be applied over with the margins option. +#' 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. If both margins and target_dims are specified, margins takes priority over target_dims. +#' @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 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 multicore threads to use for parallel computation. +#' @param ncores The number of multicore threads to use for parallel computation. #' @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 Array or matrix or vector resulting from AtomicFun. +#' @return List of arrays or matrices or vectors resulting from applying AtomicFun to data. #' @references Wickham, H (2011), The Split-Apply-Combine Strategy for Data Analysis, Journal of Statistical Software. #' @export #' @examples diff --git a/man/Apply.Rd b/man/Apply.Rd index 63a64de..2f69b6c 100644 --- a/man/Apply.Rd +++ b/man/Apply.Rd @@ -4,48 +4,42 @@ \alias{Apply} \title{Wrapper for Applying Atomic Functions to Arrays.} \usage{ -Apply(data, target_dims = NULL, AtomicFun, ..., margins = NULL, parallel = FALSE, - ncores = NULL) +Apply(data, target_dims = NULL, AtomicFun, ..., output_dims = NULL, + margins = NULL, ncores = NULL) } \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{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. If both margins and target_dims are specified, target_dims takes priority over margins.} +\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{AtomicFun}{Function to be applied to the arrays.} \item{...}{Additional arguments to be used in the AtomicFun.} -\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, target_dims takes priority over margins.} +\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{parallel}{Logical, should the function be applied in parallel.} +\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{ncores}{The number of cores to use for parallel computation.} +\item{ncores}{The number of multicore threads to use for parallel computation.} } \value{ -Array or matrix or vector resulting from AtomicFun. +List of arrays or matrices or vectors resulting from applying AtomicFun to data. } \description{ -Takes lists of multidimensional objects as input, which may have different numbers of dimensions and dimension lengths. The user can specify which dimensions of each array (or matrix) the function is to be applied over with the margins option. +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. } \details{ -A user can apply a function that receives 1 or more objects as input, each with a different number of dimensions, and returns as a result a single array with any number of dimensions. +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'. } \examples{ #Change in the rate of exceedance for two arrays, with different #dimensions, for some matrix of exceedances. -array_1 <- array(rnorm(2000), c(10,10,20)) # array with 20 timesteps -array_2 <- array(rnorm(1000), c(10, 10, 15)) # array with 15 timesteps -thresholds <- matrix(rnorm(100), 10, 10) # matrix of thresholds (no timesteps) - -# Function for calculating the change in the frequency of exceedances over the -#thresholds for array_1 relative to array_2 (percentage change). - -test_fun <- function(x, y, z) {(((sum(x > z) / (length(x))) / - (sum(y > z) / (length(y)))) * 100) - 100} -data = list(array_1, array_2, thresholds) +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 = data, margins = margins, AtomicFun = "test_fun") +test <- Apply(data, margins = margins, AtomicFun = "test_fun") } \references{ Wickham, H (2011), The Split-Apply-Combine Strategy for Data Analysis, Journal of Statistical Software. -- GitLab From 9cada4436dd0e41e319a59cb867f038f3cbf19d1 Mon Sep 17 00:00:00 2001 From: Nicolau Manubens Date: Mon, 5 Feb 2018 16:32:06 +0100 Subject: [PATCH 29/29] Bumped to v1.0.0. --- DESCRIPTION | 2 +- NAMESPACE | 1 + multiApply-manual.pdf | Bin 0 -> 68100 bytes 3 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 multiApply-manual.pdf diff --git a/DESCRIPTION b/DESCRIPTION index 3e7559a..0e1cede 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: multiApply Title: Apply Functions to Multiple Multidimensional Arguments -Version: 0.0.1 +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")), diff --git a/NAMESPACE b/NAMESPACE index 213333a..283fbf0 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -4,4 +4,5 @@ importFrom(foreach, registerDoSEQ) importFrom(doParallel, registerDoParallel) importFrom(plyr, splat) importFrom(plyr, llply) +importFrom(stats, setNames) export(Apply) diff --git a/multiApply-manual.pdf b/multiApply-manual.pdf new file mode 100644 index 0000000000000000000000000000000000000000..bf1c86e977e001ee0b102d7b749e031b158ac0d0 GIT binary patch literal 68100 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 Ks_N?R#svVP#qwPM literal 0 HcmV?d00001 -- GitLab