docs: def minimize(func, x0, data=None, method=None, lower_bounds=None, upper_bounds=None, constraints_func=None,
               nmr_observations=None, cl_runtime_info=None, options=None):
      """Minimization of one or more variables.
  
      For an easy wrapper of function maximization, see :func:`maximize`.
  
      All boundary conditions are enforced using the penalty method. That is, we optimize the objective function:
  
      .. math::
  
          F(x) = f(x) \mu \sum \max(0, g_i(x))^2
  
      where :math:`F(x)` is the new objective function, :math:`f(x)` is the old objective function, :math:`g_i` are
      the boundary functions defined as :math:`g_i(x) \leq 0` and :math:`\mu` is the penalty weight.
  
      The penalty weight is by default :math:`\mu = 1e20` and can be set
      using the ``options`` dictionary as ``penalty_weight``.
  
      Args:
          func (mot.lib.cl_function.CLFunction): A CL function with the signature:
  
              .. code-block:: c
  
                  double <func_name>(local const mot_float_type* const x,
                                     void* data,
                                     local mot_float_type* objective_list);
  
              The objective list needs to be filled when the provided pointer is not null. It should contain
              the function values for each observation. This list is used by non-linear least-squares routines,
              and will be squared by the least-square optimizer. This is only used by the ``Levenberg-Marquardt`` routine.
  
          x0 (ndarray): Initial guess. Array of real elements of size (n, p), for 'n' problems and 'p'
              independent variables.
          data (mot.lib.kernel_data.KernelData): the kernel data we will load. This is returned to the likelihood function
              as the ``void* data`` pointer.
          method (str): Type of solver.  Should be one of:
              - 'Levenberg-Marquardt'
              - 'Nelder-Mead'
              - 'Powell'
              - 'Subplex'
  
              If not given, defaults to 'Powell'.
          lower_bounds (tuple): per parameter a lower bound, if given, the optimizer ensures ``a <= x`` with
              a the lower bound and x the parameter. If not given, -infinity is assumed for all parameters.
              Each tuple element can either be a scalar or a vector. If a vector is given the first dimension length
              should match that of the parameters.
          upper_bounds (tuple): per parameter an upper bound, if given, the optimizer ensures ``x >= b`` with
              b the upper bound and x the parameter. If not given, +infinity is assumed for all parameters.
              Each tuple element can either be a scalar or a vector. If a vector is given the first dimension length
              should match that of the parameters.
          constraints_func (mot.optimize.base.ConstraintFunction): function to compute (inequality) constraints.
              Should hold a CL function with the signature:
  
              .. code-block:: c
  
                  void <func_name>(local const mot_float_type* const x,
                                   void* data,
                                   local mot_float_type* constraints);
  
              Where ``constraints_values`` is filled as:
  
              .. code-block:: c
  
                  constraints[i] = g_i(x)
  
              That is, for each constraint function :math:`g_i`, formulated as :math:`g_i(x) <= 0`, we should return
              the function value of :math:`g_i`.
  
          nmr_observations (int): the number of observations returned by the optimization function.
              This is only needed for the ``Levenberg-Marquardt`` method.
          cl_runtime_info (mot.configuration.CLRuntimeInfo): the CL runtime information
          options (dict): A dictionary of solver options. All methods accept the following generic options:
              - patience (int): Maximum number of iterations to perform.
              - penalty_weight (float): the weight of the penalty term for the boundary conditions
  
      Returns:
          mot.optimize.base.OptimizeResults:
              The optimization result represented as a ``OptimizeResult`` object.
              Important attributes are: ``x`` the solution array.
      """
      if not method:
          method = 'Powell'
  
      cl_runtime_info = cl_runtime_info or CLRuntimeInfo()
  
      if len(x0.shape) < 2:
          x0 = x0[..., None]
  
      lower_bounds = _bounds_to_array(lower_bounds or np.ones(x0.shape[1]) * -np.inf)
      upper_bounds = _bounds_to_array(upper_bounds or np.ones(x0.shape[1]) * np.inf)
  
      if method == 'Powell':
          return _minimize_powell(func, x0, cl_runtime_info, lower_bounds, upper_bounds,
                                  constraints_func=constraints_func, data=data, options=options)
      elif method == 'Nelder-Mead':
          return _minimize_nmsimplex(func, x0, cl_runtime_info, lower_bounds, upper_bounds,
                                     constraints_func=constraints_func, data=data, options=options)
      elif method == 'Levenberg-Marquardt':
          return _minimize_levenberg_marquardt(func, x0, nmr_observations, cl_runtime_info, lower_bounds, upper_bounds,
                                               constraints_func=constraints_func, data=data, options=options)
      elif method == 'Subplex':
          return _minimize_subplex(func, x0, cl_runtime_info, lower_bounds, upper_bounds,
                                   constraints_func=constraints_func, data=data, options=options)
      raise ValueError('Could not find the specified method "{}".'.format(method))