Exit Conditions
***************

Exit conditions determine when the optimization will end. Once
triggered, all optimizers will be stopped and the "GloMPOManager" will
return its result. These are different from Stoppers, which only stop
one optimizer that is then usually replaced by another.


Abstract Base ExitCondition
===========================

class BaseExitCondition

   Abstract class from which all exit conditions must inherit to be
   compatible with GloMPO.

   Attributes:
   logger : logging.Logger
      "logging.Logger" instance into which status messages may be
      added.

   abstract __call__(manager)

      Evaluates if the exit condition is met.

      Parameters:
      manager
         "GloMPOManager" instance which is managing the optimization.
         Its attributes can be accessed when determining the exit
         criteria.

      Returns:
      bool
         "True" if the exit criteria is met, "False" otherwise.

      Notes:
      For proper functionality, the result of this method must be
      saved to "last_result" before returning.

   __iter__()

      Provides a (flattened) iteration through all the bases which
      comprise the combined stopper/exitcondition

   __str__()

      Produces a string of the stopper/exit condition's name and
      configuration.

   property last_result

      The result of the last "__call__()".

   reset()

      Clears previous evaluation result to avoid misleading printing.
      Resets "last_result" to "None". Given that stoppers and
      exitconditions are evaluated lazily, it is possible for
      misleading results to be returned by "str_with_result()"
      indicating a stopper/exit condition has been evaluated when it
      has not. Bases are thus reset before "__call__()" to prevent
      this.

   str_with_result()

      String representation of the object with its result.


Combining Base ExitConditions
=============================

Instances of "BaseExitCondition" can be combined with "&" and "|"
boolean operations. This allows individual exit conditions to be very
simple, but be combined into more sophisticated conditions. Combining
exit conditions in this way produces a new "BaseExitCondition" object
which itself can be further combined. This allows conditions to be as
deeply nested as one desires. For example:

>>> a = ExitConditionA()
>>> b = ExitConditionB()
>>> c = ExitConditionC()
>>> d = ExitConditionD()
>>> combi = a & b | c & d

The order of evaluation is "&" before "|", thus the above would be
equivalent to:

>>> combi = ((a & b) | (c & d))

Important:

  A combined "BaseExitCondition" is evaluated lazily. This means:

  1. "a & b" will not evaluate "b" if "a" is "False"

  2. "a | b" will not evaluate "b" if "a" is "True"

Lazy evaluation ensures a faster return, and explains the presence of
"None" when an evaluation statement is printed. For example:

>>> combi(...)
True
>>> from scm.glompo.common.helpers import nested_string_formatting
>>> nested_string_formatting(combi.str_with_result())
'[
  ExitConditionA() = True &
  ExitConditionB() = True
 ] |
 [
  ExitConditionC() = None &
  ExitConditionD() = None
 ]'

Make sure to structure your nesting in the correct order. For example,
if you want to make sure a certain exit condition is always evaluated,
place it first. If an exit condition is slow to evaluate, place it
last.

All of the above holds true for Stoppers too as they share a common
hidden base.


Included ExitConditions
=======================

For convenience, GloMPO comes bundled with several simple exit
conditions already included.

class MaxOptimizersConverged(nconv)

   Bases: "BaseExitCondition"

   Returns "True" when "nconv" optimizers have converged normally.
   'Normally' here is defined as exiting the minimization loop
   according to the optimizer's own internal convergence criteria,
   rather than any GloMPO intervention.

class MaxOptimizersStarted(omax)

   Bases: "BaseExitCondition"

   Returns "True" after "omax" optimizers have been started.

class MaxOptimizersStopped(max_stops)

   Bases: "BaseExitCondition"

   Returns "True" after "max_stops" optimizers have been shutdown by
   the manager.

class MaxTotalFunctionCalls(fmax)

   Bases: "BaseExitCondition"

   Returns "True" after "fmax" function evaluations have been executed
   across all managed optimizers.

class StopsAfterConvergence(n_stopped=0, n_converged=1)

   Bases: "BaseExitCondition"

   Evaluation based on the number of single optimizers converged and
   the number of optimizers stopped thereafter. Returns "True" after
   "n_stopped" optimizers have been stopped by GloMPO after
   "n_converged" optimizers have reached normal convergence.

class TargetFunctionValue(target, atol=1e-06)

   Bases: "BaseExitCondition"

   Returns "f_best <= target + atol", where "f_best" is the best value
   seen thus far by the manager.

class TimeLimit(*, session_max=None, overall_max=None)

   Bases: "BaseExitCondition"

   Time based exit criteria. Differentiates between session time and
   overall optimisation time, the difference is only relevant in the
   case where checkpointing is being used.

   Parameters:
   session_max
      Maximum time in seconds which may elapse from the time
      "GloMPOManager.start_manager()" is called.

   overall_max
      Maximum time in seconds which may elapse in total over all
      optimisation sessions. In other words the total time used
      previously is loaded from the checkpoint. In the case where
      checkpoint is not used, both quantities are equivalent.

   Notes:
   For the avoidance of doubt, both times cannot be used
   simultaneously. If required rather initialise two instances with
   one condition each.
