On Sat, Jan 14, 2017 at 11:20 PM, Andrew Nelson <andyfaff@gmail.com> wrote:
On 15 January 2017 at 05:03, Denis Laxalde <denis@laxalde.org> wrote:
Evgeni Burovski a écrit :
Small details, feel free to take or leave:
1. What is `step` in `for step in solver`? If it's a current "state of the minimizer", then can its contents be retrieved from the solver object itself (step vs `solver.result` above)? Or is it just a step number and the solver holds all the information at each step via `solver.result?` This needs to be specified and documented.
This `step` is an `OptimizeStep`. It's a new class introduced here to wrap any useful information produced by the solver during one iteration. In this case, it holds the current `x` and function value along with iteration number. The idea is to return an extensible object instead of, say, a tuple that may cause bw compat issues in the future. I think this is documented, feel free to comment further if things are not clear enough.
Best `x` and best energy are already available as `solver.x` and `solver.population_energy[0]`, but I think this kind of object being returned from `__next__` is a good idea. Does it have to be an `OptimizeStep`, or can we return an preliminary `OptimizeResult`?
2. Since the solver class is iterable, just iterating it without the context manager should be equivalent. Something like this (pseudocode)
solver = DifferentialEvolutionSolver(...) while not_converged: result = next(solver)
Also, the context manager interface is actually orthogonal to the iteration API. The context manager abstracts out the solver *initialization* (population initialization and energies calculation) and *termination* (possible polishing and build of the `OptimizeResult`).
Whilst the context manager is orthogonal to the iteration API, Evgeni's example would not be possible with the PR is it currently exists - the only way of initialising the solver (population initialization and energies calculation) would be to go through the context manager. Denis suggested that a way around this would be to have a separate initialize_population() method. It's a good idea to have this method (for reasons I outline later). However, if you create the object outside a context manager I don't think that one should have to call an additional initialize_population() method to be able to iterate the object. There are a few reasons for this: (a) it's an extra line to type (b) iterating the object without going through the context manager is not equivalent (c) the solver object possesses public population and population_energies attributes, these attributes need to be present in a newly created object, not be created at a later stage (d) if the population and population_energies attributes are present I'd argue it makes more sense for them to contain relevant values (e) given a solver object how does one know if initialize_population has already been called (which would be a common occurrence when used in an interactive interpreter)? (f) you can't call next(solver) if the population hasn't been initialised. You shouldn't need to know the state of the solver object to be able to iterate it, it should be iterable straightaway.
Currently the initial energy calculations are done in `__init__` and one uses the object as:
# stepwise solver = DifferentialEvolutionSolver(...) while not solver.converged: next(solver)
-or-
# all at once solver = DifferentialEvolutionSolver(...) result = solver.solve()
The proposed PR:
with solver as DifferentialEvolutionSolver(...): while not solver.converged(): next(solver) result = solver.result
-or possibly-
solver = DifferentialEvolutionSolver(...) # extra line to type, solver.initialize_population() while not solver.converged: next(solver)
Perhaps the following course could be followed:
(1) make the __next__ method return an OptimizeStep object (or OptimizeResult). (2) add a polish() method that takes the current best solution and polishes it. Having a separate polish method is useful as you can polish the object at any step, getting functionality similar to optimize.brute. (3) Add __enter__() and __exit__() methods to allow the object to be used as a context manager. The __exit__() method can use the public polish() method. I'd like to keep the existing solve() method, it can call __exit__() at the end. (4) population initialization and the initial energy calculation is moved to a public method, initialize_population(). This method is either called from __init__ (the status quo), or it is called on the first call to __next__(), making the first call to __next__ take twice as long. My preference is to keep the status quo because one can create the solver and immediately polish the best population member (c.f. optimize.brute), as well as the points raised above. The rationale for making it public is that once you've done one optimization you can re-initialise it with a different method ('latinhypercube' or 'random') and solve again or you could imagine re-initialising the population with a given population array (which has been requested previously) and start iterating again.
It seems to me we are all saying pretty much the same thing: 1. If an instance of DifferentialEvolutionSolver is used as a context manager, __init__ is called before __enter__ anyway, so all initialization can happen in __init__, like it does in master: In [33]: class C(object): def __init__(self): print('init') def __enter__(self): print('enter') def __exit__(self, *args, **kwds): print('exit', args, kwds) ....: In [34]: with C() as c: ....: pass ....: init enter ('exit', (None, None, None), {}) This way it seems there's no need for either having a special `init_population` method or doing something special at first call to __next__ 2. Since __init__ is always called, various initialization strategies can be handled as a keyword arg in __init__(..., init_method="latinhypercube"), possibly overloaded for a callable or a population from a previous run? Or maybe the latter (reusing a population) is better handled by an alternative constructor. But this is certainly separate from both the context manager or iteration. 3. I agree with Denis that `make_a_step_then_stop` is spelled __next__ in python :-). 4. `.solve` method is indeed the standalone `differential_evolution` function. 5. It seems natural to use __exit__ for polishing, if desired. 6. OptimizeStep could be a (current version of) OptimizeResult. In fact, I'd suggest it *is* an OptimizeResult. Am I missing something? Evgeni