Adaptive Rate Monte Carlo Optimizer
***********************************

class ARMCOptimizer(_opt_id=None, _signal_pipe=None, _results_queue=None, _pause_flag=None, _is_log_detailed=False, _workers=1, _backend='threads', phi=1.0, gamma=2.0, a_target=0.25, super_iter_len=1000, sub_iter_len=100, move_range=array([0.9, 0.905, 0.91, 0.915, 0.92, 0.925, 0.93, 0.935, 0.94, 0.945, 0.95, 0.955, 0.96, 0.965, 0.97, 0.975, 0.98, 0.985, 0.99, 0.995, 1.005, 1.01, 1.015, 1.02, 1.025, 1.03, 1.035, 1.04, 1.045, 1.05, 1.055, 1.06, 1.065, 1.07, 1.075, 1.08, 1.085, 1.09, 1.095, 1.1  ]), move_func=<ufunc 'multiply'>)

   Bases: "BaseOptimizer"

   An optimizer using the Adaptive Rate Monte Carlo (ARMC) algorithm.

   1. A trial state, S_{\omega}, is generated by moving a random
      parameter retrieved from a user-specified parameter set (*e.g.*
      atomic charge). By default, parameters moves are applied in a
      multiplicative manner.

   2. The move is accepted if the new set of parameters, S_{\omega},
      lowers the auxiliary error (\Delta \varepsilon_{QM-MM}) with
      respect to the previous set of accepted parameters S_{\omega-i}
      (i > 0; see (1)). The auxiliary error is calculated with a user-
      specified cost function.

      p(\omega \leftarrow \omega-i) = \Biggl \lbrace {     1, \quad
      \Delta \varepsilon_{QM-MM} ( S_{\omega} )         \; \lt \;
      \Delta \varepsilon_{QM-MM} ( S_{\omega-i} )     \atop     0,
      \quad         \Delta \varepsilon_{QM-MM} ( S_{\omega} )
      \; \gt \;         \Delta \varepsilon_{QM-MM} ( S_{\omega-i} ) }

   3. The parameter history is updated. Either S_{\omega} or
      S_{\omega-i} is increased by the variable \phi (see (2)) if,
      respectively, the new parameters are accepted or rejected. In
      this manner the underlying PES is continuously modified,
      preventing the optimizer from getting permanently stuck in a
      (local) parameter space minima.

      \Delta \varepsilon_{QM-MM} ( S_{\omega} ) + \phi \quad \text{if}
      \quad \Delta \varepsilon_{QM-MM} ( S_{\omega} ) \; \lt \; \Delta
      \varepsilon_{QM-MM} ( S_{\omega-i} ) \atop \Delta \varepsilon_
      {QM-MM} ( S_{\omega-i} ) + \phi \quad \text{if} \quad \Delta
      \varepsilon_{QM-MM} ( S_{\omega} ) \; \gt \; \Delta \varepsilon_
      {QM-MM} ( S_{\omega-i} )

   4. The parameter \phi is updated at regular intervals in order to
      maintain a constant acceptance rate \alpha_{t}. This is
      illustrated in (3), where \phi is updated the beginning of every
      super-iteration \kappa. In this example the total number of
      iterations, \kappa \omega, is divided into \kappa super- and
      \omega sub-iterations.

      \phi_{\kappa \omega} = \phi_{ ( \kappa - 1 ) \omega} * \gamma^{
      \text{sgn} ( \alpha_{t} - \overline{\alpha}_{ ( \kappa - 1 ) })
      } \quad \kappa = 1, 2, 3, ..., N

   Parameters:
   _opt_id, _signal_pipe, _results_queue, _pause_flag,
   _is_log_detailed, _workers, _backend
      See "BaseOptimizer".

   phi
      The variable \phi.

   gamma
      The constant \gamma.

   a_target
      The target acceptance rate \alpha_{t}.

   super_iter_len
      The total number of each super-iterations \kappa. Total number
      of iterations: \kappa \omega.

   sub_iter_len
      The length of each ARMC sub-iteration \omega. Total number of
      iterations: \kappa \omega.

   move_range
      An array-like object containing all allowed move sizes.

   move_func
      A callable for performing the moves. The callable should take 2
      floats (*i.e.* a single value from the parameter set
      *S_{omega-i}* and the move size) and return 1 float (*i.e.* an
      updated value for the parameter set *S_{omega}*).

   Attributes:
   phi : float
      The variable \phi.

   gamma : float
      The constant \gamma.

   a_target : float
      The target acceptance rate \alpha_{t}.

   super_iter : range
      A range object with the total number of each super-iterations
      \kappa. Total number of iterations: \kappa \omega.

   sub_iter : range
      A range object with the length of each ARMC sub-iteration
      \omega. Total number of iterations: \kappa \omega.

   move_range : numpy.ndarray[float]
      An array-like object containing all allowed move sizes.

   move_func : Callable[[float, float], float]
      A callable for performing the moves. The callable should take 2
      floats (*i.e.* a single value from the parameter set
      *S_{omega-i}* and the move size) and return 1 float (*i.e.* an
      updated value for the parameter set *S_{omega}*).

   run : Callable
      See the "function" parameter in "ARMCOptimizer.minimize()".

   bounds : numpy.ndarray or (None, None)
      An 2D array (or 2-tuple filled with "None") denoting minimum and
      maximum values for each to-be moved parameter. See the "bounds"
      parameter in "ARMCOptimizer.minimize()".

   x_best : numpy.ndarray[float]
      The parameter set which minimizes the user-specified error
      function.

   fx_best : float
      The error associated with "x_best".

   fx_old : float
      The error of the last set of accepted parameters.

   See Also:

   The paper describing the original ARMC implementation: Salvatore
   Cosseddu *et al.*, *J. Chem. Theory Comput.*, **2017**, *13*,
   297–308 10.1021/acs.jctc.6b01089.

   The Python-based implementation of ARMC this class is based on:
   Automated Forcefield Optimization Extension github.com/nlesc-nano
   /auto-FOX.

   property bounds

      Get or set the "bounds" parameter of "ARMCOptimizer.minimize()".

   checkpoint_save(path, force=None, block=None)

      Save current state, suitable for restarting.

      Parameters:
      path
         Path to file into which the object will be dumped. Typically
         supplied by the manager.

      force
         Set of variable names which will be forced into the dumped
         file. Convenient shortcut for overwriting if fails for a
         particular optimizer because a certain variable is filtered
         out of the data dump.

      block
         Set of variable names which are typically caught in the
         construction of the checkpoint but should be excluded. Useful
         for excluding some properties.

      Notes:
      1. Only the absolutely critical aspects of the state of the
         optimizer need to be saved. The manager will resupply
         multiprocessing parameters when the optimizer is
         reconstructed.

      2. This method will almost never be called directly by the user.
         Rather it will be called (via signals) by the manager.

      3. This is a basic implementation which should suit most
         optimizers; may need to be overwritten. Typically it is
         sufficient to call the super method and use the "force" and
         "block" parameters to get a working implementation.

   minimize(function, x0, bounds=None)

      Start the minimization process.

      Parameters:

      function : Callable
         The function to be minimized.

      x0 : array-like[float]
         A 1D array-like object representing the set of to-be
         optimized parameters S.

      bounds : array-like[float], optional
         An (optional) 2D array-like object denoting minimum and
         maximum values for each to-be moved parameter. The sequence
         should be of the same length as "x0".

   move(x0, x0_min=None, x0_max=None)

      Create a copy of "x0" and apply a random move to it.

      The move will be applied with "move_func" (multiplication by
      default) using a random value from "move_range".

      Parameters:

      x0 : numpy.ndarray[float]
         A 1D array of parameters S_{\omega-i}.

      value_min : numpy.ndarray[float], optional
         A 1D array minimum values for each to-be moved parameter. The
         array should be of the same length as "x0".

      value_max : numpy.ndarray[float], optional
         A 1D array maximum values for each to-be moved parameter. The
         array should be of the same length as "x0".

      Returns:

      numpy.ndarray[float]
         A copy of "x0" S_{\omega}. A single value is moved within
         this parameter set.

   phi_apply(aux_err)

      Apply "phi" to the supplied auxiliary error: \Delta \varepsilon_
      {QM-MM} + \phi.

   phi_update()

      Update the variable \phi ("phi").

      \phi is updated based on the target acceptance rate, \alpha_{t}
      ("a_target"), and the acceptance rate, "acceptance", of the
      current super-iteration:

         \phi_{\kappa \omega} = \phi_{ ( \kappa - 1 ) \omega} *
         \gamma^{     \text{sgn} ( \alpha_{t} - \overline{\alpha}_{ (
         \kappa - 1 ) }) }

      Parameters:

      acceptance : numpy.ndarray[bool]
         A boolean array for keeping track of accepted moves over the
         course of the current sub-iteration of \kappa.
