From 80f508d356119e52ac105519b029f9b7114c3af4 Mon Sep 17 00:00:00 2001 From: mb706 Date: Thu, 24 Aug 2017 07:04:00 -0400 Subject: [PATCH 1/4] Adding ParamSetSugar --- NAMESPACE | 5 + R/ParamSetSugar.R | 351 ++++++++++++++++++++++++++++ man/LearnerParam.Rd | 2 - man/Param.Rd | 2 - man/discrete.Rd | 11 + man/funct.Rd | 11 + man/pSS.Rd | 85 +++++++ man/untyped.Rd | 11 + tests/testthat/test_paramSetSugar.R | 120 ++++++++++ 9 files changed, 594 insertions(+), 4 deletions(-) create mode 100644 R/ParamSetSugar.R create mode 100644 man/discrete.Rd create mode 100644 man/funct.Rd create mode 100644 man/pSS.Rd create mode 100644 man/untyped.Rd create mode 100644 tests/testthat/test_paramSetSugar.R diff --git a/NAMESPACE b/NAMESPACE index 821b2ec2..deb7330d 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -68,6 +68,7 @@ export(checkParamSet) export(convertParamSetToIrace) export(dfRowToList) export(dfRowsToList) +export(discrete) export(discreteNameToValue) export(discreteValueToName) export(dropParams) @@ -75,6 +76,7 @@ export(evaluateParamExpressions) export(filterParams) export(filterParamsDiscrete) export(filterParamsNumeric) +export(funct) export(generateDesign) export(generateDesignOfDefaults) export(generateGridDesign) @@ -161,6 +163,8 @@ export(makeOptPathDF) export(makeParamSet) export(makeUntypedLearnerParam) export(makeUntypedParam) +export(pSS) +export(pSSLrn) export(paramValueToString) export(plotEAF) export(plotOptPath) @@ -175,6 +179,7 @@ export(setOptPathElEOL) export(setValueCNames) export(trafoOptPath) export(trafoValue) +export(untyped) export(updateParVals) import(BBmisc) import(checkmate) diff --git a/R/ParamSetSugar.R b/R/ParamSetSugar.R new file mode 100644 index 00000000..236a3d76 --- /dev/null +++ b/R/ParamSetSugar.R @@ -0,0 +1,351 @@ +#' defined to avoid problems with the static type checker +#' @export +discrete = list + +#' defined to avoid problems with the static type checker +#' @export +funct = list + +#' defined to avoid problems with the static type checker +#' @export +untyped = list + + +#' @title Turn the argument list into a \code{ParamSet} of \code{LearnerParam}s +#' +#' @description +#' +#' \code{pSS}, short for \code{paramSetSugar}, is a shorthand API for \code{ParamHelpers} +#' which enables entry of \code{\link{ParamSet}}s in short form. It behaves similarly to +#' \code{\link{makeParamSet}}, but instead of having to construct each parameter individually, +#' the parameters can be given in shorthand form with a convenient syntax, making use of R's +#' nonstandard evaluation. +#' +#' The arguments are of the form +#' +#' \code{name = default: type range [^ dimension] [settings]}. +#' +#' \code{name} is any valid R identifier name. +#' +#' \dQuote{= default} Determines the 'default' setting +#' in \dQuote{makeXXXLearnerParam}. Note that this is different from an R function parameter +#' default value, in that it serves only as information to the user and does not set the +#' parameter to this value if it is not given. To define `no default`, use NA or +#' leave the \dQuote{= default} part out. Leaving it out can cause problems when R's static +#' type checker verifies a package, so this is *only* recommended for interactive sessions +#' and top-level applications! (To actually set a parameter default to NA, put it in parentheses) +#' +#' \code{type} is one of +#' \dQuote{integer}, \dQuote{numeric}, \dQuote{logical}, \dQuote{discrete}, \dQuote{funct}, \dQuote{character}, \dQuote{untyped} +#' +#' \code{range} is absent for type \dQuote{logical}, \dQuote{funct}, \dQuote{character}, or \dQuote{untyped}. For \dQuote{discrete}, +#' it is either \code{[valuelist]} with \code{valuelist} evaluating to a list, +#' or of the form \code{[value1, value2, ...]}, creating a discrete parameter of character +#' or numeric values according to \code{value1}, +#' \code{value2} etc. If \code{type} is one of \dQuote{integer} or \dQuote{numeric}, +#' \code{range} is of the form \code{[lowBound, upBound]}, where \code{lowBound} +#' and \code{upBound} must either be numerical (or integer) values indicating the +#' lower and upper bound, or may be missing (indicating the absence of a bound). To indicate +#' an exclusive bound, prefix the values with a tilde (\dQuote{~}). For a \dQuote{numeric} variable, to +#' indicate an unbounded value which may not be infinite, you can use \code{~Inf} resp \code{~-Inf}, +#' or use tilde-dot (\dQuote{~.}). +#' +#' \code{^ dimension} may be absent, resulting in a normal \code{\link{LearnerParam}}, or present, +#' resulting in a \code{VectorLearnerParam}. Note that a one-dimensional \code{VectorLearnerParam} +#' is distinct from a normal \code{\link{LearnerParam}}. +#' +#' \code{settings} may be a collection of further settings to supply to \code{makeXXXLearnerParam} +#' and is optional. To specify a series of settings, put in double square brackets (\code{[[}, \code{]]}), +#' and comma-separate settings if more than one is present. +#' +#' This makes definition of \code{\link{ParamSet}}s shorter and more readable. +#' +#' The difference between \code{pSS} and \code{pSSLrn} is only in the default value of \code{.pss.learner.params} +#' being \code{FALSE} for the former and \code{TRUE} for the latter. +#' +#' @param ... Parameters, see description. +#' @param .pss.learner.params [\code{logical}]\cr +#' Whether to create \code{\link{LearnerParam}} instead of \code{\link{Param}} objects. +#' Default is \code{TRUE} for \code{pSSLrn} and \code{FALSE} for \code{pSS}. +#' @param .pss.env [\code{environment}]\cr +#' Which environment to use when evaluating expressions. Defaults to the calling +#' function's frame. +#' +#' @examples +#' pSSLrn(a = NA: integer [~0, ]^2 [[requires = expression(b != 0)]], +#' b = -10: numeric [~., 0], c: discrete [x, y, 1]) +#' # is equivalent to +#' makeParamSet( +#' makeIntegerVectorLearnerParam('a', len = 2, lower = 1, # note exclusive bound +#' upper = Inf, requires = expression(b != 0)), +#' makeNumericLearnerParam('b', lower = -Inf, upper = 0, +#' allow.inf = FALSE, default = -10), # note infinite value is prohibited. +#' makeDiscreteLearnerParam('c', values = list(x = "x", y = "y", `1` = 1)) +#' ) +#' +#' +#' @export +pSS = function(..., .pss.learner.params = FALSE, .pss.env = parent.frame()) { + promises = substitute(pSS(...)) # match.call doesn't work with indirect calls. + promises[[1]] = NULL + allparams = lapply(seq_along(promises), function(paridx) { + thispar = promises[[paridx]] + name = coalesce(names(promises)[paridx], "") + parseSingleParameter(name, thispar, .pss.learner.params, .pss.env) + }) + makeParamSet(params = allparams) +} + +#' @rdname pSS +#' @export +pSSLrn = function(..., .pss.learner.params = TRUE, .pss.env = parent.frame()) { + pSS(..., .pss.learner.params = .pss.learner.params, .pss.env = .pss.env) +} + +### Auxiliary functions + +# formerr: Give informational error about malformed parameter +formerr = function(pstring, specific) { + stopf("Parameter '%s' must be of the form\n%s\n%s", pstring, + "NAME = DEFAULT: TYPE [RANGE] [^ DIMENSION] [SETTINGS]", + specific) +} + +# get the makeXXXParam function appropriate for the type and vector-ness +getConstructor = function(type, is.learner, is.vector) { + normal.const = list(numeric = makeNumericParam, + integer = makeIntegerParam, + logical = makeLogicalParam, + discrete = makeDiscreteParam, + funct = makeFunctionParam, + character = makeCharacterParam, + untyped = makeUntypedParam) + vector.const = list(numeric = makeNumericVectorParam, + integer = makeIntegerVectorParam, + logical = makeLogicalVectorParam, + discrete = makeDiscreteVectorParam, + character = makeCharacterVectorParam) + normlrn.const = list(numeric = makeNumericLearnerParam, + integer = makeIntegerLearnerParam, + logical = makeLogicalLearnerParam, + discrete = makeDiscreteLearnerParam, + funct = makeFunctionLearnerParam, +# character = makeCharacterLearnerParam, + untyped = makeUntypedLearnerParam) + vectlrn.const = list(numeric = makeNumericVectorLearnerParam, + integer = makeIntegerVectorLearnerParam, + logical = makeLogicalVectorLearnerParam, + discrete = makeDiscreteVectorLearnerParam) +# character = makeCharacterVectorLearnerParam, + if (is.vector) { + if (is.learner) { + vectlrn.const[[type]] + } else { + vector.const[[type]] + } + } else { + if (is.learner) { + normlrn.const[[type]] + } else { + normal.const[[type]] + } + } +} + +### parameter parsing sub-functions + +# parseDimension: parse the '^n' part indicating a vector +parseDimension = function(pdeco, pstring, pss.env) { + if (is.recursive(pdeco[[3]]) && identical(pdeco[[3]][[1]], quote(`[[`))) { # settings + exponent = pdeco[[3]][[2]] + # remove '^..' part, but keep settings part + new.pdeco = pdeco[[3]] + new.pdeco[[2]] = pdeco[[2]] + pdeco = new.pdeco + } else { + exponent = pdeco[[3]] + pdeco = pdeco[[2]] # remove '^...' part + } + len = eval(exponent, envir = pss.env) + if (!is.numeric(len) && !is.na(len)) { + formerr(pstring, sprintf("`^` found, but exponent %s did not eval to numeric.", deparseJoin(exponent))) + } + list(pdeco = pdeco, len = len) +} + +# parseDiscrete: parse the range part of a discrete parameter +parseDiscrete = function(pdeco, pstring, pss.env) { + if (length(pdeco) == 2) { + # only one value given -> interpret it as expression that gives the values list + return(eval(pdeco[[2]], envir = pss.env)) + } else if (all(names(pdeco) == "")) { + # succession of names of the kind (a, b, c, 1, 2, 3) -> turn it into a list + vallist = as.list(pdeco) + vallist[[1]] = NULL + vallist = lapply(vallist, function(item) { + if (is.name(item)) { + as.character(item) + } else if (is.numeric(item)) { + item + } else { + formerr(pstring, sprintf("value list %s invalid", deparseJoin(pdeco))) + } + }) + names(vallist) = sapply(vallist, as.character) + return(vallist) + } else { + pdeco[[1]] = quote(list) + return(eval(as.call(pdeco), envir = pss.env)) + } +} + +# parseNumeric: parse the range part of a numeric / integer parameter +parseNumeric = function(pdeco, ptype, pstring, pss.env) { + if (length(pdeco) != 3) { + formerr(pstring, "invalid numeric / integer range") + } + quasi.inf = .Machine$double.xmax + parse.bound = function(expr, lower) { + if (is.recursive(expr) && identical(expr[[1]], quote(`~`))) { + if (length(expr) == 2 && identical(expr[[2]], quote(`.`))) { + value = ifelse(lower, -Inf, Inf) + } else { + value = eval(expr[[2]], envir = pss.env) + } + if (is.infinite(value)) { + if (ptype == "integer") { + formerr(pstring, '"."-bounds (unbounded but excluding "Inf") are only allowed for "numeric" variables.') + } + if ((value < 0) == lower) { + value = ifelse(lower, -quasi.inf, quasi.inf) + } + } else if (ptype == "integer") { + value = value + ifelse(lower, 1, -1) + } else { + if (value == 0) { + value = value + .Machine$double.xmin * ifelse(lower, 1, -1) + } else { + epsilon = ifelse(lower == (value > 0), .Machine$double.eps, -.Machine$double.neg.eps) + value = value + epsilon * value + } + } + } else if (identical(expr, substitute())) { + if (length(expr) > 1) { + formerr(pstring, "invalid numeric / integer range") + } + value = ifelse(lower, -Inf, Inf) + } else { + value = eval(expr, envir = pss.env) + } + } + lower.bound = parse.bound(pdeco[[2]], TRUE) + upper.bound = parse.bound(pdeco[[3]], FALSE) + + allow.inf = TRUE + if (lower.bound == -quasi.inf || upper.bound == quasi.inf) { + if (lower.bound != -Inf && upper.bound != Inf) { + # no true 'Inf' occurs, so we can translate quasi.inf to Inf and + # instead use the 'allow.inf' parameter + if (lower.bound == -quasi.inf) { + lower.bound = -Inf + } + if (upper.bound == quasi.inf) { + upper.bound = Inf + } + allow.inf = FALSE + } + } + rl = list(lower = lower.bound, upper = upper.bound) + if (ptype == "numeric") { + rl$allow.inf = allow.inf + } + rl +} + +### parsing single parameter +# this function does the heavy lifting: +# it takes the name and expression of a given parameter and returns +# the constructed ParamSet. +parseSingleParameter = function(name, thispar, is.learner, pss.env) { + constructor.params = list() + additional.settings = list() + is.vector = FALSE + pstring = deparseJoin(thispar) + if (name != "") { + pstring = paste(name, "=", pstring) + } + + if (!identical(thispar[[1]], quote(`:`))) { + formerr(pstring, "`:` was missing or at unexpected position.") + } + + if (name == "") { # no default + constructor.params$id = as.character(thispar[[2]]) + } else { + constructor.params$id = name + if (!identical(thispar[[2]], NA)) { + constructor.params["default"] = list(eval(thispar[[2]], envir = pss.env)) + } + } + + pdeco = thispar[[3]] + if (is.recursive(pdeco) && identical(pdeco[[1]], quote(`^`))) { # dimension + rl = parseDimension(pdeco, pstring, pss.env) + pdeco = rl$pdeco + constructor.params$len = rl$len + is.vector = TRUE + } + if (is.recursive(pdeco) && identical(pdeco[[1]], quote(`[[`))) { # settings + additional.settings = as.list(pdeco) + additional.settings[[1]] = NULL # delete `[[` + additional.settings[[1]] = NULL # delete part before `[[` + additional.settings = lapply(additional.settings, function(x) eval(x, envir = pss.env)) + pdeco = pdeco[[2]] + } + if (!is.recursive(pdeco)) { + pdeco = list(pdeco) # so we can access [[1]] + } + if (identical(pdeco[[1]], quote(`[`))) { + pdeco[[1]] = NULL + } else if (!( + identical(pdeco[[1]], quote(logical)) || identical(pdeco[[1]], quote(funct)) || + identical(pdeco[[1]], quote(character)) || identical(pdeco[[1]], quote(untyped)))) { + # the only type that may have no range attached is `logical` + if (as.character(pdeco[[1]]) %in% c("integer", "numeric", "logical", "discrete")) { + formerr(pstring, sprintf("range is missing")) + } else { + formerr(pstring, sprintf("range must be indicated using square brackets")) + } + } + ptype = as.character(pdeco[[1]]) + if (!ptype %in% c("integer", "numeric", "logical", "discrete", "funct", "character", "untyped")) { + formerr(pstring, sprintf("Unknown parameter type %s", deparseJoin(pdeco[[1]]))) + } + + # check sanity of 'range' part + needspostfix = ptype %in% c("discrete", "numeric", "integer") + if (!needspostfix && length(pdeco) > 1) { + formerr(pstring, sprintf("Logical parameter with unexpected postfix: %s", deparseJoin(pdeco))) + } + if (needspostfix && length(pdeco) == 1) { + formerr(pstring, sprintf("Parameter of type '%s' needs range postfix.", ptype)) + } + + # interpret range of discrete parameters + if (ptype == "discrete") { + constructor.params$values = parseDiscrete(pdeco, pstring, pss.env) + } + if (ptype %in% c("numeric", "integer")) { + constructor.params = insert(constructor.params, parseNumeric(pdeco, ptype, pstring, pss.env)) + } + constructor.params = insert(constructor.params, additional.settings) + do.call(getConstructor(ptype, is.learner, is.vector), constructor.params, quote = TRUE) +} + +# deparseJoin: deparse, but work with longer than 500 char expressions, mostly. +# Note that this is a heuristic for user messages only, the result can not be +# parsed again! +deparseJoin = function(what, sep = " ") { + collapse(deparse(what, 500), sep = sep) +} + diff --git a/man/LearnerParam.Rd b/man/LearnerParam.Rd index ee103df7..9dde21a6 100644 --- a/man/LearnerParam.Rd +++ b/man/LearnerParam.Rd @@ -85,8 +85,6 @@ this parameter only makes sense if its requirements are satisfied (dependent par Can be an object created either with \code{expression} or \code{quote}, the former type is auto-converted into the later. Only really useful if the parameter is included in a \code{\link{ParamSet}}. -Note that if your dependent parameter is a logical Boolean you need to verbosely write -\code{requires = quote(a == TRUE)} and not \code{requires = quote(a)}. Default is \code{NULL} which means no requirements.} \item{tunable}{[\code{logical(1)}]\cr diff --git a/man/Param.Rd b/man/Param.Rd index ab407a64..3c9498fa 100644 --- a/man/Param.Rd +++ b/man/Param.Rd @@ -91,8 +91,6 @@ this parameter only makes sense if its requirements are satisfied (dependent par Can be an object created either with \code{expression} or \code{quote}, the former type is auto-converted into the later. Only really useful if the parameter is included in a \code{\link{ParamSet}}. -Note that if your dependent parameter is a logical Boolean you need to verbosely write -\code{requires = quote(a == TRUE)} and not \code{requires = quote(a)}. Default is \code{NULL} which means no requirements.} \item{tunable}{[\code{logical(1)}]\cr diff --git a/man/discrete.Rd b/man/discrete.Rd new file mode 100644 index 00000000..e7b33335 --- /dev/null +++ b/man/discrete.Rd @@ -0,0 +1,11 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/ParamSetSugar.R +\name{discrete} +\alias{discrete} +\title{defined to avoid problems with the static type checker} +\usage{ +discrete() +} +\description{ +defined to avoid problems with the static type checker +} diff --git a/man/funct.Rd b/man/funct.Rd new file mode 100644 index 00000000..3ab205ca --- /dev/null +++ b/man/funct.Rd @@ -0,0 +1,11 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/ParamSetSugar.R +\name{funct} +\alias{funct} +\title{defined to avoid problems with the static type checker} +\usage{ +funct() +} +\description{ +defined to avoid problems with the static type checker +} diff --git a/man/pSS.Rd b/man/pSS.Rd new file mode 100644 index 00000000..257f1106 --- /dev/null +++ b/man/pSS.Rd @@ -0,0 +1,85 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/ParamSetSugar.R +\name{pSS} +\alias{pSS} +\alias{pSSLrn} +\title{Turn the argument list into a \code{ParamSet} of \code{LearnerParam}s} +\usage{ +pSS(..., .pss.learner.params = FALSE, .pss.env = parent.frame()) + +pSSLrn(..., .pss.learner.params = TRUE, .pss.env = parent.frame()) +} +\arguments{ +\item{...}{Parameters, see description.} + +\item{.pss.learner.params}{[\code{logical}]\cr +Whether to create \code{\link{LearnerParam}} instead of \code{\link{Param}} objects. +Default is \code{TRUE} for \code{pSSLrn} and \code{FALSE} for \code{pSS}.} + +\item{.pss.env}{[\code{environment}]\cr +Which environment to use when evaluating expressions. Defaults to the calling +function's frame.} +} +\description{ +\code{pSS}, short for \code{paramSetSugar}, is a shorthand API for \code{ParamHelpers} +which enables entry of \code{\link{ParamSet}}s in short form. It behaves similarly to +\code{\link{makeParamSet}}, but instead of having to construct each parameter individually, +the parameters can be given in shorthand form with a convenient syntax, making use of R's +nonstandard evaluation. + +The arguments are of the form + +\code{name = default: type range [^ dimension] [settings]}. + +\code{name} is any valid R identifier name. + +\dQuote{= default} Determines the 'default' setting +in \dQuote{makeXXXLearnerParam}. Note that this is different from an R function parameter +default value, in that it serves only as information to the user and does not set the +parameter to this value if it is not given. To define `no default`, use NA or +leave the \dQuote{= default} part out. Leaving it out can cause problems when R's static +type checker verifies a package, so this is *only* recommended for interactive sessions +and top-level applications! (To actually set a parameter default to NA, put it in parentheses) + +\code{type} is one of +\dQuote{integer}, \dQuote{numeric}, \dQuote{logical}, \dQuote{discrete}, \dQuote{funct}, \dQuote{character}, \dQuote{untyped} + +\code{range} is absent for type \dQuote{logical}, \dQuote{funct}, \dQuote{character}, or \dQuote{untyped}. For \dQuote{discrete}, +it is either \code{[valuelist]} with \code{valuelist} evaluating to a list, +or of the form \code{[value1, value2, ...]}, creating a discrete parameter of character +or numeric values according to \code{value1}, +\code{value2} etc. If \code{type} is one of \dQuote{integer} or \dQuote{numeric}, +\code{range} is of the form \code{[lowBound, upBound]}, where \code{lowBound} +and \code{upBound} must either be numerical (or integer) values indicating the +lower and upper bound, or may be missing (indicating the absence of a bound). To indicate +an exclusive bound, prefix the values with a tilde (\dQuote{~}). For a \dQuote{numeric} variable, to +indicate an unbounded value which may not be infinite, you can use \code{~Inf} resp \code{~-Inf}, +or use tilde-dot (\dQuote{~.}). + +\code{^ dimension} may be absent, resulting in a normal \code{\link{LearnerParam}}, or present, +resulting in a \code{VectorLearnerParam}. Note that a one-dimensional \code{VectorLearnerParam} +is distinct from a normal \code{\link{LearnerParam}}. + +\code{settings} may be a collection of further settings to supply to \code{makeXXXLearnerParam} +and is optional. To specify a series of settings, put in double square brackets (\code{[[}, \code{]]}), +and comma-separate settings if more than one is present. + +This makes definition of \code{\link{ParamSet}}s shorter and more readable. + +The difference between \code{pSS} and \code{pSSLrn} is only in the default value of \code{.pss.learner.params} +being \code{FALSE} for the former and \code{TRUE} for the latter. +} +\examples{ +pSSLrn(a = NA: integer [~0, ]^2 [[requires = expression(b != 0)]], + b = -10: numeric [~., 0], c: discrete [x, y, 1]) +# is equivalent to +makeParamSet( + makeIntegerVectorLearnerParam('a', len = 2, lower = 1, # note exclusive bound + upper = Inf, requires = expression(b != 0)), + makeNumericLearnerParam('b', lower = -Inf, upper = 0, + allow.inf = FALSE, default = -10), # note infinite value is prohibited. + makeDiscreteLearnerParam('c', values = list(x = "x", y = "y", `1` = 1)) +) + + +} diff --git a/man/untyped.Rd b/man/untyped.Rd new file mode 100644 index 00000000..84ce2a62 --- /dev/null +++ b/man/untyped.Rd @@ -0,0 +1,11 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/ParamSetSugar.R +\name{untyped} +\alias{untyped} +\title{defined to avoid problems with the static type checker} +\usage{ +untyped() +} +\description{ +defined to avoid problems with the static type checker +} diff --git a/tests/testthat/test_paramSetSugar.R b/tests/testthat/test_paramSetSugar.R new file mode 100644 index 00000000..127a30ec --- /dev/null +++ b/tests/testthat/test_paramSetSugar.R @@ -0,0 +1,120 @@ + +context("ParamSetSugar") + +test_that("ParamSetSugar generates the expected ParamSets", { + + param.list = list(a = "b", b = "a") + + x = 10 + + # list of pairs that whould have identical elements + param.sets = list( + list(makeParamSet(makeIntegerLearnerParam("a"), makeIntegerLearnerParam("b")), + pSSLrn(a: integer[, ], b = NA: integer[, ])), + list(makeParamSet(makeDiscreteLearnerParam("b", c("a", "b", "c")), + makeDiscreteLearnerParam("c", list(a = 1, b = 2, c = 3), default = 1), + makeDiscreteVectorLearnerParam("d", 2, list(a = "a", b = "b", `1` = 1), default = list("a", 1)), + makeDiscreteLearnerParam("e", param.list, when = "both", requires = expression(b == "a"), default = "b")), + pSSLrn(b: discrete[a, b, c], + c = 1: discrete[a = 1, b = 2, c = 3], + d = list("a", 1): discrete[a, b, 1]^2, + e = "b": discrete[param.list] [[when = "both", requires = expression(b == "a")]])), + list(makeParamSet(makeNumericParam("a", allow.inf = TRUE, default = 1, tunable = FALSE), + makeNumericParam("b", lower = 0, allow.inf = TRUE, default = 2), + makeNumericVectorParam("c", 3, upper = 0, allow.inf = FALSE, special.vals = list(-1)), + makeNumericVectorParam("d", 2, lower = 0, upper = 1, default = c(0.5, 0.5), requires = expression(a == 0))), + pSSLrn(a = 1: numeric[, ] [[tunable = FALSE]], + b = 2: numeric[0, ], + c: numeric[~., x - 10] ^ (2 + 1) [[special.vals = list(1 - 2)]], + d = c(1 / 2, 0.5): numeric[0, 1]^2 [[requires = expression(a == 0), allow.inf = FALSE]], .pss.learner.params = FALSE)), + list(makeParamSet(), + pSSLrn()) + ) + + for (ps.pair in param.sets) { + expect_identical(ps.pair[[1]], ps.pair[[2]]) + } + +}) + +test_that("ParamSetSugar works with params that have no bounds", { + + ps.sugar = pSS(a = TRUE: logical, b = identity: funct, c = "test": character, d = list(1, 2, 3): untyped) + ps.normal = makeParamSet( + makeLogicalParam("a", TRUE), + makeFunctionParam("b", identity), + makeCharacterParam("c", "test"), + makeUntypedParam("d", list(1, 2, 3))) + + expect_identical(ps.sugar, ps.normal) + +}) + +test_that("ParamSetSugar handles exotic bounds well", { + + # list of triplets: (param set, feasible example points, infeasible example points). + + rangetests = list( + list(pSSLrn(a: integer[, ]), + list(list(a = -1), list(a = 0), list(a = 2^30)), + list(list(a = -Inf), list(a = Inf), list(a = 0.5))), + list(pSSLrn(a: numeric[, ]), + list(list(a = 0), list(a = .Machine$double.xmax), list(a = -Inf)), + list()), + list(pSSLrn(a: integer[0, ]), + list(list(a = 0), list(a = 1), list(a = 100)), + list(list(a = -1), list(a = 0.5))), + list(pSSLrn(a: numeric[0, ~.]), + list(list(a = 0), list(a = .Machine$double.xmax)), + list(list(a = -1), list(a = Inf))), + list(pSSLrn(a: numeric[~0, ]), + list(list(a = Inf), list(a = 1), list(a = .Machine$double.eps)), + list(list(a = -1), list(a = 0), list(a = -Inf))), + list(pSSLrn(a: numeric[~., ]), + list(list(a = Inf), list(a = -.Machine$double.xmax)), + list(list(a = -Inf))), + list(pSSLrn(a: numeric[~., ~Inf]), + list(list(a = 0), list(a = .Machine$double.xmax)), + list(list(a = Inf), list(a = -Inf))), + list(pSSLrn(a: numeric[~0, 1]), + list(list(a = .Machine$double.eps), list(a = 1), list(a = 0.5)), + list(list(a = 0), list(a = -Inf), list(a = Inf), list(a = 1 + .Machine$double.eps))), + list(pSSLrn(a: discrete[1, 2, 3]), + list(list(a = 1), list(a = 2), list(a = 3)), + list(list(a = "1"), list(a = 4))), + list(pSSLrn(a: discrete[a = "b", b = "c"]), + list(list(a = "b"), list(a = "c")), + list(list(a = "a"))), + list(pSSLrn(a: numeric[~1, ~2^30]), + list(list(a = 2), list(a = 2^30 - 1)), + list(list(a = 1), list(a = 2^30))) + ) + + for (rt in rangetests) { + for (feas in rt[[2]]) { + expect_true(isFeasible(rt[[1]], feas)) + } + for (infeas in rt[[3]]) { + expect_false(isFeasible(rt[[1]], infeas)) + } + } + +}) + +test_that("ParamSetSugar works when called indirectly", { + + pss2 = function(..., env) { + x = 3 + pSSLrn(..., .pss.env = env) + } + + pss1 = function(...) { + x = 2 + pss2(..., env = parent.frame()) + } + + x = 1 + expect_identical(pss1(.pss.learner.params = FALSE, a = x: integer[x, x * 2]^x [[tunable = (x == 2)]]), + makeParamSet(makeIntegerVectorParam("a", 1, 1, 2, default = 1, tunable = FALSE))) +}) + From f60a15950511816560f40f68ff3217f15affbdd0 Mon Sep 17 00:00:00 2001 From: mb706 Date: Mon, 11 Dec 2017 19:32:04 +0100 Subject: [PATCH 2/4] some documentation cleanup --- R/ParamSetSugar.R | 61 +++++++++++++++++++++++++++-------------------- 1 file changed, 35 insertions(+), 26 deletions(-) diff --git a/R/ParamSetSugar.R b/R/ParamSetSugar.R index 236a3d76..80c81bad 100644 --- a/R/ParamSetSugar.R +++ b/R/ParamSetSugar.R @@ -11,35 +11,42 @@ funct = list untyped = list -#' @title Turn the argument list into a \code{ParamSet} of \code{LearnerParam}s +#' @title Turn the argument list into a \code{ParamSet} #' #' @description #' -#' \code{pSS}, short for \code{paramSetSugar}, is a shorthand API for \code{ParamHelpers} +#' \code{pSS}, short for \dQuote{ParamSet Sugar}, is a shorthand API for \code{\link{makeParamSet}} #' which enables entry of \code{\link{ParamSet}}s in short form. It behaves similarly to #' \code{\link{makeParamSet}}, but instead of having to construct each parameter individually, #' the parameters can be given in shorthand form with a convenient syntax, making use of R's #' nonstandard evaluation. #' +#' This makes definition of \code{\link{ParamSet}}s shorter and more readable. +#' +#' The difference between \code{pSS} and \code{pSSLrn} is only in the default value of \code{.pss.learner.params} +#' being \code{FALSE} for the former and \code{TRUE} for the latter. +#' +#' @section Details: #' The arguments are of the form #' #' \code{name = default: type range [^ dimension] [settings]}. #' #' \code{name} is any valid R identifier name. #' -#' \dQuote{= default} Determines the 'default' setting -#' in \dQuote{makeXXXLearnerParam}. Note that this is different from an R function parameter +#' \code{= default} Determines the 'default' setting +#' in \code{makeXXXParam}. Note that this is different from an R function parameter #' default value, in that it serves only as information to the user and does not set the -#' parameter to this value if it is not given. To define `no default`, use NA or +#' parameter to this value if it is not given. To define \sQuote{no default}, use \code{NA} or #' leave the \dQuote{= default} part out. Leaving it out can cause problems when R's static -#' type checker verifies a package, so this is *only* recommended for interactive sessions -#' and top-level applications! (To actually set a parameter default to NA, put it in parentheses) +#' type checker verifies a package, so this is \emph{only} recommended for interactive sessions +#' and top-level applications. (To actually set a parameter default to NA, use \code{(NA)} in parentheses) #' #' \code{type} is one of -#' \dQuote{integer}, \dQuote{numeric}, \dQuote{logical}, \dQuote{discrete}, \dQuote{funct}, \dQuote{character}, \dQuote{untyped} +#' \dQuote{integer}, \dQuote{numeric}, \dQuote{logical}, \dQuote{discrete}, \dQuote{funct}, \dQuote{character}, \dQuote{untyped}. +#' Each of these types leads to a \code{\link{Param}} or \code{\link{LearnerParam}} of the given type to be created. #' -#' \code{range} is absent for type \dQuote{logical}, \dQuote{funct}, \dQuote{character}, or \dQuote{untyped}. For \dQuote{discrete}, -#' it is either \code{[valuelist]} with \code{valuelist} evaluating to a list, +#' \code{range} is optional and only used for \emph{integer}, \emph{numeric}, and \emph{discrete} parameters. +#' For \dQuote{discrete}, it is either \code{[valuelist]} with \code{valuelist} evaluating to a list, #' or of the form \code{[value1, value2, ...]}, creating a discrete parameter of character #' or numeric values according to \code{value1}, #' \code{value2} etc. If \code{type} is one of \dQuote{integer} or \dQuote{numeric}, @@ -47,22 +54,18 @@ untyped = list #' and \code{upBound} must either be numerical (or integer) values indicating the #' lower and upper bound, or may be missing (indicating the absence of a bound). To indicate #' an exclusive bound, prefix the values with a tilde (\dQuote{~}). For a \dQuote{numeric} variable, to -#' indicate an unbounded value which may not be infinite, you can use \code{~Inf} resp \code{~-Inf}, +#' indicate an unbounded value which may not be infinite, you can use \code{~Inf} or \code{~-Inf}, #' or use tilde-dot (\dQuote{~.}). #' -#' \code{^ dimension} may be absent, resulting in a normal \code{\link{LearnerParam}}, or present, -#' resulting in a \code{VectorLearnerParam}. Note that a one-dimensional \code{VectorLearnerParam} -#' is distinct from a normal \code{\link{LearnerParam}}. +#' \code{^ dimension} is optionally determining the dimension of a \sQuote{vector} parameter. +#' If it is absent, the result is a normal \code{\link{Param}} or \code{\link{LearnerParam}}, if it is present, +#' the result is a \code{Vector(Learner)Param}. Note that a one-dimensional \code{Vector(Learner)Param} +#' is distinct from a normal \code{(Learner)Param}. #' -#' \code{settings} may be a collection of further settings to supply to \code{makeXXXLearnerParam} -#' and is optional. To specify a series of settings, put in double square brackets (\code{[[}, \code{]]}), +#' \code{settings} may be a collection of further settings to supply to \code{makeXXXParam} +#' and is optional. To specify one or more settings, put in double square brackets (\code{[[}, \code{]]}), #' and comma-separate settings if more than one is present. #' -#' This makes definition of \code{\link{ParamSet}}s shorter and more readable. -#' -#' The difference between \code{pSS} and \code{pSSLrn} is only in the default value of \code{.pss.learner.params} -#' being \code{FALSE} for the former and \code{TRUE} for the latter. -#' #' @param ... Parameters, see description. #' @param .pss.learner.params [\code{logical}]\cr #' Whether to create \code{\link{LearnerParam}} instead of \code{\link{Param}} objects. @@ -73,15 +76,21 @@ untyped = list #' #' @examples #' pSSLrn(a = NA: integer [~0, ]^2 [[requires = expression(b != 0)]], -#' b = -10: numeric [~., 0], c: discrete [x, y, 1]) +#' b = -10: numeric [~., 0], +#' c: discrete [x, y, 1], +#' d: logical, +#' e: integer) +#' #' # is equivalent to +#' #' makeParamSet( -#' makeIntegerVectorLearnerParam('a', len = 2, lower = 1, # note exclusive bound +#' makeIntegerVectorLearnerParam("a", len = 2, lower = 1, # note exclusive bound #' upper = Inf, requires = expression(b != 0)), -#' makeNumericLearnerParam('b', lower = -Inf, upper = 0, +#' makeNumericLearnerParam("b", lower = -Inf, upper = 0, #' allow.inf = FALSE, default = -10), # note infinite value is prohibited. -#' makeDiscreteLearnerParam('c', values = list(x = "x", y = "y", `1` = 1)) -#' ) +#' makeDiscreteLearnerParam("c", values = list(x = "x", y = "y", `1` = 1), +#' makeLogicalLearnerParam("d"), +#' makeIntegerLearnerParam("e")) #' #' #' @export From bbc30a8a72e482eff1feacd03597769dd80d866b Mon Sep 17 00:00:00 2001 From: mb706 Date: Mon, 11 Dec 2017 20:57:56 +0100 Subject: [PATCH 3/4] reorganizing for legibility --- R/ParamSetSugar.R | 201 ++++++++++++++++++++++++++-------------------- 1 file changed, 116 insertions(+), 85 deletions(-) diff --git a/R/ParamSetSugar.R b/R/ParamSetSugar.R index 80c81bad..3f439e96 100644 --- a/R/ParamSetSugar.R +++ b/R/ParamSetSugar.R @@ -44,6 +44,7 @@ untyped = list #' \code{type} is one of #' \dQuote{integer}, \dQuote{numeric}, \dQuote{logical}, \dQuote{discrete}, \dQuote{funct}, \dQuote{character}, \dQuote{untyped}. #' Each of these types leads to a \code{\link{Param}} or \code{\link{LearnerParam}} of the given type to be created. +#' Note that \dQuote{character} is not available if \sQuote{Learner}-parameters are created. #' #' \code{range} is optional and only used for \emph{integer}, \emph{numeric}, and \emph{discrete} parameters. #' For \dQuote{discrete}, it is either \code{[valuelist]} with \code{valuelist} evaluating to a list, @@ -114,10 +115,10 @@ pSSLrn = function(..., .pss.learner.params = TRUE, .pss.env = parent.frame()) { ### Auxiliary functions # formerr: Give informational error about malformed parameter -formerr = function(pstring, specific) { +formerr = function(pstring, pattern, ...) { stopf("Parameter '%s' must be of the form\n%s\n%s", pstring, "NAME = DEFAULT: TYPE [RANGE] [^ DIMENSION] [SETTINGS]", - specific) + sprintf(pattern, ...)) } # get the makeXXXParam function appropriate for the type and vector-ness @@ -161,6 +162,115 @@ getConstructor = function(type, is.learner, is.vector) { } } + +### parsing single parameter +# this function does the heavy lifting: +# it takes the name and expression of a given parameter and returns +# the constructed ParamSet. +# @param name [character(1)] the "name" of this item in the call object, this is the name of the parameter, if present. +# @param thispar [language] the rest of the expression in the call, to be parsed +# @param is.learner [logical(1)] whether to construct a LearnerParam +# @param pss.env [environment] the environment to use +# @return [Param | LearnerParam] the constructed parameter. +parseSingleParameter = function(name, thispar, is.learner, pss.env) { + constructor.params = list() + additional.settings = list() + is.vector = FALSE + pstring = deparseJoin(thispar) # represent this parameter in warning / error messages + if (name != "") { + pstring = paste(name, "=", pstring) + } + + if (length(thispar) < 3 || !identical(thispar[[1]], quote(`:`))) { + formerr(pstring, "`:` was missing or at unexpected position.") + } + + if (name == "") { # no default, i.e. param is not of the form 'parname = default: type' + constructor.params$id = as.character(thispar[[2]]) + } else { + constructor.params$id = name + if (!identical(thispar[[2]], NA)) { + # the following uses single braces + list() to keep 'NULL' values + constructor.params["default"] = list(eval(thispar[[2]], envir = pss.env)) + } + } + + pdeco = thispar[[3]] # pdeco: the part after the ':' + + # We now need to go up the table of operator precedences, see 'help(Syntax)' + # We check the operators '^' (dimension), '[[' (additional settings), '[' (range) + + # ** dimension ** + if (is.recursive(pdeco) && identical(pdeco[[1]], quote(`^`))) { + rl = parseDimension(pdeco, pstring, pss.env) + pdeco = rl$pdeco # replace pdeco with what's left after removing dimension part + constructor.params$len = rl$len + is.vector = TRUE + } + + # ** settings ** + if (is.recursive(pdeco) && identical(pdeco[[1]], quote(`[[`))) { + additional.settings = as.list(pdeco) + additional.settings[[1]] = NULL # delete `[[` + additional.settings[[1]] = NULL # delete part before `[[` + additional.settings = lapply(additional.settings, function(x) eval(x, envir = pss.env)) + pdeco = pdeco[[2]] # replace pdeco with what's left after removing settings part + } + + # ** range (if any) ** + if (!is.recursive(pdeco)) { + pdeco = list(pdeco) # so we can access [[1]] + } + hasrange = identical(pdeco[[1]], quote(`[`)) + if (hasrange) { + pdeco[[1]] = NULL + } + + # from here on, pdeco[[1]] is the type, pdeco[[2]], ... is the range + + # ** actual parameter type ** + if (!is.name(pdeco[[1]])) { + formerr(pstring, "Unknown parameter type %s", deparseJoin(pdeco[[1]])) + } + + ptype = as.character(pdeco[[1]]) + + if (is.null(getConstructor(ptype, is.learner, is.vector))) { + if (ptype == "character") { + formerr(pstring, "Parameter type 'character' not allowed for Learner params.") + } + formerr(pstring, "Unknown parameter type %s", ptype) + } + + # ** check sanity of 'range' part ** + if (hasrange && ptype %nin% c("discrete", "numeric", "integer")) { + formerr(pstring, "'%s' parameter may not have range parameter: %s", ptype, deparseJoin(pdeco)) + } + if (!hasrange && length(pdeco) > 1) { + formerr(pstring, "'%s' parameter with unexpected postfix: %s", pstring, deparseJoin(pdeco)) + } + if (hasrange && length(pdeco) == 1) { + formerr(pstring, "'%s' may not have empty range.", ptype) + } + + # build the arguments to the make***Param constructor call in the 'constructor.params' variable + if (ptype == "discrete") { + if (!hasrange) { + formerr(pstring, "discrete parameter must be of the form 'param: discrete[val1, val2]'\nRange suffix ([...] part) is missing.") + } + constructor.params$values = parseDiscrete(pdeco, pstring, pss.env) # range is an enumeration of items, or a list + } + if (ptype %in% c("numeric", "integer")) { + if (hasrange) { + constructor.params = insert(constructor.params, parseNumeric(pdeco, ptype, pstring, pss.env)) # range is [lower, upper] + } else if (ptype == "numeric") { + constructor.params$allow.inf = TRUE + } + } + constructor.params = insert(constructor.params, additional.settings) + do.call(getConstructor(ptype, is.learner, is.vector), constructor.params, quote = TRUE) +} + ### parameter parsing sub-functions # parseDimension: parse the '^n' part indicating a vector @@ -176,8 +286,8 @@ parseDimension = function(pdeco, pstring, pss.env) { pdeco = pdeco[[2]] # remove '^...' part } len = eval(exponent, envir = pss.env) - if (!is.numeric(len) && !is.na(len)) { - formerr(pstring, sprintf("`^` found, but exponent %s did not eval to numeric.", deparseJoin(exponent))) + if (!is.atomic(len) || (!is.numeric(len) && !is.na(len))) { + formerr(pstring, "`^` found, but exponent %s did not eval to numeric.", deparseJoin(exponent)) } list(pdeco = pdeco, len = len) } @@ -197,7 +307,7 @@ parseDiscrete = function(pdeco, pstring, pss.env) { } else if (is.numeric(item)) { item } else { - formerr(pstring, sprintf("value list %s invalid", deparseJoin(pdeco))) + formerr(pstring, "value list %s invalid", deparseJoin(vallist)) } }) names(vallist) = sapply(vallist, as.character) @@ -223,7 +333,7 @@ parseNumeric = function(pdeco, ptype, pstring, pss.env) { } if (is.infinite(value)) { if (ptype == "integer") { - formerr(pstring, '"."-bounds (unbounded but excluding "Inf") are only allowed for "numeric" variables.') + formerr(pstring, '"~."-bounds (unbounded but excluding "Inf") are only allowed for "numeric" variables.') } if ((value < 0) == lower) { value = ifelse(lower, -quasi.inf, quasi.inf) @@ -271,85 +381,6 @@ parseNumeric = function(pdeco, ptype, pstring, pss.env) { rl } -### parsing single parameter -# this function does the heavy lifting: -# it takes the name and expression of a given parameter and returns -# the constructed ParamSet. -parseSingleParameter = function(name, thispar, is.learner, pss.env) { - constructor.params = list() - additional.settings = list() - is.vector = FALSE - pstring = deparseJoin(thispar) - if (name != "") { - pstring = paste(name, "=", pstring) - } - - if (!identical(thispar[[1]], quote(`:`))) { - formerr(pstring, "`:` was missing or at unexpected position.") - } - - if (name == "") { # no default - constructor.params$id = as.character(thispar[[2]]) - } else { - constructor.params$id = name - if (!identical(thispar[[2]], NA)) { - constructor.params["default"] = list(eval(thispar[[2]], envir = pss.env)) - } - } - - pdeco = thispar[[3]] - if (is.recursive(pdeco) && identical(pdeco[[1]], quote(`^`))) { # dimension - rl = parseDimension(pdeco, pstring, pss.env) - pdeco = rl$pdeco - constructor.params$len = rl$len - is.vector = TRUE - } - if (is.recursive(pdeco) && identical(pdeco[[1]], quote(`[[`))) { # settings - additional.settings = as.list(pdeco) - additional.settings[[1]] = NULL # delete `[[` - additional.settings[[1]] = NULL # delete part before `[[` - additional.settings = lapply(additional.settings, function(x) eval(x, envir = pss.env)) - pdeco = pdeco[[2]] - } - if (!is.recursive(pdeco)) { - pdeco = list(pdeco) # so we can access [[1]] - } - if (identical(pdeco[[1]], quote(`[`))) { - pdeco[[1]] = NULL - } else if (!( - identical(pdeco[[1]], quote(logical)) || identical(pdeco[[1]], quote(funct)) || - identical(pdeco[[1]], quote(character)) || identical(pdeco[[1]], quote(untyped)))) { - # the only type that may have no range attached is `logical` - if (as.character(pdeco[[1]]) %in% c("integer", "numeric", "logical", "discrete")) { - formerr(pstring, sprintf("range is missing")) - } else { - formerr(pstring, sprintf("range must be indicated using square brackets")) - } - } - ptype = as.character(pdeco[[1]]) - if (!ptype %in% c("integer", "numeric", "logical", "discrete", "funct", "character", "untyped")) { - formerr(pstring, sprintf("Unknown parameter type %s", deparseJoin(pdeco[[1]]))) - } - - # check sanity of 'range' part - needspostfix = ptype %in% c("discrete", "numeric", "integer") - if (!needspostfix && length(pdeco) > 1) { - formerr(pstring, sprintf("Logical parameter with unexpected postfix: %s", deparseJoin(pdeco))) - } - if (needspostfix && length(pdeco) == 1) { - formerr(pstring, sprintf("Parameter of type '%s' needs range postfix.", ptype)) - } - - # interpret range of discrete parameters - if (ptype == "discrete") { - constructor.params$values = parseDiscrete(pdeco, pstring, pss.env) - } - if (ptype %in% c("numeric", "integer")) { - constructor.params = insert(constructor.params, parseNumeric(pdeco, ptype, pstring, pss.env)) - } - constructor.params = insert(constructor.params, additional.settings) - do.call(getConstructor(ptype, is.learner, is.vector), constructor.params, quote = TRUE) -} # deparseJoin: deparse, but work with longer than 500 char expressions, mostly. # Note that this is a heuristic for user messages only, the result can not be From 7e3e20926157b680ea2cb352b0afbd1b25a4fe72 Mon Sep 17 00:00:00 2001 From: mb706 Date: Mon, 11 Dec 2017 20:58:08 +0100 Subject: [PATCH 4/4] a few more tests --- tests/testthat/test_paramSetSugar.R | 49 ++++++++++++++++++++++++++--- 1 file changed, 44 insertions(+), 5 deletions(-) diff --git a/tests/testthat/test_paramSetSugar.R b/tests/testthat/test_paramSetSugar.R index 127a30ec..d99978aa 100644 --- a/tests/testthat/test_paramSetSugar.R +++ b/tests/testthat/test_paramSetSugar.R @@ -9,8 +9,12 @@ test_that("ParamSetSugar generates the expected ParamSets", { # list of pairs that whould have identical elements param.sets = list( - list(makeParamSet(makeIntegerLearnerParam("a"), makeIntegerLearnerParam("b")), - pSSLrn(a: integer[, ], b = NA: integer[, ])), + + # empty defaults + list(makeParamSet(makeIntegerLearnerParam("a"), makeIntegerLearnerParam("b"), makeIntegerLearnerParam("c")), + pSSLrn(a: integer[, ], b = NA: integer[, ], c: integer)), + + # discrete parameters list(makeParamSet(makeDiscreteLearnerParam("b", c("a", "b", "c")), makeDiscreteLearnerParam("c", list(a = 1, b = 2, c = 3), default = 1), makeDiscreteVectorLearnerParam("d", 2, list(a = "a", b = "b", `1` = 1), default = list("a", 1)), @@ -19,16 +23,21 @@ test_that("ParamSetSugar generates the expected ParamSets", { c = 1: discrete[a = 1, b = 2, c = 3], d = list("a", 1): discrete[a, b, 1]^2, e = "b": discrete[param.list] [[when = "both", requires = expression(b == "a")]])), + + # ranges, extra parameters list(makeParamSet(makeNumericParam("a", allow.inf = TRUE, default = 1, tunable = FALSE), makeNumericParam("b", lower = 0, allow.inf = TRUE, default = 2), makeNumericVectorParam("c", 3, upper = 0, allow.inf = FALSE, special.vals = list(-1)), makeNumericVectorParam("d", 2, lower = 0, upper = 1, default = c(0.5, 0.5), requires = expression(a == 0))), - pSSLrn(a = 1: numeric[, ] [[tunable = FALSE]], + pSSLrn(a = 1: numeric [[tunable = FALSE]], b = 2: numeric[0, ], c: numeric[~., x - 10] ^ (2 + 1) [[special.vals = list(1 - 2)]], d = c(1 / 2, 0.5): numeric[0, 1]^2 [[requires = expression(a == 0), allow.inf = FALSE]], .pss.learner.params = FALSE)), + + # empty parameter set list(makeParamSet(), pSSLrn()) + ) for (ps.pair in param.sets) { @@ -39,12 +48,15 @@ test_that("ParamSetSugar generates the expected ParamSets", { test_that("ParamSetSugar works with params that have no bounds", { - ps.sugar = pSS(a = TRUE: logical, b = identity: funct, c = "test": character, d = list(1, 2, 3): untyped) + ps.sugar = pSS(a = TRUE: logical, b = identity: funct, c = "test": character, d = list(1, 2, 3): untyped, e = 1: numeric, f = 0: integer) + ps.normal = makeParamSet( makeLogicalParam("a", TRUE), makeFunctionParam("b", identity), makeCharacterParam("c", "test"), - makeUntypedParam("d", list(1, 2, 3))) + makeUntypedParam("d", list(1, 2, 3)), + makeNumericParam("e", default = 1, allow.inf = TRUE), + makeIntegerParam("f", default = 0)) expect_identical(ps.sugar, ps.normal) @@ -118,3 +130,30 @@ test_that("ParamSetSugar works when called indirectly", { makeParamSet(makeIntegerVectorParam("a", 1, 1, 2, default = 1, tunable = FALSE))) }) +test_that("ParamSetSugar gives error messages", { + + expect_error(pSS(test), ":` was missing") + expect_error(pSS(1 + 1), ":` was missing") + expect_error(pSS("a: integer"), ":` was missing") + + expect_error(pSS(x: function(){}), "Unknown parameter type function") + + expect_error(pSSLrn(x: character), "'character' not allowed for Learner params.") + expect_identical(pSS(x: character), makeParamSet(makeCharacterParam("x"))) + + expect_error(pSS(x: nonexistent), "Unknown parameter type nonexistent") + + expect_error(pSS(x: funct[1, 2]), "'funct' parameter may not have range parameter") + + expect_error(pSS(x: discrete), "discrete parameter must be of the form.*Range suffix.*is missing") + + expect_error(pSS(x: funct ^ list), "`\\^` found, but exponent list did not eval to numeric") + + expect_error(pSS(x: discrete[1, 2, 1 + 1]), "value list .*1 \\+ 1) invalid") + + expect_error(pSS(x: numeric[1, 2, 3]), "invalid numeric / integer range") + + expect_error(pSS(x: integer[~., ]), '"~."-bounds \\(unbounded but excluding "Inf"\\) are only allowed for "numeric" variables.') + +}) +