Skip to content

Troubleshooting

A ropt run keeps going through a failed realization or an infeasible result, so it can end without a result to return and without raising an error. Check exit_code before using what comes back. This page collects the behaviours that most often cause confusion.

Skim it once to know what is here, then come back with a symptom and read the table of the section it belongs to.

Results and exit codes

The returned result is the best feasible one. optimize returns the best result that satisfies every constraint to within constraint_tolerance, which defaults to 1e-10 and applies to bounds and linear constraints as well as nonlinear ones. If no evaluation satisfies them, the run ends normally with result.results set to None. The evaluations themselves are not lost: every result, feasible or not, still reaches the handlers attached to the run.

The return value is a summary, not the record of the run. It holds a single result, the best feasible evaluation. The whole history is available instead: attach a HistoryHandler or a DataFrameHandler to collect every result as it arrives. That history is also what remains when there is no best result to return.

NaN means "this realization failed", and one failure is already too many. realization_min_success defaults to all realizations, so a single NaN ends the run with TOO_FEW_REALIZATIONS. If some realizations are allowed to fail, set it lower:

config = {
    "variables": {"variable_count": 3, "perturbation_magnitudes": 1e-6},
    "realizations": {"weights": [1.0] * 10, "realization_min_success": 8},
}

examples/simple/failures.py runs the same problem twice, once with the default and once allowing the failure, and prints the exit code and result of each.

Not everything that goes wrong raises. Check exit_code before using a result. The exit code gives the reason the run ended; the result is the best feasible evaluation recorded before it did, and is None only when there was none. A run that fails part-way therefore still returns what it had reached. See When something goes wrong.

OPTIMIZER_FINISHED does not mean a limit was reached. It is the exit code for every run the backend ended by itself — because it converged, or because it reached max_iterations or convergence_tolerance. Those two live in the backend section and are enforced by the algorithm rather than counted by ropt, so a run can stop far short of max_functions, and the exit code does not distinguish which of the three applied. Only MAX_FUNCTIONS_REACHED and MAX_BATCHES_REACHED correspond to a limit ropt enforced itself.

A broken machine is an error, not a failed realization. When the machinery itself fails — a worker process is killed, a cluster job never writes its result — the run stops with an ExecutionError giving the number of evaluations lost and the reason. It is not absorbed as a NaN, because a result computed over the workers that survived is indistinguishable from one computed over the whole ensemble.

What you see Most likely cause
result.results is None, but exit_code is OPTIMIZER_FINISHED No evaluation satisfied the constraints to within constraint_tolerance (default 1e-10; bounds and linear constraints count too), so there is no best feasible result to return. The evaluations are still in the handlers. Raise the tolerance, or check that the constraints can be satisfied at all.
TOO_FEW_REALIZATIONS although only one realization failed realization_min_success defaults to all of them. Lower it.
The run stopped long before max_functions, with OPTIMIZER_FINISHED The backend ended it: max_iterations, convergence_tolerance, or its own convergence test. Those are set in the backend section; see Limiting the length of a run.
ExecutionError part-way through a run The machinery failed, not your objective. The message names the reason and how many evaluations went with it.
Numbers do not match what you configured Results carry the configured values and the optimizer's scaled ones side by side; see Scaling of results.

Your evaluation function

Outside your process, your objective works on a copy. On a process_pool, a local_pool or an hpc_pool, your evaluation function is sent to a worker together with the data it uses. Anything it writes there — a global, a cache, a list it appends to — is thrown away when the worker finishes. Return what you need instead; see Handlers and the process boundary.

Several runs may call your objective at the same time. optimize_many always runs its optimizations concurrently. Given a pool, their evaluations go there; given none, each run evaluates on its own driver thread, so your evaluation function is called from several threads at once and has to tolerate that. See Many optimizations at once.

Metadata is copied with copy.deepcopy. Every result gets its own copy of the metadata you passed, so keep it to plain data — numbers, strings, lists, arrays. A lock, an open file, or a database connection cannot be copied and raises a TypeError in the middle of the run.

