Discrete and Mixed-Integer Variables
By default every variable is continuous. Marking some or all of them as
integer-valued takes one field, variables.types, but it changes what the rest
of the configuration has to look like: an integer variable cannot be
differentiated, so the problem needs a method that searches without gradients,
and that method needs bounds.
There are two runnable scripts for this page: examples/discrete.py, where every variable is an integer, and examples/mixed.py, where continuous and integer variables appear in one problem.
Warning
Only differential_evolution handles integer variables. The other SciPy
methods silently treat them as continuous — no error, only a fractional
answer to a problem you meant to be discrete. Choosing the method is not
optional here.
All variables integer
discrete.py maximizes min(3x, y) over two integers, subject to
x + y <= 10. The types field takes a single value that applies to every
variable:
config: dict[str, Any] = {
"variables": {
"variable_count": 2,
"types": "integer",
"lower_bounds": [0.0, 0.0],
"upper_bounds": [10.0, 10.0],
},
"optimizer": {
"max_functions": 5,
},
"backend": {
"method": "differential_evolution",
"options": {"rng": 4},
"parallel": False,
},
}
Three things go together. types marks the variables as integers; the backend
section selects differential_evolution, the only method that will respect
that; and lower_bounds / upper_bounds are mandatory, because that method
searches within a box rather than stepping from a start point.
types takes the strings "real" and "integer". These are the values of
VariableType, whose members are strings, so
VariableType.INTEGER equals "integer". The scripts use the strings; the
members, imported from ropt.enums, are accepted in their place.
The types field, and every other field of the variables section, is described
under variables.
The objective is an ordinary evaluation function. It receives the variables as floats that happen to hold integral values:
def function(
variables: NDArray[np.float64],
_context: EvaluationFunctionContext,
) -> float | list[float]:
x, y = variables
objective = -min(3.0 * x, y)
if linear:
return float(objective)
return [float(objective), float(x + y)]
The script imposes x + y <= 10 as a nonlinear constraint by default, and as a
linear one with --linear. Both forms work unchanged with integer variables;
see Constraints.
Mixing continuous and integer variables
To make only some variables discrete, give types one entry per variable
instead of a single value. mixed.py does this for the first four variables of
an ensemble Rosenbrock problem, keeping two continuous and two integer:
CONFIG: dict[str, Any] = {
"variables": {
"variable_count": DIM,
"perturbation_magnitudes": 1e-6,
"lower_bounds": 0.0,
"upper_bounds": 10.0,
"types": ["real", "real", "integer", "integer"],
},
"backend": {
"method": "differential_evolution",
"options": {"rng": 4},
"max_iterations": 50,
},
"realizations": {
"weights": [1.0] * REALIZATIONS,
},
}
Nothing else changes. The realizations, the objective and the call to
optimize
are the same as in Ensemble-Based Optimization
— being partly discrete is a property of the variables, not of the problem
around them.
What to expect from the search
A gradient-free method reaches an answer by evaluating many points rather than by following a gradient, and the cost profile differs accordingly:
perturbation_magnitudesis unused. No perturbations are evaluated, because no gradient is estimated.- Limit the optimization with
max_functionsormax_iterationsrather than a convergence tolerance. - The result is reproducible only if the method's own generator is seeded —
hence
"options": {"rng": 4}in both scripts.