Configuration
Every ropt optimization run is described by a configuration dictionary. This
page covers what that dictionary becomes, the rules that apply to all of it, and
how the pieces fit together. For the field-by-field description of each section,
see Configuration Sections.
The configuration object
The dictionary is validated into an EnOptContext,
where each top-level key becomes a field holding an instance of that section's
configuration class: variables becomes a
VariablesConfig, objectives an
ObjectiveFunctionsConfig, and so on.
Only variables is required; every other section has defaults.
Three keys are different in kind. samplers, realization_filters and
function_estimators do not hold settings but components — objects built by
the plugin system, which the sections that use them refer to by key.
Most of the rules below follow from this one step. Because the dictionary is converted once, validation and coercion happen at construction rather than during the run; because the result is a set of objects rather than the dict you wrote, those objects are frozen; and because components are shared rather than copied, they are addressed by key.
Top-level layout
CONFIG = {
"variables": {...}, # required
"objectives": {...}, # optional
"linear_constraints": {...}, # optional
"nonlinear_constraints": {...}, # optional
"realizations": {...}, # optional
"optimizer": {...}, # optional
"backend": {...}, # optional
"gradient": {...}, # optional
"realization_filters": [...], # optional, tuple
"function_estimators": [...], # optional, tuple
"samplers": [...], # optional, tuple
"names": {...}, # optional, for labelled output
}
Only variables is required. Each value is either a plain dict (which Pydantic
validates against the corresponding config class) or a list/tuple of such
dicts for the plugin-bearing fields.
Rules that apply everywhere
Pydantic validation
All configuration dictionaries are validated using
Pydantic. Inputs are automatically coerced to the
expected types when possible. For example, you can pass a list wherever a
tuple is expected, or a plain list of numbers wherever a NumPy array is
required — Pydantic will handle the conversion during validation.
Some values are also adjusted during validation. For instance, the weights
fields in objectives and realizations are normalized to sum to 1:
Broadcasting
Many per-variable, per-objective, or per-constraint fields are NumPy arrays. A size-1 value is broadcast to match the relevant count, for example:
"variables": {
"variable_count": 5,
"lower_bounds": 0.0, # broadcast to [0, 0, 0, 0, 0]
"upper_bounds": [1, 2, 3, 4, 5],
}
Length-mismatched arrays raise a validation error.
Sharing optimizer components by key
Each tuple-typed field holds the optimizer components of the corresponding kind:
| Field | Component type |
|---|---|
realization_filters |
RealizationFilter |
function_estimators |
FunctionEstimator |
samplers |
Sampler |
You usually specify each component with a small config dict; see Providing optimizer components below.
Other config sections refer to these components by key. Give them as a
mapping to choose the keys yourself, or as a list, which is keyed by position —
so a list entry is reached by the integer that used to index it. For example,
VariablesConfig has a samplers field that
selects a sampler for each variable:
"samplers": [
{"method": "scipy/default"}, # index 0
{"method": "scipy/sobol"}, # index 1
],
"variables": {
"variable_count": 4,
"samplers": [0, 0, 1, 1], # variables 0,1 use sampler 0; 2,3 use sampler 1
},
The same thing written with names:
{
"samplers": {"coarse": {...}, "fine": {...}},
"variables": {
"samplers": ["coarse", "coarse", "fine", "fine"],
},
}
Use a single key to share one component across all elements; thanks to
broadcasting, a single value (the default "0") is sufficient.
For optional fields like realization_filters, None (the default) leaves the
corresponding element unfiltered. Any other key must exist, or building the
context fails with an error naming the unknown key.
Two runnable scripts select a component by position this way: examples/simple/realization_filter.py and examples/simple/function_estimator.py, each of which registers a custom component and points the objectives at it.
Providing optimizer components
Each entry ends up as an object that implements the component's base class (from the table above). You can build that object yourself, but usually you just provide a config dict and let the plugin system build it. Each tuple element accepts any of three equivalent forms, and a Pydantic validator converts it to the required object:
-
A plain
dict— the usual case. Give a small dictionary with amethodfield (and optionaloptions), androptbuilds the object for you through the plugin system:"samplers": [ {"method": "scipy/default"}, # index 0 -> SciPySampler {"method": "scipy/sobol"}, # index 1 -> SciPySampler ],The validator looks up a plugin in the
ropt.pluginssub-package by themethodfield ("plugin/method"form, or just"method"for implicit discovery) and builds the object, applying anyoptions. -
A typed config object, for example
SamplerConfig. This is the same as the dict form — the dict is validated into exactly this object — but lets you build it explicitly in Python. -
An already-constructed object — advanced. Pass an instance of a built-in class (for example a
SciPySamplerfromropt.sampler.scipy) or of your ownSamplersubclass, and it is used as-is. This is mainly for when you write the Python object yourself and want to provide it directly, without registering it as a plugin.
The same pattern applies to backend, function_estimators, and
realization_filters. You can mix these forms
freely — for example, a hand-built Sampler instance alongside a dict-configured
one in the same tuple. The dict and config-object forms resolve the method
through the plugin system, so the plugin must be registered; providing an object
directly does not.
Method strings
All method fields use the same naming convention:
"plugin/method"— explicit: use methodmethodfrom the plugin namedplugin. For example,"scipy/default"selects thedefaultmethod from thescipyplugin."method"— implicit: omit the plugin name and letroptsearch all registered plugins for one that supportsmethod. This is convenient when only one plugin provides the method, but ambiguous if multiple plugins expose the same name. The name"default"cannot be used this way, since more than one plugin may define one; write it as"plugin/default".
The plugin part corresponds to the name under which the plugin is registered
(via an entry point); the method part is any name the plugin declares in its
methods attribute. For example, the built-in SciPy backend is named scipy
and supports methods like "default", "SLSQP", and "L-BFGS-B".
Both the plugin name and the method name are case-insensitive, so
"SciPy/SLSQP", "scipy/slsqp", and "SCIPY/Slsqp" all resolve to the
same backend.
The backend field accepts one further form, "external/...", which runs the
named backend in a separate process; see Running the optimizer in a separate
process.
Immutability
The configuration objects an EnOptContext holds
are frozen, so an individual setting cannot be changed in place. The context
itself is not: replacing one of its fields wholesale is not prevented, but
nothing re-runs the work construction did — bounds, perturbation magnitudes and
linear constraints are all scaled at that point — so
the result is inconsistent. To change settings, build a new context from a
modified dict.
Warning
Treat an EnOptContext as read-only after construction. Do not try to
serialize and round-trip them (for example, to/from JSON). Some parameters
are scaled during construction in a way that cannot be undone, so
building an EnOptContext from those serialized values would scale
them again, incorrectly. NumPy arrays and plugin instances may also not
come back unchanged from a round-trip. Persist the raw input dicts instead
if you intend to
modify the values.
Naming things for output
One section describes nothing about the problem itself. names attaches labels
to the parts of a run — the variables, the objectives, the realizations, and the
rest — so that results come back readable:
"names": {
"variable": ("x", "y", "z"),
"objective": ("val", "cost"),
"realization": ("r0", "r1"),
},
Each key selects an axis, and each value is a tuple as long as that axis, so three variables take three labels. Axes left unnamed keep their integer indices, which is what a reader sees in place of a label. A key may also be the name of a metadata entry holding array values, which labels the axis that metadata spans.
Labels are not purely decorative. Some components use them to identify the items they are given instead of relying on position, which lets results be matched between runs whose ordering differs. That applies to the realization names; the others affect only how a printed table reads.
examples/simple/export.py
names variables, objectives and realizations before exporting to a frame, and
examples/simple/metadata.py
names a metadata axis. The accepted axis names are listed under
names, and
Working with Results shows how the labels appear in an
exported table.
A worked example
CONFIG = {
"variables": {
"variable_count": 5,
"lower_bounds": -5.0,
"upper_bounds": 5.0,
"perturbation_magnitudes": 1e-5,
},
"objectives": {"weights": [1.0]},
"realizations": {"weights": [1.0] * 10},
"gradient": {"number_of_perturbations": 5},
"optimizer": {"max_batches": 50},
"backend": {
"method": "scipy/default",
"options": {"maxiter": 200},
},
}
This configures a 5-variable problem with bounded variables, an ensemble of 10 equally-weighted realizations, 5 perturbations per gradient estimate, SciPy's default optimizer, and a limit of 50 batches.
Full configuration schema
Expand the block below to see every field and its default value.
Fully expanded configuration (all defaults shown)
The example below shows every top-level section of the
EnOptContext configuration with all fields
set to their default values. In practice you only need to specify the
fields you want to override — everything else is filled in automatically.
from ropt.enums import BoundaryType, PerturbationType, VariableType
CONFIG = {
"variables": {
"variable_count": ..., # required, no default
"lower_bounds": -float("inf"), # default: -inf
"upper_bounds": float("inf"), # default: +inf
"types": VariableType.REAL, # default: "real" (continuous)
"mask": True, # default: all free
"scales": 1.0, # default: no scaling
"offsets": 0.0, # default: no offset
"perturbation_magnitudes": 0.005,
"perturbation_types": PerturbationType.ABSOLUTE,
"boundary_types": BoundaryType.MIRROR_BOTH,
"samplers": "0", # default: use first sampler for all
"seed": 1,
},
"objectives": {
"weights": [1.0], # default: single objective, weight 1.0
"scales": 1.0, # default: no scaling
"offsets": 0.0, # default: no offset
"auto_scale": False, # default: do not estimate scales
"maximize": False, # default: minimize
"realization_filters": None, # default: no filter
"function_estimators": "0", # default: use first estimator for all
},
"linear_constraints": None, # No linear constraints
"nonlinear_constraints": None, # No non-linear constraints
"realizations": {
"weights": [1.0], # default: single realization, weight 1.0
"realization_min_success": None, # default: equal to number of realizations
},
"optimizer": {
"max_batches": None, # default: no limit
"max_functions": None, # default: no limit
"output_dir": None, # default: no output directory
"stdout": None, # default: not captured
"stderr": None, # default: not captured
},
"backend": {
"method": "scipy/default", # default: SciPy SLSQP
"max_iterations": None, # default: backend-specific
"convergence_tolerance": None, # default: backend-specific
"parallel": False, # default: Do not evaluate in parallel
"verbose": None, # default: silent
"options": None, # default: no extra options
},
"gradient": {
"number_of_perturbations": 5,
"perturbation_min_success": None, # default: equal to number_of_perturbations
"merge_realizations": False, # default: estimate and average gradients
"evaluation_policy": "auto", # default: evaluate functions and perturbations
}, # as needed
"samplers": [
{
"method": "scipy/default", # default: standard normal N(0,1)
"options": {},
"shared": False, # default: Each realizations has its own
# set of perturbations
},
],
"function_estimators": [
{
"method": "default/default", # default: weighted average
"options": {},
},
],
"realization_filters": [], # default: none configured
"names": {}, # default: none configured
}
Some sections above are set to None or [] because they are optional
and problem-specific. When configured, their internal structure is as
follows:
# linear_constraints (all fields required, no defaults):
"linear_constraints": {
"coefficients": ..., # required: 2D array (constraints × variables)
"lower_bounds": ..., # required: 1D array (one per constraint)
"upper_bounds": ..., # required: 1D array (one per constraint)
"scales": 1.0, # default: no scaling
"auto_scale": False, # default: do not estimate scales
}
# nonlinear_constraints (bounds are required, the rest has defaults):
"nonlinear_constraints": {
"lower_bounds": ..., # required: 1D array (one per constraint)
"upper_bounds": ..., # required: 1D array (one per constraint)
"scales": 1.0, # default: no scaling
"auto_scale": False, # default: do not estimate scales
"realization_filters": None, # default: no filter
"function_estimators": "0", # default: use first estimator
}
# realization_filters entries (method is required):
"realization_filters": [
{
"method": ..., # required: str ("plugin/method")
"options": {},
},
]