scijit.optimize.differential_evolution

scijit.optimize.differential_evolution(func, bounds, args=(), strategy='best1bin', maxiter=1000, popsize=15, tol=0.01, mutation=(0.5, 1.0), recombination=0.7, rng=None, callback=None, disp=False, polish=True, init='latinhypercube', atol=0.0, updating='immediate', workers=1, constraints=(), x0=None, integrality=None, vectorized=False, seed=None)

Differential evolution, a population-based stochastic optimizer.

Callback style Afunc is a plain @njit func(x, *args) -> float64.

Parameters:
funccallable

A plain @njit f(x, *args) -> float.

bounds(n, 2) array_like

One (min, max) pair per variable. Must be finite.

argstuple, optional

Extra parameters, unpacked into the argument list of func after x, so args=(a, b) calls func(x, a, b). Its elements may be of different types and shapes. Default (), which calls func(x); None means the same. An ndarray or a list arrives as ONE argument instead, func(x, args). See Notes.

strategystr, optional

Name of the mutation strategy. 'best1bin' is the default. Full list in the module docstring. A callable strategy is not accepted.

maxiterint or None, optional

Maximum number of generations. Default 1000. None resolves to 1000.

popsizeint, optional

Population-size multiplier. Default 15. Must be >= 1.

tolfloat, optional

Relative convergence tolerance on the population energies. Default 0.01. Must be >= 0.

mutationfloat or 2-tuple, optional

A constant mutation factor, or a (lo, hi) dithering range. Default (0.5, 1). Every entry must lie in [0, 2).

recombinationfloat, optional

Crossover probability. Default 0.7. Must lie in [0, 1].

rngint or numpy.random.Generator, optional

Source of every draw the population and the generation loop make. None (default) uses the internal xorshift64* generator; an integer builds a numpy.random.default_rng for the call; a Generator is drawn from directly and its state advances. See Notes.

callbackcallable, optional

Called once per generation, after the convergence quantity is known and before the test that would end the run. A plain Python callback(xk) or callback(intermediate_result), or an @njit callback(xk). Returning a truthy value or raising StopIteration halts the run; the result then reports success=False and message='callback function requested stop early', and the polish still runs.

dispbool, optional

Print the per-iteration line, and the line before the polish. Default False.

polishbool, optional

Polish the best member with L-BFGS-B over the same box. True by default. The polished point replaces the best member only when it improves the objective, the polish converged, and it lies inside the bounds. A callable polish is not accepted; see Notes.

initstr, optional

'latinhypercube' (default), 'random', 'sobol' or 'halton'. An ndarray of starting points is not accepted.

atolfloat, optional

Absolute convergence tolerance on the population energies. Default 0.0. Must be >= 0.

updating, workers, constraints, x0, integrality, vectorizedoptional

Accepted only at their defaults. See Notes.

seedint, optional

BEYOND SCIPY. Seeds the internal xorshift64* generator, which is the rng at None path. None (default) draws fresh entropy per call, so two identical calls give different answers. See Notes.

Returns:
resDEResult

A namedtuple carrying x, fun, nit, nfev, population, population_energies, success, message and jac. nfev is a count of the evaluations of func this routine makes, the polish’s included. jac is the L-BFGS-B gradient at the polished point, and zeros where no polish was accepted.

Raises:
ValueError

Empty bounds, a non-finite bound, bounds that is not (n, 2), popsize < 1, maxiter < 0, a negative tol or atol, a recombination outside [0, 1], a mutation entry outside [0, 2), an unknown strategy or init, a rng that is not None, an integer or a numpy.random.Generator, or any of callback, x0, integrality, constraints, vectorized, workers and updating away from its default.

See also

scipy.optimize.differential_evolution

The scipy routine this mirrors.

scijit.optimize.brute

Exhaustive grid, deterministic.

scijit.optimize.basinhopping

Random restarts around a local minimizer.

Notes

UNAVAILABLE: an OptimizeResult. res is a namedtuple, so its field set is fixed. jac is always present where scipy adds the key only after a polish it accepted. constr, constr_violation, maxcv and constr_penalty, which scipy carries under constraints, are absent.

NOT IMPLEMENTED: a callable strategy, a callable polish, and an ndarray in init.

init='sobol' and init='halton' draw the initial population from the same quasi-random sequences scipy uses: unscrambled, the points from scijit.stats.qmc and scipy.stats.qmc agree to the bit. scipy scrambles them with a numpy.random.Generator, which compiled code cannot hold, so the scramble seed comes off this solver’s own stream and the population, the search path and the evaluation count are not scipy’s at the same seed.

args is unpacked into the objective’s argument list, func(x, *args).

DELIBERATELY DIFFERENT: an ndarray, a list or a bare scalar args reaches func as ONE argument, func(x, args). scipy unpacks the first two element by element and refuses the third. The arity of a compiled call is fixed when it compiles, so a sequence whose length is known only at run time cannot be unpacked: func(x, *args) on an ndarray inside @njit is a TypingError. An objective written to the unpacked shape refuses the ndarray rather than reading it wrongly.

NOT IMPLEMENTED: x0, integrality, constraints, vectorized, workers and updating='deferred'. Each raises ValueError away from its default rather than being ignored.

DELIBERATELY DIFFERENT: scipy’s legacy convergence value passed to callback(xk, convergence=val) is not carried here. Its value is tol / (std(energies) / |mean(energies)|), which this entry point does not compute.

DELIBERATELY DIFFERENT: an intermediate_result here carries x and fun. scipy’s also carries convergence, message, nfev, nit, population, population_energies and success.

An @njit callback halts by raising; its return value is not read. A plain Python callback also halts on a truthy return.

With rng at None the draws come from an internal xorshift64* generator carried in a per-call state array. seed selects its stream and seed=None draws a fresh one per call.

DELIBERATELY DIFFERENT: an integer rng builds a numpy.random.default_rng, which is PCG64, where scipy’s check_random_state builds the legacy MT19937 RandomState that compiled code cannot hold. Under any rng the population initialisation, the dither and the crossover consume draws in scipy’s order and shapes, and the trial index is taken from one uniform() where scipy calls choice, which numba does not implement. So the same integer explores a different population.

A rng at None is safe to call from a numba.prange loop, because the generator state is per-call. One Generator SHARED across a prange is not: 70,499 of 100,000 draws were distinct across 32 threads. An integer rng builds its generator inside the call, so it is per-call as well.

https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.differential_evolution.html

Examples

>>> import numpy as np
>>> from numba import njit
>>> from scijit.optimize import differential_evolution
>>> @njit
... def q(x, target):
...     return (x[0] - target[0]) ** 2 + (x[1] - target[1]) ** 2
>>> bounds = np.array([[-3.0, 3.0], [-3.0, 3.0]])
>>> @njit
... def run():
...     return differential_evolution(q, bounds,
...                                   (np.array([1.5, -0.5]),),
...                                   'best1bin', 200)
>>> res = run()
>>> np.round(res.x, 4)
array([ 1.5, -0.5])
>>> res.success
True