R/PipelineDefinition.R
932ec5c4
 .validatePipelineDef <- function(object){
93cfa123
   
932ec5c4
   e <- c()
93cfa123
   if( !is.list(object@functions) || 
       !all(vapply(object@functions, is.function, logical(1))) ) 
932ec5c4
     e <- c("`functions` should be a (named) list of functions!")
93cfa123
   if(!all(vapply( object@functions, FUN.VALUE=logical(1),
                   FUN=function(x) "x" %in% names(formals(x)))))
932ec5c4
     e <- c(e, "Each function should at least take the argument `x`.")
   isf <- function(x) is.null(x) || is.function(x)
93cfa123
   if( !is.list(object@aggregation) || 
       !all(vapply(object@aggregation, isf, logical(1))) ) 
932ec5c4
     stop("`aggregation` should be a list of functions and/or NULL slots!")
93cfa123
   if( !is.list(object@evaluation) || 
       !all(vapply(object@evaluation, isf, logical(1))) ) 
932ec5c4
     stop("`evaluation` should be a list of functions and/or NULL slots!")
   if(!all(names(object@descriptions)==names(object@functions))) 
     e <- c(e, "descriptions do not match functions.")
   if(!all(names(object@evaluation)==names(object@functions))) 
     e <- c(e, "evaluation do not match functions.")
   if(!all(names(object@aggregation)==names(object@functions))) 
     e <- c(e, "aggregation do not match functions.")
   args <- unlist( lapply( object@functions, 
                           FUN=function(x){ setdiff(names(formals(x)), "x") }) )
   if(any(duplicated(args))) e <- c(e, paste("Some arguments (beside `x`) is",
     "used in more than one step, which is not currently supported."))
   if(length( wa <- setdiff(names(object@defaultArguments),args) )>0)
     e <- c(e, paste("The following default arguments are not in the pipeline's 
            functions:", paste(wa, collapse=", ")))
   if(length(e) == 0) TRUE else e
 }
 
 #' @import methods
7dbc17c4
 #' @exportClass PipelineDefinition
932ec5c4
 setClass( "PipelineDefinition", 
           slots=representation( functions="list", descriptions="list", 
                                 evaluation="list", aggregation="list", 
eebbb8b3
                                 initiation="function",
932ec5c4
                                 defaultArguments="list", misc="list" ),
           prototype=prototype( functions=list(), descriptions=list(), 
                                evaluation=list(), aggregation=list(), 
eebbb8b3
                                initiation=identity,
932ec5c4
                                defaultArguments=list(), misc=list() ),
           validity=.validatePipelineDef )
 
 
 
 #' PipelineDefinition
7dbc17c4
 #' 
 #' Creates on object of class `PipelineDefinition` containing step functions,
 #' as well as optionally step evaluation and aggregation functions.
932ec5c4
 #'
 #' @param functions A list of functions for each step
 #' @param descriptions A list of descriptions for each step
 #' @param evaluation A list of optional evaluation functions for each step
 #' @param aggregation A list of optional aggregation functions for each step
eebbb8b3
 #' @param initiation A function ran when initiating a dataset
932ec5c4
 #' @param defaultArguments A lsit of optional default arguments
 #' @param misc A list of whatever.
 #' @param verbose Whether to output additional warnings (default TRUE).
 #'
 #' @return An object of class `PipelineDefinition`, with the slots functions,
 #' descriptions, evaluation, aggregation, defaultArguments, and misc.
7dbc17c4
 #' 
 #' @aliases PipelineDefinition-class
a53f1220
 #' @seealso \code{\link{PipelineDefinition-methods}}, 
 #' \code{\link{addPipelineStep}}. For an example pipeline, see 
 #' \code{\link{scrna_pipeline}}.
932ec5c4
 #' @export
54a0f1cc
 #' @examples
 #' PipelineDefinition(
 #'   list( step1=function(x, meth1){ get(meth1)(x) },
 #'         step2=function(x, meth2){ get(meth2)(x) } )
 #' )
932ec5c4
 PipelineDefinition <- function( functions, descriptions=NULL, evaluation=NULL,
eebbb8b3
                                 aggregation=NULL, initiation=identity, 
                                 defaultArguments=list(), 
932ec5c4
                                 misc=list(), verbose=TRUE ){
93cfa123
   if(!is.list(functions) || !all(vapply(functions, is.function, logical(1)))) 
932ec5c4
     stop("`functions` should be a (named) list of functions!")
   n <- names(functions)
a53f1220
   if(is.null(n)) 
     n <- names(functions) <- paste0("step",seq_len(length(functions)))
54a0f1cc
   descriptions <- .checkInputList(descriptions, functions, FALSE)
   evaluation <- .checkInputList(evaluation, functions)
   aggregation2 <- .checkInputList(aggregation, functions)
   names(aggregation2)<-names(evaluation)<-names(descriptions)<-names(functions)
   for(f in names(aggregation2)){
     if(is.null(aggregation2[[f]]) && !is.null(evaluation[[f]]) &&
        !(f %in% names(aggregation)))
       aggregation2[[f]] <- defaultStepAggregation
932ec5c4
   }
   if(is.null(misc)) misc <- list()
a53f1220
   x <- new("PipelineDefinition", functions=functions,descriptions=descriptions,
          evaluation=evaluation, aggregation=aggregation2, 
          initiation=initiation, defaultArguments=defaultArguments, misc=misc)
   
93cfa123
   w <- which( !vapply(x@aggregation, is.null, logical(1)) & 
                 vapply(x@evaluation, is.null, logical(1)) )
a53f1220
   if(verbose && length(w)>0){
     warning(paste("An aggregation is defined for some steps that do not have",
                   "a defined evaluation function: ",
                   paste(names(x@functions)[w], collapse=", "),
                   "It is possible that evaluation is performed by the step's",
                   "function itself.") )
   }
   x
932ec5c4
 }
 
54a0f1cc
 .checkInputList <- function( x, fns, containsFns=TRUE, 
                              name=deparse(substitute(x)) ){
   name <- paste0("`",name,"`")
   if(!is.null(x)){
     if(length(x)!=length(fns)){
       if(is.null(names(x)))
         stop("If ", name, " does not have the same length as the number of ",
              "steps, its slots should be named.")
       if(length(unknown <- setdiff(names(x),names(fns)))>0)
         stop("Some elements of ",name," (",paste(unknown,collapse=", "),")",
              "are unknown.")
       x <- lapply(names(fns), FUN=function(f){
         if(is.null(x[[f]])) return(NULL)
         x[[f]]
       })
       names(x) <- names(fns)
     } 
     if( !is.null(names(x)) ){
       if(!all(names(x)==names(fns)) )
         stop("The names of ",name," should match those of `functions`")
     }
   }else{
     x <- lapply(fns,FUN=function(x) NULL)
   }
93cfa123
   if(containsFns && 
      !all(vapply(x, FUN=function(x) is.null(x) || is.function(x), logical(1))))
54a0f1cc
     stop(name," should be a list of functions")
   x
 }
 
7dbc17c4
 #' Methods for \code{\link{PipelineDefinition}} class
 #' @name PipelineDefinition-methods
 #' @rdname PipelineDefinition-methods
 #' @aliases PipelineDefinition-method
 #' @seealso \code{\link{PipelineDefinition}}, \code{\link{addPipelineStep}}
 #' @param object An object of class \code{\link{PipelineDefinition}}
3fceaac8
 #' @return Depends on the method.
36e3057a
 #' @examples
 #' pd <- mockPipeline()
 #' length(pd)
 #' names(pd)
 #' pd$step1
 #' pd[2:1]
7dbc17c4
 NULL
 
 #' @rdname PipelineDefinition-methods
 #' @importMethodsFrom methods show
c728308f
 #' @importFrom knitr opts_current
932ec5c4
 setMethod("show", signature("PipelineDefinition"), function(object){
c728308f
   # colors and bold are going to trigger errors when rendered in a knit, so
   # we disable them when rendering
   isKnit <- tryCatch( isTRUE(getOption('knitr.in.progress')) || 
                         length(knitr::opts_current$get())>0,
                       error=function(e) FALSE)
93cfa123
   fns <- unlist(lapply(names(object@functions), FUN=function(x){ 
c728308f
     x2 <- x
     if(!isKnit) x2 <- paste0("\033[1m",x,"\033[22m")
93cfa123
     y <- lapply( names(formals(object@functions[[x]])), FUN=function(n){
eebbb8b3
       if(!is.null(def <- object@defaultArguments[[n]]))
         n <- paste0(n,"=",deparse(def,100,FALSE))
       n
     })
93cfa123
     y <- paste0("  - ", x2, "(", paste(unlist(y), collapse=", "), ")")
932ec5c4
     if(!is.null(object@evaluation[[x]]) || !is.null(object@aggregation[[x]])) 
c728308f
       y <- paste0(y, ifelse(isKnit, " * ", " \033[34m*\033[39m "))
     if(!is.null(object@descriptions[[x]])){
       x2 <- object@descriptions[[x]]
       if(!isKnit) x2 <- paste0("\033[3m",x2,"\033[23m")
       y <- paste(y, x2, sep="\n")
     }
932ec5c4
     y
93cfa123
   }))
932ec5c4
   cat("A PipelineDefinition object with the following steps:\n")
   cat(paste(fns,collapse="\n"))
   cat("\n")
 })
 
7558b7b2
 #' get names of PipelineDefinition steps
7dbc17c4
 #' @rdname PipelineDefinition-methods
 #' @param x An object of class \code{\link{PipelineDefinition}}
932ec5c4
 setMethod("names", signature("PipelineDefinition"), function(x){
   names(x@functions)
 })
7558b7b2
 
 #' set names of PipelineDefinition steps
7dbc17c4
 #' @rdname PipelineDefinition-methods
20c06813
 #' @param value Replacement values
7dbc17c4
 setMethod("names<-", signature("PipelineDefinition"), function(x, value){
   if(any(duplicated(value))) stop("Some step names are duplicated!")
   names(x@functions) <- value
   names(x@evaluation) <- value
   names(x@aggregation) <- value
   names(x@descriptions) <- value
   validObject(x)
   x
 })
932ec5c4
 
7dbc17c4
 #' @rdname PipelineDefinition-methods
20c06813
 #' @param name The step name
932ec5c4
 setMethod("$", signature("PipelineDefinition"), function(x, name){
   x@functions[[name]]
 })
 
7dbc17c4
 #' @rdname PipelineDefinition-methods
932ec5c4
 setMethod("length", signature("PipelineDefinition"), function(x){
   length(x@functions)
 })
 
7dbc17c4
 #' @rdname PipelineDefinition-methods
20c06813
 #' @param i The index(es) of the steps
932ec5c4
 setMethod("[",signature("PipelineDefinition"), function(x, i){
   new("PipelineDefinition", functions=x@functions[i], 
        descriptions=x@descriptions[i], evaluation=x@evaluation[i],
        aggregation=x@aggregation[i], misc=x@misc)
 })
 
7dbc17c4
 #' @rdname PipelineDefinition-methods
932ec5c4
 setMethod("as.list",signature("PipelineDefinition"), function(x){
   x@functions
 })
 
7dbc17c4
 #' @exportMethod arguments
20c06813
 #' @rdname PipelineDefinition-methods
7dbc17c4
 setGeneric("arguments", function(object) args(object))
 #' @rdname PipelineDefinition-methods
 setMethod("arguments",signature("PipelineDefinition"), function(object){
   lapply(object@functions, FUN=function(x){ setdiff(names(formals(x)), "x") })
932ec5c4
 })
 
20c06813
 #' @rdname PipelineDefinition-methods
243e2f21
 #' @exportMethod defaultArguments
 setGeneric("defaultArguments", function(object) NULL)
 #' @exportMethod defaultArguments<-
20c06813
 #' @rdname PipelineDefinition-methods
243e2f21
 setGeneric("defaultArguments<-", function(object, value) NULL)
 #' @rdname PipelineDefinition-methods
 setMethod("defaultArguments",signature("PipelineDefinition"), function(object){
   object@defaultArguments
 })
 #' @rdname PipelineDefinition-methods
 setMethod( "defaultArguments<-",signature("PipelineDefinition"), 
            function(object, value){
   object@defaultArguments <- value
   validObject(object)
   object
 })
 
7dbc17c4
 #' @exportMethod stepFn
20c06813
 #' @rdname PipelineDefinition-methods
b04a719a
 setGeneric("stepFn", function(object, step=NULL, type) standardGeneric("stepFn"))
7dbc17c4
 #' @param step The name of the step for which to set or get the function
243e2f21
 #' @param type The type of function to set/get, either `functions`, 
 #' `evaluation`, `aggregation`, `descriptions`, or `initiation` (will parse 
 #' partial matches)
7dbc17c4
 #' @rdname PipelineDefinition-methods
a53f1220
 setMethod("stepFn", signature("PipelineDefinition"), 
b04a719a
           function(object, step=NULL, type){
210e0cb3
   ft <- c("functions","evaluation","aggregation","descriptions","initiation")
   type <- match.arg( type, ft )
b04a719a
   if(is.null(step)) return(slot(object, type))
7dbc17c4
   step <- match.arg(step, names(object))
   slot(object, type)[[step]]
 })
 #' @exportMethod stepFn<-
20c06813
 #' @rdname PipelineDefinition-methods
a53f1220
 setGeneric( "stepFn<-", 
             function(object, step, type, value) standardGeneric("stepFn<-") )
7dbc17c4
 #' @rdname PipelineDefinition-methods
a53f1220
 setMethod( "stepFn<-", signature("PipelineDefinition"), 
            function(object, step, type, value){
   ft <- c("functions","evaluation","aggregation","descriptions","initiation")
   type <- match.arg(type, ft)
d409ddaa
   if(type!="descriptions" && !is.null(value) && !is.function(value))
243e2f21
     stop("Replacement value should be a function.")
   if(type=="initiation"){
     slot(object, type) <- value
   }else{
     step <- match.arg(step, names(object))
     slot(object, type)[[step]] <- value
   }
4cf5460d
   if(type=="evaluation" && !is.null(value)){
     # also add the default aggregation:
     if(is.null(slot(object, "aggregation")[[step]]))
       slot(object, "aggregation")[[step]] <- defaultStepAggregation
   }
7dbc17c4
   object
 })
 
 
 #' addPipelineStep
 #' 
 #' Add a step to an existing \code{\link{PipelineDefinition}}
 #'
 #' @param object A \code{\link{PipelineDefinition}}
 #' @param name The name of the step to add
a53f1220
 #' @param after The name of the step after which to add the new step. If NULL,
 #' will add the step at the beginning of the pipeline.
 #' @param slots A optional named list with slots to fill for that step (i.e. 
 #' `functions`, `evaluation`, `aggregation`, `descriptions` - will be parsed)
7dbc17c4
 #'
 #' @return A \code{\link{PipelineDefinition}}
a53f1220
 #' @seealso \code{\link{PipelineDefinition}}, 
 #' \code{\link{PipelineDefinition-methods}}
7dbc17c4
 #' @importFrom methods is slot
932ec5c4
 #' @export
7dbc17c4
 #'
 #' @examples
54a0f1cc
 #' pd <- mockPipeline()
7dbc17c4
 #' pd
54a0f1cc
 #' pd <- addPipelineStep(pd, name="newstep", after="step1", 
7dbc17c4
 #'                       slots=list(description="Step that does nothing..."))
 #' pd
 addPipelineStep <- function(object, name, after=NULL, slots=list()){
a53f1220
   if(!is(object, "PipelineDefinition")) 
     stop("object should be a PipelineDefinition")
7dbc17c4
   if(name %in% names(object)) stop("There is already a step with that name!")
   if(!is.null(after) && !(after %in% names(object))) 
     stop("`after` should either be null or the name of a step.")
   n <- c("functions","evaluation","aggregation","descriptions")
a53f1220
   if(length(slots)>0) 
93cfa123
     names(slots) <- vapply( names(slots), choices=n, FUN=match.arg, 
                             FUN.VALUE=character(1) )
a53f1220
   if(!all(names(slots) %in% n)) 
     stop(paste("fns should be a function or a list", 
     "with one or more of the following names:\n", paste(n,collapse=", ")))
7dbc17c4
   
   if(is.null(after)){
     i1 <- vector("integer")
     i2 <- seq_along(names(object))
   }else{
     w <- which(names(object)==after)
36e3057a
     i1 <- seq_len(w)
     i2 <- seq.int(from=w+1, to=length(object))
7dbc17c4
     if(w==length(object)) i2 <- vector("integer")
932ec5c4
   }
7dbc17c4
   ll <- list(NULL)
   names(ll) <- name
   for(f in n) slot(object,f) <- c(slot(object,f)[i1], ll, slot(object,f)[i2])
   for(f in names(slots)) stepFn(object, name, f) <- slots[[f]]
   if(is.null(stepFn(object, name, "functions"))) 
     stepFn(object, name, "functions") <- identity
   validObject(object)
   object
54a0f1cc
 }
 
 #' mockPipeline
 #' 
 #' A mock `PipelineDefinition` for use in examples.
 #'
 #' @return a `PipelineDefinition`
 #' @export
 #'
 #' @examples
 #' mockPipeline()
 mockPipeline <- function(){
   PipelineDefinition(
     list( step1=function(x, meth1){ get(meth1)(x) },
           step2=function(x, meth2){ get(meth2)(x) } ),
     evaluation=list( step2=function(x) c(mean=mean(x), max=max(x)) ),
d29e21a7
     descriptions=list( step1="This steps applies meth1 to x.",
                        step2="This steps applies meth2 to x."),
54a0f1cc
     defaultArguments=list(meth1=c("log","sqrt"), meth2="cumsum")
   )
 }