<SYSTEM_TASK:>
Solve the following problem using Python, implementing the functions described below, one line at a time
<END_TASK>
<USER_TASK:>
Description:
 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))