Skip to content

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/simple/discrete.py, where every variable is an integer, and examples/simple/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, just 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": VariableType.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.

VariableType comes from ropt.enums, not from ropt.simple:

from ropt.enums import VariableType

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": [
            VariableType.REAL,
            VariableType.REAL,
            VariableType.INTEGER,
            VariableType.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.

A gradient-free method reaches an answer by evaluating many points rather than by following a slope, so plan for a different cost profile:

  • perturbation_magnitudes is unused. No perturbations are evaluated, because no gradient is estimated.
  • Budget the run with max_functions or max_iterations rather than a convergence tolerance.
  • The result is reproducible only if the method's own generator is seeded — hence "options": {"rng": 4} in both scripts.

Where to next