File: isFeasible.R

package info (click to toggle)
r-cran-paramhelpers 1.14.2-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 992 kB
  • sloc: ansic: 102; sh: 13; makefile: 2
file content (173 lines) | stat: -rw-r--r-- 6,720 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
#' @title Check if parameter value is valid.
#'
#' @description Check if a parameter value satisfies the constraints of the
#' parameter description. This includes the `requires` expressions and the
#' `forbidden` expression, if `par` is a [ParamSet()]. If `requires` is not
#' satisfied, the parameter value must be set to scalar `NA` to be still
#' feasible, a single scalar even in a case of a vector parameter. If the result
#' is `FALSE` the attribute `"warning"` is attached which gives the reason for
#' the negative result.
#'
#' If the parameter has `cnames`, these are also checked.
#'
#' @template arg_par_or_set
#' @param x (any) \cr
#'   Single value to check against the `Param` or `ParamSet`. For a `ParamSet`
#'   `x` must be a list. `x` has to contain the untransformed values. If the
#'   list is named, it is possible to only pass a subset of parameters defined
#'   in the [ParamSet()] `par`. In that case, only conditions regarding the
#'   passed parameters are checked. (Note that this might not work if one of the
#'   passed params has a `requires` setting which refers to an unpassed param.)
#' @param use.defaults (`logical(1)`)\cr
#'   Whether defaults of the [Param()]/[ParamSet()] should be used if no values
#'   are supplied. If the defaults have requirements that are not met by `x` it
#'   will be feasible nonetheless. Default is `FALSE`.
#' @param filter (`logical(1)`)\cr
#'   Whether the [ParamSet()] should be reduced to the space of the given Param
#'   Values. Note that in case of `use.defaults = TRUE` the filtering will be
#'   conducted after the insertion of the default values. Default is `FALSE`.
#' @return `logical(1)`.
#' @examples
#' p = makeNumericParam("x", lower = -1, upper = 1)
#' isFeasible(p, 0) # True
#' isFeasible(p, 2) # False, out of bounds
#' isFeasible(p, "a") # False, wrong type
#' # now for parameter sets
#' ps = makeParamSet(
#'   makeNumericParam("x", lower = -1, upper = 1),
#'   makeDiscreteParam("y", values = c("a", "b"))
#' )
#' isFeasible(ps, list(0, "a")) # True
#' isFeasible(ps, list("a", 0)) # False, wrong order
#' @export
isFeasible = function(par, x, use.defaults = FALSE, filter = FALSE) {
  UseMethod("isFeasible")
}

#' @export
isFeasible.Param = function(par, x, use.defaults = FALSE, filter = FALSE) {
  # we don't have to consider requires here, it is not a param set
  constraintsOkParam(par, x)
}

#' @export
isFeasible.LearnerParam = function(par, x, use.defaults = FALSE, filter = FALSE) {
  # we don't have to consider requires here, it is not a param set
  constraintsOkParam(par, x)
}

#' @export
isFeasible.ParamSet = function(par, x, use.defaults = FALSE, filter = FALSE) {

  named = testNamed(x)
  assertList(x)
  res = FALSE
  # insert defaults if they comply with the requirements
  if (named && use.defaults) {
    x = updateParVals(old.par.vals = getDefaults(par), new.par.vals = x, par.set = par)
  }
  if (!named && filter) {
    stopf("filter = TRUE only works with named input")
  }
  if (named && any(names(x) %nin% getParamIds(par))) {
    stopf("Following names of given values do not match with ParamSet: %s", collapse(setdiff(names(x), getParamIds(par))))
  }
  if (isForbidden(par, x)) {
    attr(res, "warning") = "The given parameter setting has forbidden values."
    return(res)
  }
  if (filter) {
    par = filterParams(par, ids = names(x))
    x = x[getParamIds(par)]
  } else if (length(x) != length(par$pars)) {
    stopf("Param setting of length %i does not match ParamSet length %i", length(x), length(par$pars))
  }
  if (!named) {
    names(x) = getParamIds(par)
  }
  missing.reqs = setdiff(getRequiredParamNames(par), names(x))
  if (length(missing.reqs) > 0) {
    stopf("Following parameters are missing but needed for requirements: %s", collapse(missing.reqs))
  }

  # FIXME: very slow
  for (i in seq_along(par$pars)) {
    p = par$pars[[i]]
    v = x[[i]]
    # no requires, just check constraints
    if (!requiresOk(p, x)) {
      # if not, val must be NA
      if (!isScalarNA(v)) {
        attr(res, "warning") = sprintf("Param %s is set but does not meet requirements %s", convertToShortString(x[i]), sQuote(collapse(deparse(p$requires), sep = "")))
        return(res)
      }
    } else {
      # requires, ok, check constraints
      if (!isFeasible(p, v)) {
        attr(res, "warning") = sprintf("The parameter setting %s does not meet constraints", convertToShortString(x[i]))
        return(res)
      }
    }
  }
  return(TRUE)
}

# are the contraints ok for value of a param (not considering requires)
constraintsOkParam = function(par, x) {
  if (isSpecialValue(par, x)) {
    return(TRUE)
  }
  type = par$type
  # this should work in any! case.
  if (type == "untyped") {
    return(TRUE)
  }
  inValues = function(v) any(vlapply(par$values, function(w) isTRUE(all.equal(w, v))))
  ok = if (type == "numeric") {
    is.numeric(x) && length(x) == 1 && (par$allow.inf || is.finite(x)) && inBoundsOrExpr(par = par, x = x)
  } else if (type == "integer") {
    is.numeric(x) && length(x) == 1 && is.finite(x) && inBoundsOrExpr(par, x) && x == as.integer(x)
  } else if (type == "numericvector") {
    is.numeric(x) && checkLength(par, x) && all((par$allow.inf | is.finite(x)) & inBoundsOrExpr(par, x))
  } else if (type == "integervector") {
    is.numeric(x) && checkLength(par, x) && all(is.finite(x) & inBoundsOrExpr(par, x) & x == as.integer(x))
  } else if (type == "discrete") {
    inValues(x)
  } else if (type == "discretevector") {
    is.list(x) && checkLength(par, x) && all(vlapply(x, inValues))
  } else if (type == "logical") {
    is.logical(x) && length(x) == 1 && !is.na(x)
  } else if (type == "logicalvector") {
    is.logical(x) && checkLength(par, x) && !anyMissing(x)
  } else if (type == "character") {
    is.character(x) && length(x) == 1 && !is.na(x)
  } else if (type == "charactervector") {
    is.character(x) && checkLength(par, x) && !anyMissing(x)
  } else if (type == "function") {
    is.function(x)
  }
  # if we have cnames, check them
  if (!is.null(par$cnames)) {
    ok = ok && !is.null(names(x)) && all(names(x) == par$cnames)
  }
  return(ok)
}

# checks if the requires part of the i-th param is valid for value x (x[[i]] is value or i-th param)
requiresOk = function(par, x) {
  if (is.null(par$requires)) {
    TRUE
  } else {
    isTRUE(eval(par$requires, envir = x))
  }
}


# helper function which checks whether 'x' lies within the boundaries (unless they are expressions)
inBoundsOrExpr = function(par, x) {
  (is.expression(par$lower) || all(x >= par$lower)) && (is.expression(par$upper) || all(x <= par$upper))
}

checkLength = function(par, x) {
  (is.expression(par$len) || is.na(par$len) || length(x) == par$len)
}