Skip to content

Variables

The variable vector is the one thing every configuration must define: the values the optimizer is allowed to change. variable_count sets how many there are, and everything else about them — bounds, types, which are held fixed, and the units the optimizer works in — is optional.

CONFIG = {
    "variables": {
        "variable_count": 3,
        "lower_bounds": -1.0,
        "upper_bounds": [1.0, 2.0, 3.0],
    },
}

A single value is broadcast to every variable, so lower_bounds above applies to all three. The defaults are \(-\infty\) and \(+\infty\), which is to say unbounded. The field-by-field description is under variables.

Holding variables fixed

The mask field marks which variables the optimizer may change. It takes one boolean per variable, True for free and False for fixed, and defaults to all free:

"variables": {
    "variable_count": 3,
    "mask": [True, True, False],   # the third variable is held at its start value
}

A fixed variable keeps the value it had in the initial vector, is never perturbed, and is reported back unchanged. This is the way to explore a subset of a larger problem without rewriting the configuration around it.

Integer variables

The types field makes variables integer-valued rather than continuous. That choice constrains the rest of the configuration, because an integer variable cannot be differentiated — see Discrete and Mixed-Integer Variables.

Scaling

Variables reach the optimizer as \(y = (x - o)/s\), using the scales and offsets fields, and are reported back as \(x = s\,y + o\). Both directions come from the same two arrays, so they cannot disagree.

This matters when variables differ by orders of magnitude. An optimizer takes a step of the same size in every direction and judges convergence with a single tolerance for all of them, and neither is meaningful unless the variables are comparable in size. The bounds are one source of scales, and scales_and_offsets_from_bounds derives them:

from ropt.utils import scales_and_offsets_from_bounds

scales, offsets = scales_and_offsets_from_bounds([0.0, 100.0], [1.0, 600.0])

That maps both variables onto \([0, 1]\). Scales must be positive, since a scale is a change of units and nothing else. There is no auto_scale for variables: the bounds are the only information available before the run starts, and using them is a choice rather than a default.

Everything describing the variables moves with them — the bounds, the absolute perturbation magnitudes, and the linear constraints — so scaling is not something that has to be applied consistently by hand. The details are under Scaling the variables.

Scaling changes what a run reports

Results carry both domains: variables in the units configured here, and scaled.variables in the optimizer's. Only the first is comparable between runs that scale differently. The runnable demonstration is examples/simple/scaling.py, and Working with Results describes which fields have two domains.

Perturbations

The remaining fields of the section — perturbation_magnitudes, perturbation_types, boundary_types, samplers and seed — describe how variables are perturbed when a gradient is estimated, rather than the variables themselves. They are covered in Stochastic Gradients and Samplers.