What you see Most likely cause
A global or a cache your objective writes to is never updated The objective ran in a worker process, on a copy. Return the value as metadata instead.
An objective that works alone misbehaves under optimize_many It is being called from several threads at once. Remove the shared mutable state, or guard it with a lock of its own.
TypeError: cannot pickle ... part-way through a run Something in metadata cannot be deep-copied.

Pools, stopping and processes

A run uses the pool you hand it, and no other. Nothing is picked up from the surrounding code. A run given no pool= evaluates in-process, on the thread that called it — even if a session is open next to it.

Threads cannot be interrupted. Evaluations on a thread_pool run to completion even after Ctrl-C, because Python cannot interrupt a thread from outside. If an evaluation may run long and has to be interruptible, put it on a local_pool or an hpc_pool; see Stopping a run.

A process_pool re-imports your script. Its workers are started with spawn, so each of them imports the file the run was started from, and everything at module level runs again in every worker. That is why the entry point has to sit behind if __name__ == "__main__":. Without the guard the workers try to start workers of their own, and the pool never opens.

A pool does not outlive its session. Closing the session, or the pool itself, releases its workers, and a run started on it afterwards is refused before anything runs. Open the pool inside the block that uses it; see Releasing a pool early.

What you see Most likely cause
Ctrl-C appears to do nothing A process-wide signal setting, changed by an imported package. Call restore_keyboard_interrupt; see Keyboard Interrupts.
The program will not exit after Ctrl-C Evaluations on a thread_pool are still running and cannot be interrupted.
Pages of multiprocessing tracebacks, ending in ExecutionError: Could not start worker processes The script has no if __name__ == "__main__": guard, so every worker re-ran it from the top.
A WorkflowError about a closed pool The pool outlived the with session() block that created it, or was closed explicitly.
More workers made everything slower numpy and similar libraries already use every core. Set OMP_NUM_THREADS=1 and let the pool provide the parallelism; see Which pool should I use?.
Simulators keep running after the run stopped A process_pool kills only its own workers. Use a local_pool, which signals the whole process group.
A cluster directive has no effect The submission script never mentions that variable, or the value was clamped to the queue's limit; see Running on an HPC cluster.

Running many at once

optimize_many really does run everything at once. A handler must be in a shared group, and a single report= callback given to all the runs is called by several threads at the same time. Keep such a callback free of shared state, or give each run its own.

A handler cannot move from local to shared. Passing a handler to a run binds it to that run's compute step, and shared_handlers refuses it from then on. Decide per handler which role it plays; if you need both, use two handlers.

A slow shared handler holds up every run. A group processes events one at a time, and the run that emitted one waits until every handler has finished with it. Keep shared handlers cheap, or register a slow one with threaded.

Not every optimizer can run beside another. A backend that needs its own working directory, writes to a fixed file name, or keeps state inside its library cannot run concurrently in one process; select it as external/... to give it a process of its own. Optimizer output capture is likewise for one run at a time.

What you see Most likely cause
A handler collects nothing from optimize_many A plain handler belongs to one run. Put it in a shared group.
shared_handlers refuses a handler that worked before It was used as a local handler once, which binds it permanently. Use a second handler.
Results from a shared report= callback are jumbled or lost The callback is called from every run's thread at once. Use a shared group, or one callback per run.
Many runs are slower than expected while the pool sits idle A shared handler is serializing them. Make it cheaper, or run it threaded.
A second concurrent run raises WorkflowError about output capture Only one run at a time may set stdout or stderr; see Many optimizations at once.

Configuration

Runs are reproducible by default. The seed is fixed at 1 unless you set it, so repeating a run reproduces its perturbations exactly. Change the seed when you deliberately want an independent repetition of the same problem.

Label tuples are checked at export, not at the start. A tuple in names that is not as long as the axis it labels lets the run finish normally; the mismatch surfaces later, as an error from pandas or polars when a result is turned into a table.

What you see Most likely cause
RuntimeError: Auto-scaling of the objectives failed to estimate a scale factor The first batch averaged to zero, or to a value that is not finite, so no scale could be derived. Set the scales yourself, or start somewhere else.
Two runs that should agree give different gradients Quasi-random samplers (sobol, halton, lhs) are seeded in the order the samplers are defined, so reordering that section changes the perturbations.

See also

  • The same failure model at the low level, in more detail: Error handling.