Optimizer Backends
ropt.backend
Optimizer backends: the bridge to an external optimization library.
A backend receives an
OptimizationProblem, requests function and
gradient values through the
OptimizerCallback interface, and advances the
optimization from an initial variable vector. It is selected through the
backend field of an EnOptContext, either as an
instance or as a BackendConfig naming a method.
The context itself never reaches the backend: ropt reduces it to an
OptimizationProblem when the run starts.
ropt ships SciPyBackend and
ExternalBackend, which runs an
optimizer in a separate process; further backends come from plugin packages. See
Writing a Plugin for implementing one.
Backend
Bases: ABC
Abstract base class for optimizer backend implementations.
A backend configures a concrete optimization algorithm, interacts with the
ropt evaluation pipeline through an
OptimizerCallback, and executes the main
optimization loop. See
Writing plugins.
What a backend receives
What a backend is asked to solve arrives as an
OptimizationProblem: already scaled,
and already reduced to the free variables. A backend therefore neither
scales nor masks anything; ropt unscales and expands results for
reporting.
Non-linear constraints arrive normalized, as values that are
non-negative when the constraint is satisfied, so a backend compares them
against zero and never handles a bound. See
OptimizerCallbackResult. A backend
whose algorithm expects the opposite convention negates the values and
their gradients.
Process-global state
While a run is in progress a backend must not change the working directory,
the environment, sys.stdout or sys.stderr, or file descriptors 1 and 2,
since it shares them with every other run in the process. See
What a backend may not change.
methods
class-attribute
The optimization algorithms this class provides.
Either a set of names, which the registry matches case-insensitively, or a
predicate for classes that cannot enumerate them. Include "default" in the
set if this class has one. See MethodSpec.
__init__
abstractmethod
Create a new backend instance.
Called during instantiation. Subclasses should store the configuration
and perform any lightweight initialization. Validation and
problem-dependent setup should usually be deferred to validate_options
and start.
Method name with prefix
backend_config.method may be prefixed in the form
"backend/method". Implementations should account for this when
parsing the method name.
Handling the default method
backend_config.method may be set to "default", in which case it
should be mapped to the backend's actual default method.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
backend_config
|
BackendConfig
|
Configuration specifying the method and its options. |
required |
start
abstractmethod
start(
problem: OptimizationProblem,
optimizer_callback: OptimizerCallback,
*,
evaluation_policy: Literal[
"speculative", "separate", "auto"
],
output_dir: Path | None,
) -> None
Run the optimization algorithm on the given problem.
Starts the backend's main optimization loop. During execution, the
implementation uses optimizer_callback to request any objective,
constraint, or gradient evaluations its algorithm needs.
Called at most once per backend instance.
evaluation_policy is whether functions and gradients should be asked
for together ("speculative"), in separate calls ("separate"), or as
the algorithm happens to need them ("auto").
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
problem
|
OptimizationProblem
|
The problem to solve, in free-variable space. |
required |
optimizer_callback
|
OptimizerCallback
|
Callback for requesting evaluations from |
required |
evaluation_policy
|
Literal['speculative', 'separate', 'auto']
|
How functions and gradients are requested. |
required |
output_dir
|
Path | None
|
Directory for files the optimizer writes, or |
required |
bypasses_python_output
property
Indicate whether the optimizer prints without going through Python.
Compiled optimizers commonly write to file descriptors 1 and 2 directly
instead of through sys.stdout, which puts their output beyond reach of
the capture ropt applies for the stdout and stderr settings of
OptimizerConfig. A backend wrapping
such a library should override this to return True, and ropt then
redirects the descriptors as well.
The answer may differ per method, in which case return it based on the
configured method. Return True for the whole backend when unsure: the
cost is that capture briefly rewires process-global state, whereas the
cost of being wrong the other way is output escaping to the terminal.
See Writing a Plugin.
Returns:
| Type | Description |
|---|---|
bool
|
|
validate_options
abstractmethod
Validate backend-specific options for the configured method.
Checks that the options supplied through the
BackendConfig object have the expected
type, contain only supported keys, and satisfy any method-specific
value constraints. Options are a dictionary or a list, depending on the
backend. The exception raised must be a ValueError, or derive from one.
Raises:
| Type | Description |
|---|---|
ValueError
|
If the provided options are invalid. |
OptimizationProblem
The problem a backend is asked to solve.
Everything on this object is scaled and lives in free-variable
space: the variables that variables.mask fixes are gone, the bounds and
the linear constraints are reduced to the ones that remain, and the values
delivered through the OptimizerCallback
use the same space. A backend therefore neither scales nor masks anything.
See Backend for the rest of the contract.
Attributes:
| Name | Type | Description |
|---|---|---|
initial_values |
The starting point, shape |
|
lower_bounds |
The lower bounds, shape |
|
upper_bounds |
The upper bounds, shape |
|
variable_types |
The type of each variable, shape |
__init__
Reduce a context and a full-space starting point to a problem.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
context
|
EnOptContext
|
The optimization context. |
required |
initial_values
|
NDArray[float64]
|
The values of all variables, including fixed ones. |
required |
variable_count
property
The number of free variables.
Returns:
| Type | Description |
|---|---|
int
|
The number of variables the optimizer sees. |
linear_constraints
property
linear_constraints: (
tuple[
NDArray[float64],
NDArray[float64],
NDArray[float64],
NDArray[bool_],
]
| None
)
The linear constraints, reduced to the problem the optimizer solves.
Rows that cannot constrain the optimization are dropped: a row is kept only if it has a non-zero coefficient on a free variable and at least one finite bound. The columns of the fixed variables are removed, and the contribution those variables make is folded into the bounds.
The four arrays are aligned, with one entry per surviving constraint.
equality marks the constraints whose bounds coincide; it follows the
bounds as configured, so scaling cannot change it. Pass the tuple to
split_linear_constraints
to get the same normalized form as the non-linear constraints.
Returns:
| Type | Description |
|---|---|
tuple[NDArray[float64], NDArray[float64], NDArray[float64], NDArray[bool_]] | None
|
|
nonlinear_equalities
property
Which non-linear constraint values are equalities.
One flag per value delivered through the
OptimizerCallback: a constraint with a
finite lower and a finite upper bound delivers two values, an equality
one, and a constraint with no finite bound none.
Returns:
| Type | Description |
|---|---|
NDArray[bool_] | None
|
A flag per value, or |
validate_supported_constraints
validate_supported_constraints(
method: str,
supported_constraints: dict[str, set[str]],
required_constraints: dict[str, set[str]],
) -> None
Raise if this problem's constraints do not suit the chosen method.
Constraint types are identified by the keys "bounds", "linear:eq",
"linear:ineq", "nonlinear:eq" and "nonlinear:ineq".
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
method
|
str
|
The name of the optimization method used. |
required |
supported_constraints
|
dict[str, set[str]]
|
The methods supporting each constraint type. |
required |
required_constraints
|
dict[str, set[str]]
|
The methods requiring each constraint type. |
required |
Raises:
| Type | Description |
|---|---|
UnsupportedError
|
If the constraints do not match the method. |
ropt.backend.scipy.SciPyBackend
Bases: Backend
Backend implementation using SciPy optimization algorithms.
Implements the Backend interface to expose
optimization algorithms from
scipy.optimize
to ropt.
The algorithm is selected via the method field of the
BackendConfig object. Not every algorithm
accepts every kind of problem:
| Method | Gradient | Bounds | Linear eq. | Linear ineq. | Non-linear eq. | Non-linear ineq. | Integer |
|---|---|---|---|---|---|---|---|
| Nelder-Mead | |||||||
| Powell | |||||||
| CG | |||||||
| BFGS | |||||||
| Newton-CG | |||||||
| L-BFGS-B | |||||||
| TNC | |||||||
| COBYLA | |||||||
| COBYQA | |||||||
| SLSQP | |||||||
| trust-constr | |||||||
| differential_evolution | required |
A method without a gradient asks only for function values, so no
perturbations are evaluated for it. Configuring a method with a constraint
it does not support raises
UnsupportedError when the run starts.
differential_evolution requires bound constraints on all variables, and
is the only method that handles integer variables; the others silently
treat them as continuous.
SciPy prints its own progress report when
verbose is set. trust-constr is the
only method with reporting levels of its own; for the rest the setting is
on or off. disp is a no-op for l-bfgs-b and nelder-mead in
recent SciPy releases, which therefore report nothing.
Algorithm-specific options are passed through the options dictionary.
Click on the common options or the method name for the corresponding
scipy.optimize
documentation:
Common Options:
The keep_feasible option is used to maintain feasibility with
respect to bound, linear and non-linear constraints, by passing
it to the constraint handling code of the underlying SciPy
optimizer. Some algorithms may choose to ignore this option.
Hessian Options:
hess, exception_strategy, min_curvature, min_denominator, init_scale
These options are used to configure the Hessian approximation
method for the optimizer. The hess option specifies the type
of Hessian approximation to use ("BFGS" or "SR1"), while the
other options provide additional parameters for the chosen
method.
Method-specific Options:
| Method | Options |
|---|---|
| Nelder-Mead | maxfev, xatol, fatol, adaptive |
| Powell | maxfev, xtol, ftol |
| CG | gtol, norm, eps, finite_diff_rel_step, c1, c2 |
| BFGS | gtol, norm, eps, finite_diff_rel_step, xrtol, c1, c2 |
| Newton-CG | xtol, eps, c1, c2 |
| L-BFGS-B1 | disp, maxcor, ftol, gtol, eps, maxfun, iprint, maxls, finite_diff_rel_step |
| TNC2 | maxfun, eps, scale, offset, maxCGit, eta, stepmx, accuracy, minfev, ftol, xtol, gtol, rescale, finite_diff_rel_step, |
| COBYLA | rhobeg, tol, catol |
| COBYQA | maxfev, f_target, feasibility_tol, initial_tr_radius, final_tr_radius, scale |
| SLSQP | ftol, eps, finite_diff_rel_step |
| trust-constr | gtol, xtol, barrier_tol, sparse_jacobian, initial_tr_radius, initial_constr_penalty, initial_barrier_parameter, initial_barrier_tolerance, factorization_method, finite_diff_rel_step, verbose |
| differential_evolution | strategy, popsize, tol, mutation, recombination, rng, polish, init, atol, updating |
Notes:
- Options in italics override a common option with a different type or behavior.
- Options with
strikethroughindicate a common option that is not supported.
__init__
Initialize the SciPy backend.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
backend_config
|
BackendConfig
|
The backend configuration. |
required |
Raises:
| Type | Description |
|---|---|
UnsupportedError
|
If the requested method is not supported. |
ropt.backend.external.ExternalBackend
Bases: Backend
Backend implementation that runs an optimizer in a separate process.
Implements the Backend interface by spawning a
child process to run a delegate backend. The child process performs the
optimization independently and communicates back through queues to request
function evaluations, report optimizer states, and propagate errors.
Method naming
Unlike other backends, the method field of
BackendConfig must include both the plugin
and method name in one of these forms:
external/plugin-name/method-nameexternal/method-name
The external/ prefix is stripped before the remainder is forwarded to
the delegate plugin. Standard plugin-name/method-name resolution without
the prefix is not supported by this backend.
Note
The problem is sent to the child process by serializing it, so
everything the delegate needs must be serializable, including the
delegate class itself. The standard library can send anything that can
be looked up by name, which covers the built-in plugins and any plugin
class at the top level of a module or of the script you ran. Installing
the optional cloudpickle extra lifts that restriction, so plugins and
plugin instances of classes defined inside a function or a notebook can
be sent as well.
__init__
Initialize the external backend.
The method field of the configuration must be prefixed with
external/.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
backend_config
|
BackendConfig
|
The backend configuration. |
required |
ropt.backend.utils
Utility functions for use by optimizer backend plugins.
This module provides helpers for resolving the requested reporting level, checking what the optimizer writes below the Python level, unique output path construction, and splitting linear constraints into the same normalized form as the non-linear constraints delivered through the callback.
The problem itself, including its constraints, arrives as an
OptimizationProblem. Every array these
helpers accept or return is scaled, as is everything else a backend sees.
resolve_verbosity
Resolve how much the optimizer should report.
Normalizes the verbose field of
BackendConfig into a single value, so that
backends need not distinguish True from 1 themselves:
| Result | Meaning |
|---|---|
None |
Report at the optimizer's own default level. |
0 |
Do not report. |
n |
Report at level n, clamped to what the optimizer offers. |
This does not determine where the output goes: that is set by the
stdout and stderr settings of
OptimizerConfig.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
verbose
|
bool | int | None
|
The |
required |
Returns:
| Type | Description |
|---|---|
int | None
|
The reporting level, or |
collect_native_output
Collect the output a callable writes below the Python level.
Runs run with sys.stdout and sys.stderr replaced, so that everything
written through Python is diverted, and with file descriptors 1 and 2
pointing at a temporary file. Whatever reaches that file was therefore
written without going through Python.
Use this to check a backend's
bypasses_python_output
declaration from its test suite: a non-empty result means the declaration
must be True for the method that was run.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
run
|
Callable[[], None]
|
A callable that runs an optimization with the backend under test. |
required |
Returns:
| Type | Description |
|---|---|
str
|
Everything the callable wrote below the Python level. |
create_output_path
create_output_path(
base_name: str,
base_dir: Path | None = None,
name: str | None = None,
suffix: str | None = None,
) -> Path
Construct a unique output path, appending an index if necessary.
Builds a path from the provided components. If the resulting path already
exists on disk, a three-digit counter suffix (for example -001, -002) is
appended or incremented until a non-existing path is found.
The path is assembled as:
<base_dir>/<base_name>[-<name>][-<index>][<suffix>]
base_dir is created (including parents) if it does not exist.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
base_name
|
str
|
Base file or directory name. |
required |
base_dir
|
Path | None
|
Parent directory, or |
None
|
name
|
str | None
|
Optional label appended to |
None
|
suffix
|
str | None
|
Optional file extension including the leading dot |
None
|
Returns:
| Type | Description |
|---|---|
Path
|
A |
split_linear_constraints
split_linear_constraints(
coefficients: NDArray[float64],
lower_bounds: NDArray[float64],
upper_bounds: NDArray[float64],
equality: NDArray[bool_],
) -> tuple[
NDArray[np.float64],
NDArray[np.float64],
NDArray[np.bool_],
]
Split linear constraints into values compared against zero.
Produces the same form as the non-linear constraint values delivered
through the OptimizerCallback, so that a
backend that handles both together can concatenate them. Entry k evaluates
to coefficients[k] @ variables - offsets[k], which is non-negative when the
constraint is satisfied. A constraint with a finite lower and a finite upper
bound contributes two entries, and an equality one.
The arguments are those returned by
linear_constraints.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
coefficients
|
NDArray[float64]
|
The constraint coefficients. |
required |
lower_bounds
|
NDArray[float64]
|
The lower bounds. |
required |
upper_bounds
|
NDArray[float64]
|
The upper bounds. |
required |
equality
|
NDArray[bool_]
|
Which constraints have coinciding bounds. |
required |
Returns:
| Type | Description |
|---|---|
tuple[NDArray[float64], NDArray[float64], NDArray[bool_]]
|
A tuple of |