
"""
*smooth*, as demonstrated in the examples section,
is used to call the fitting routine. There are a number
of :math:`{^{**}}` kwargs that can be assigned to the function which change how
the fit is performed, the model that is fit and various other attributes.
These are detailed below.

"""

from maxsmooth.qp import qp_class
from maxsmooth.Models import Models_class
from maxsmooth.derivatives import derivative_class
from maxsmooth.Data_save import save, save_optimum
from itertools import product
import warnings
import numpy as np
import time
import os
import shutil


class smooth(object):

    r"""

    **Parameters:**

        x: **numpy.array**
            | The x data points for the set being fitted.

        y: **numpy.array**
            | The y data points for fitting.

        N: **int**
            | The number of terms in the DCF.

    **Kwargs:**

        fit_type: **Default = 'qp-sign_flipping'**
            | This kwarg allows the user to
                switch between sampling the available discrete sign spaces
                (default) or testing all sign combinations on the derivatives
                which can be accessed by setting to 'qp'.

        model_type: **Default = 'difference_polynomial'**
            | Allows the user to
                access default Derivative Constrained Functions built into the
                software. Available options include the default, 'polynomial',
                'normalised_polynomial', 'legendre', 'log_polynomial',
                'loglog_polynomial' and 'exponential'. For more details on the
                functional form of the built in basis see the ``maxsmooth``
                paper.

        **pivot_point: Default = len(x)//2 otherwise an integer between**
        **-len(x) and len(x)**
            | Some of the built in
                models rely on pivot points in the data sets which by defualt
                is set as the middle index. This can be altered via
                this kwarg which can occasionally lead to a better quality fit.

        base_dir: **Default = 'Fitted_Output/'**
            | The location of the outputted
                data from ``maxsmooth``. This must be a string and end in '/'.
                If the file does not exist then ``maxsmooth`` will create it.
                By default the only outputted data is a summary of the best
                fit but additional data can be recorded by setting the keyword
                argument 'data_save = True'.

        data_save: **Default = False**
            | By setting this to True the algorithm
                will save every tested set of parameters, signs and objective
                function evaluations into files in base_dir. Theses files will
                be over written on repeated runs but they are needed to run the
                'chidist_plotter'.

        print_output: **Default = 1**
            | The parameter can take a value of 0, 1, 2.
                If set to 2 this outputs to the
                terminal every fit performed by the algorithm. By default the
                parameter has a value of 1 and the
                only output is the optimal solution once the code is finished.
                Setting this to 0 will prevent any output to the terminal. This
                is useful when running the code inside a nested sampling loop
                for example.

        cvxopt_maxiter: **Default = 10000 else integer**
            | This shouldn't need
                changing for most problems however if ``CVXOPT`` fails with a
                'maxiters reached' error message this can be increased.
                Doing so arbitrarily will however increase the run time of
                ``maxsmooth``.

        initial_params: **Default = None else list of length N**
            | Allows the user
                to overwrite the default initial parameters used by ``CVXOPT``.

        **constraints: Default = 2 else an integer less than or equal**
        **to N - 1**
            | The minimum constrained derivative order which is set by default
                to 2 for a Maximally Smooth Function.

        zero_crossings: **Default = None else list of integers**
            | Allows you to
                specify if the conditions should be relaxed on any
                of the derivatives between constraints and the highest order
                derivative. e.g. a 6th order fit with just a constrained 2nd
                and 3rd order derivative would have zero_crossings = [4, 5].

        cap: **Default = (len(available_signs)//N) + N else an integer**
            | Determines the maximum number of signs explored either side of
                the minimum :math:`{\chi^2}` value found after the decent
                algorithm has terminated.

        chi_squared_limit: **Default = 2 else float or int**
            | The prefactor on the maximum allowed increase in :math:`{\chi^2}`
                during the directional exploration which is defaulted at 2.
                If this value multiplied by the minimum :math:`{\chi^2}`
                value found after the descent algorithm is exceeded then the
                exploration in one direction is stopped and started in the
                other. For more details on this and 'cap' see the ``maxsmooth``
                paper.

        The following Kwargs can be used by the user to define their own basis
        function and will overwrite the 'model_type' kwarg.

        **basis_function: Default = None else function with parameters**
        **(x, y, pivot_point, N)**
            | This is a function of basis functions
                for the quadratic programming. The variable pivot_point is the
                index at the middle of the datasets x and y by default but can
                be adjusted.

        **model: Default = None else function with parameters**
        **(x, y, pivot_point, N, params)**
            | This is
                a user defined function describing the model to be fitted to
                the data.

        **der_pres: Default = None else function with parameters**
        **(m, x, y, N, pivot_point)**
            | This function describes the prefactors on the
                mth order derivative used in defining the constraint.

        **derivatives: Default = None else function with parameters**
        **(m, x, y, N, pivot_point, params)**
            | User defined function describing the mth
                order derivative used to check that conditions are being met.

        **args: Default = None else list**
            | Extra arguments for `smooth`
                to pass to the functions detailed above.

    **Output**

        .y_fit: **numpy.array**
            | The fitted array of y data from smooth().

        .optimum_chi: **float**
            | The optimum :math:`{\chi^2}` value for the fit calculated by,

            .. math::

                {X^2=\sum(y-y_{fit})^2}.

        .optimum_params: **numpy.array**
            | The set of parameters corresponding to the optimum fit.

        .rms: **float**
            | The rms value of the residuals :math:`{y_{res}=y-y_{fit}}`
                calculated by,

            .. math::

                {rms=\sqrt{\frac{\sum(y-y_{fit})^2}{n}}}

            where :math:`n` is the number of data points.

        .derivatives: **numpy.array**
            | The :math:`m^{th}` order derivatives.

        .optimum_signs: **numpy.array**
            | The sign combinations corresponding to the
                optimal result. The nature of the constraint means that a
                negative ``maxsmooth`` sign implies a positive :math:`{m^{th}}`
                order derivative and visa versa.

    """

    def __init__(self, x, y, N, **kwargs):
        self.x = x
        self.y = y

        self.N = N
        if self.N % 1 != 0:
            raise ValueError('N must be an integer or whole number float.')

        for keys, values in kwargs.items():
            if keys not in set(
                    ['fit_type', 'model_type', 'base_dir',
                        'print_output', 'cvxopt_maxiter', 'zero_crossings',
                        'data_save',
                        'constraints', 'chi_squared_limit', 'cap',
                        'initial_params', 'basis_functions',
                        'der_pres', 'model',
                        'derivatives', 'args', 'pivot_point']):
                raise KeyError("Unexpected keyword argument in smooth().")

        self.fit_type = kwargs.pop('fit_type', 'qp-sign_flipping')
        if self.fit_type not in set(['qp', 'qp-sign_flipping']):
            raise KeyError(
                "Invalid 'fit_type'. Valid entries include 'qp'\n" +
                "'qp-sign_flipping'")

        self.pivot_point = kwargs.pop('pivot_point', len(self.x)//2)
        if type(self.pivot_point) is not int:
            raise TypeError('Pivot point is not an integer index.')
        elif self.pivot_point >= len(self.x) or \
                self.pivot_point < -len(self.x):
            raise ValueError(
                'Pivot point must be in the range -len(x) - len(x).')

        self.base_dir = kwargs.pop('base_dir', 'Fitted_Output/')
        if type(self.base_dir) is not str:
            raise KeyError("'base_dir' must be a string ending in '/'.")
        elif self.base_dir.endswith('/') is False:
            raise KeyError("'base_dir' must end in '/'.")

        self.model_type = kwargs.pop('model_type', 'difference_polynomial')
        if self.model_type not in set(
                ['normalised_polynomial', 'polynomial',
                    'log_polynomial', 'loglog_polynomial',
                    'difference_polynomial',
                    'exponential', 'legendre']):
            raise KeyError(
                "Invalid 'model_type'. See documentation for built" +
                "in models.")

        self.cvxopt_maxiter = kwargs.pop('cvxopt_maxiter', 10000)
        if type(self.cvxopt_maxiter) is not int:
            raise ValueError("'cvxopt_maxiter' is not integer.")

        self.print_output = kwargs.pop('print_output', 1)
        if self.print_output not in [0, 1, 2]:
            raise ValueError("'print_output' must have a value in the set" +
                             " [0, 1, 2]. See documentation for details.")

        self.data_save = kwargs.pop('data_save', False)
        if type(self.data_save) is not bool:
            raise TypeError(
                "Boolean keyword argument with value 'data_save'" +
                " is not True or False.")

        self.constraints = kwargs.pop('constraints', 2)
        if type(self.constraints) is not int:
            raise TypeError("'constraints' is not an integer")
        if self.constraints > self.N-1:
            raise ValueError(
                "'constraints' exceeds the number of derivatives.")

        self.zero_crossings = kwargs.pop('zero_crossings', None)
        if self.zero_crossings is not None:
            for i in range(len(self.zero_crossings)):
                if type(self.zero_crossings[i]) is not int:
                    raise TypeError(
                        "Entries in 'zero_crossings'" +
                        " are not integer.")
                if self.zero_crossings[i] < self.constraints:
                    raise ValueError(
                        'One or more specified derivatives for' +
                        ' zero crossings is less than the minimum' +
                        ' constrained' +
                        ' derivative.\n zero_crossings = ' +
                        str(self.zero_crossings)
                        + '\n' + ' Minimum Constrained Derivative = '
                        + str(self.constraints))

        self.chi_squared_limit = kwargs.pop('chi_squared_limit', None)
        self.cap = kwargs.pop('cap', None)
        if self.chi_squared_limit is not None:
            if isinstance(self.chi_squared_limit, int) or \
                    isinstance(self.chi_squared_limit, float):
                pass
            else:
                raise TypeError(
                    "Limit on maximum allowed increase in chi squared" +
                    ", 'chi_squared_limit', is not an integer or float.")
        if self.cap is not None:
            if type(self.cap) is not int:
                raise TypeError(
                    "The cap on directional exploration" +
                    ", 'cap', is not an integer.")

        self.initial_params = kwargs.pop('initial_params', None)
        if self.initial_params is not None and len(self.initial_params) \
                != self.N:
            raise ValueError(
                "Initial Parameters is not equal to the number" +
                "of terms in the polynomial, N.")
        if self.initial_params is not None and len(self.initial_params) \
                == self.N:
            for i in range(len(self.initial_params)):
                if type(self.initial_params[i]) is not int:
                    if type(self.initial_params[i]) is not float:
                        raise ValueError(
                            'One or more initial' +
                            'parameters is not numeric.')

        self.basis_functions = kwargs.pop('basis_functions', None)
        self.der_pres = kwargs.pop('der_pres', None)
        self.model = kwargs.pop('model', None)
        self.derivatives_function = kwargs.pop('derivatives', None)
        self.args = kwargs.pop('args', None)

        self.new_basis = {
            'basis_function':
            self.basis_functions, 'der_pres': self.der_pres,
            'derivatives_function': self.derivatives_function,
            'model': self.model, 'args': self.args}
        if np.all([value is None for value in self.new_basis.values()]):
            pass
        else:
            count = 0
            for key, value in self.new_basis.items():
                if value is None and key != 'args':
                    raise KeyError(
                        'Attempt to change basis functions failed.' +
                        ' One or more functions not defined.' +
                        ' Please consult documentation.')
                if value is None and key == 'args':
                    warnings.warn(
                        'Warning: No additional arguments passed to' +
                        ' new basis functions')
                count += 1
            if count == len(self.new_basis):
                self.model_type = 'user_defined'

        self.y_fit, self.optimum_signs, self.optimum_params, \
            self.derivatives,\
            self.optimum_chi, self.rms, self.optimum_zc_dict \
            = self.fitting()

    def fitting(self):

        def signs_array(nums):
            return np.array(list(product(*((x, -x) for x in nums))))

        if not os.path.exists(self.base_dir):
            os.mkdir(self.base_dir)

        if os.path.isdir(self.base_dir+'Output_Parameters/'):
            shutil.rmtree(self.base_dir+'Output_Parameters/')
        if os.path.isdir(self.base_dir+'Output_Signs/'):
            shutil.rmtree(self.base_dir+'Output_Signs/')
        if os.path.isdir(self.base_dir+'Output_Evaluation/'):
            shutil.rmtree(self.base_dir+'Output_Evaluation/')

        def qp(x, y, pivot_point):  # Testing all signs

            start = time.time()

            if self.zero_crossings is not None:
                signs = signs_array([1]*(
                    self.N-self.constraints-len(self.zero_crossings)))
            else:
                signs = signs_array([1]*(self.N-self.constraints))

            params, chi_squared, zc_dict, passed_signs = [], [], [], []
            append_params, append_chi, append_zc_dict, append_passed_signs = \
                params.append, chi_squared.append, zc_dict.append, \
                passed_signs.append
            for j in range(signs.shape[0]):
                fit = qp_class(
                    x, y, self.N, signs[j, :], pivot_point,
                    self.model_type, self.cvxopt_maxiter,
                    self.zero_crossings,
                    self.initial_params, self.constraints, self.new_basis)

                if self.print_output == 2:
                    print('-'*50)
                    print('Polynomial Order:', self.N)
                    if self.zero_crossings is not None:
                        print(
                            'Number of Constrained Derivatives:',
                            self.N-self.constraints-len(self.zero_crossings))
                    else:
                        print(
                            'Number of Constrained Derivatives:',
                            self.N-self.constraints)
                    print('Signs :', signs[j, :])
                    print('Objective Function Value:', fit.chi_squared)
                    print('Parameters:', (fit.parameters).T)
                    print('Method:', self.fit_type)
                    print('Model:', self.model_type)
                    print('Constraints: m >=', self.constraints)
                    if self.zero_crossings is None:
                        print(
                            'Zero Crossings Used?' +
                            ' (0 signifies Yes\n in derivative order "i"):',
                            fit.zc_dict)
                    if self.zero_crossings is not None:
                        print(
                            'Zero Crossing Derivatives:', self.zero_crossings)
                        print(
                            'Zero Crossings Used?' +
                            ' (0 signifies Yes\n in derivative order "i"):',
                            fit.zc_dict)
                    print('-'*50)

                append_params(fit.parameters)
                append_chi(fit.chi_squared)
                append_zc_dict(fit.zc_dict)
                append_passed_signs(signs[j, :])
                if self.data_save is True:
                    save(
                        self.base_dir, fit.parameters, fit.chi_squared,
                        signs[j, :], self.N, self.fit_type)

            params, chi_squared, zc_dict, passed_signs = np.array(params), \
                np.array(chi_squared), np.array(zc_dict), \
                np.array(passed_signs)

            Optimum_chi_squared = chi_squared.min()
            for f in range(len(chi_squared)):
                if chi_squared[f] == chi_squared.min():
                    Optimum_params = params[f, :]
                    Optimum_sign_combination = passed_signs[f, :]

            y_fit = Models_class(
                Optimum_params, x, y, self.N, pivot_point,
                self.model_type, self.new_basis).y_sum
            der = derivative_class(
                x, y, Optimum_params, self.N,
                pivot_point, self.model_type, self.zero_crossings,
                self.constraints, self.new_basis)
            derivatives, Optimum_zc_dict = der.derivatives, der.zc_dict

            end = time.time()

            if self.print_output in [1, 2]:
                print('#'*50)
                print('-'*20 + 'OPTIMUM RESULT' + '-'*20)
                print('Time:', end-start)
                print('Polynomial Order:', self.N)
                if self.zero_crossings is not None:
                    print(
                        'Number of Constrained Derivatives:',
                        self.N-self.constraints-len(self.zero_crossings))
                else:
                    print(
                        'Number of Constrained Derivatives:',
                        self.N-self.constraints)
                print('Signs :', Optimum_sign_combination)
                print('Objective Function Value:', Optimum_chi_squared)
                print('Parameters:', Optimum_params.T)
                print('Method:', self.fit_type)
                print('Model:', self.model_type)
                print('Constraints: m >=', self.constraints)
                if self.zero_crossings is None:
                    print(
                        'Zero Crossings Used?' +
                        ' (0 signifies Yes\n in derivative order "i"):',
                        Optimum_zc_dict)
                if self.zero_crossings is not None:
                    print('Zero Crossing Derivatives:', self.zero_crossings)
                    print(
                        'Zero Crossings Used?' +
                        ' (0 signifies Yes\n in derivative order "i"):',
                        Optimum_zc_dict)
                print('-'*50)
                print('#'*50)

            save_optimum(
                self.base_dir, end-start, self.N,
                Optimum_sign_combination, Optimum_chi_squared,
                Optimum_params, self.fit_type, self.model_type,
                self.zero_crossings, Optimum_zc_dict, self.constraints)

            return y_fit, derivatives, Optimum_chi_squared, Optimum_params, \
                Optimum_sign_combination, Optimum_zc_dict

        def qp_sign_flipping(x, y, pivot_point):  # Sign Sampling

            start = time.time()

            if self.zero_crossings is not None:
                array_signs = signs_array([1]*(
                    self.N-self.constraints-len(self.zero_crossings)))
            else:
                array_signs = signs_array([1]*(self.N-self.constraints))

            r = np.random.randint(0, len(array_signs), 1)
            signs = array_signs[r][0]

            tested_indices = []
            chi_squared = []
            parameters = []
            tested_signs = []
            for i in range(len(array_signs)):
                if i == r:
                    tested_indices.append(i)
            fit = qp_class(
                x, y, self.N, signs, pivot_point,
                self.model_type, self.cvxopt_maxiter,
                self.zero_crossings,
                self.initial_params, self.constraints, self.new_basis)
            chi_squared.append(fit.chi_squared)
            tested_signs.append(signs)
            parameters.append(fit.parameters)
            chi_squared_old = fit.chi_squared
            previous_signs = signs

            if self.print_output == 2:
                print('-'*50)
                print('Polynomial Order:', self.N)
                if self.zero_crossings is not None:
                    print(
                        'Number of Constrained Derivatives:',
                        self.N-self.constraints-len(self.zero_crossings))
                else:
                    print(
                        'Number of Constrained Derivatives:',
                        self.N-self.constraints)
                print('Signs :', signs)
                print('Objective Function Value:', chi_squared_old)
                print('Parameters:', (fit.parameters).T)
                print('Method:', self.fit_type)
                print('Model:', self.model_type)
                print('Constraints: m >=', self.constraints)
                if self.zero_crossings is None:
                    print(
                        'Zero Crossings Used?' +
                        ' (0 signifies Yes\n in derivative order "i"):',
                        fit.zc_dict)
                if self.zero_crossings is not None:
                    print(
                        'Zero Crossing Derivatives:',
                        self.zero_crossings)
                    print(
                        'Zero Crossings Used? (0 signifies' +
                        'Yes\n in derivative order "i"):', fit.zc_dict)
                print('-'*50)
            if self.data_save is True:
                save(
                    self.base_dir, fit.parameters, fit.chi_squared,
                    signs, self.N, self.fit_type)

            # Transforms or 'steps' of sign combination
            sign_transform = []
            for i in range(len(signs)):
                base = np.array([1]*len(signs))
                base[i] = -1
                sign_transform.append(base)
            sign_transform = np.array(sign_transform)
            chi_squared_new = 0

            while chi_squared_new < chi_squared_old:
                if chi_squared_new != 0:
                    chi_squared_old = chi_squared_new
                for h in range(sign_transform.shape[0]):
                    signs = previous_signs * sign_transform[h]
                    for i in range(len(array_signs)):
                        if np.all(signs == array_signs[i]):
                            ind = i
                    if ind in set(tested_indices):
                        pass
                    else:
                        tested_indices.append(ind)
                        fit = qp_class(
                            x, y, self.N, signs, pivot_point,
                            self.model_type, self.cvxopt_maxiter,
                            self.zero_crossings, self.initial_params,
                            self.constraints, self.new_basis)
                        if fit.chi_squared < chi_squared_old:
                            chi_squared_new = fit.chi_squared
                            previous_signs = signs
                            chi_squared.append(fit.chi_squared)
                            tested_signs.append(signs)
                            parameters.append(fit.parameters)

                            if self.print_output == 2:
                                print('-'*50)
                                print('Polynomial Order:', self.N)
                                if self.zero_crossings is not None:
                                    print(
                                        'Number of Constrained Derivatives:',
                                        self.N - self.constraints -
                                        len(self.zero_crossings))
                                else:
                                    print(
                                        'Number of Constrained Derivatives:',
                                        self.N-self.constraints)
                                print('Signs :', signs)
                                print(
                                    'Objective Function Value:',
                                    fit.chi_squared)
                                print('Parameters:', fit.parameters.T)
                                print('Method:', self.fit_type)
                                print('Model:', self.model_type)
                                print('Constraints: m >=', self.constraints)
                                if self.zero_crossings is None:
                                    print(
                                        'Zero Crossings Used?' +
                                        ' (0 signifies Yes\n in derivative' +
                                        ' order "i"):',
                                        fit.zc_dict)
                                if self.zero_crossings is not None:
                                    print(
                                        'Zero Crossing Derivatives:',
                                        self.zero_crossings)
                                    print(
                                        'Zero Crossings Used?' +
                                        ' (0 signifies' +
                                        'Yes\n in derivative order "i"):',
                                        fit.zc_dict)
                                print('-'*50)
                            if self.data_save is True:
                                save(
                                    self.base_dir, fit.parameters,
                                    fit.chi_squared,
                                    signs, self.N, self.fit_type)

                            break
                    if h == sign_transform.shape[0] - 1:
                        chi_squared_new = chi_squared_old
                        break

            if self.data_save is True:
                np.save(
                    self.base_dir + str(self.N) +
                    '_'+self.fit_type+'_minimum_chi_post_descent.npy',
                    min(chi_squared))

            if self.chi_squared_limit is not None:
                lim = self.chi_squared_limit*min(chi_squared)
            else:
                lim = 2*min(chi_squared)

            if self.cap is not None:
                cap = self.cap
            else:
                cap = (len(array_signs)//self.N) + self.N

            for i in range(len(array_signs)):
                if np.all(previous_signs == array_signs[i]):
                    index = i

            down_int = 1
            while down_int < cap:
                if index-down_int < 0:
                    break
                elif (index-down_int) in set(tested_indices):
                    chi_down = 0
                    pass
                else:
                    signs = array_signs[index-down_int]
                    tested_indices.append(index - down_int)
                    fit = qp_class(
                        x, y, self.N, signs, pivot_point,
                        self.model_type, self.cvxopt_maxiter,
                        self.zero_crossings, self.initial_params,
                        self.constraints, self.new_basis)
                    chi_down = fit.chi_squared
                    chi_squared.append(fit.chi_squared)
                    tested_signs.append(signs)
                    parameters.append(fit.parameters)

                    if self.print_output == 2:
                        print('-'*50)
                        print('Polynomial Order:', self.N)
                        if self.zero_crossings is not None:
                            print(
                                'Number of Constrained Derivatives:',
                                self.N-self.constraints -
                                len(self.zero_crossings))
                        else:
                            print(
                                'Number of Constrained Derivatives:',
                                self.N-self.constraints)
                        print('Signs :', signs)
                        print('Objective Function Value:', fit.chi_squared)
                        print('Parameters:', fit.parameters.T)
                        print('Method:', self.fit_type)
                        print('Model:', self.model_type)
                        print('Constraints: m >=', self.constraints)
                        if self.zero_crossings is None:
                            print(
                                'Zero Crossings Used?' +
                                ' (0 signifies Yes\n in derivative' +
                                ' order "i"):',
                                fit.zc_dict)
                        if self.zero_crossings is not None:
                            print(
                                'Zero Crossing Derivatives:',
                                self.zero_crossings)
                            print(
                                'Zero Crossings Used? (0 signifies' +
                                'Yes\n in derivative order "i"):',
                                fit.zc_dict)
                        print('-'*50)
                    if self.data_save is True:
                        save(
                            self.base_dir, fit.parameters, fit.chi_squared,
                            signs, self.N, self.fit_type)

                    if chi_down > lim:
                        break
                down_int += 1

            up_int = 1
            while up_int < cap:
                if index+up_int >= len(array_signs):
                    break
                elif (index + up_int) in set(tested_indices):
                    chi_up = 0
                    pass
                else:
                    signs = array_signs[index+up_int]
                    tested_indices.append(index + up_int)
                    fit = qp_class(
                        x, y, self.N, signs, pivot_point,
                        self.model_type, self.cvxopt_maxiter,
                        self.zero_crossings, self.initial_params,
                        self.constraints, self.new_basis)
                    chi_up = fit.chi_squared
                    chi_squared.append(fit.chi_squared)
                    tested_signs.append(signs)
                    parameters.append(fit.parameters)

                    if self.print_output == 2:
                        print('-'*50)
                        print('Polynomial Order:', self.N)
                        if self.zero_crossings is not None:
                            print(
                                'Number of Constrained Derivatives:',
                                self.N-self.constraints -
                                len(self.zero_crossings))
                        else:
                            print(
                                'Number of Constrained Derivatives:',
                                self.N-self.constraints)
                        print('Signs :', signs)
                        print('Objective Function Value:', fit.chi_squared)
                        print('Parameters:', fit.parameters.T)
                        print('Method:', self.fit_type)
                        print('Model:', self.model_type)
                        print('Constraints: m >=', self.constraints)
                        if self.zero_crossings is None:
                            print(
                                'Zero Crossings Used?' +
                                ' (0 signifies Yes\n in derivative' +
                                ' order "i"):',
                                fit.zc_dict)
                        if self.zero_crossings is not None:
                            print(
                                'Zero Crossing Derivatives:',
                                self.zero_crossings)
                            print(
                                'Zero Crossings Used? (0 signifies' +
                                'Yes\n in derivative order "i"):',
                                fit.zc_dict)
                        print('-'*50)
                    if self.data_save is True:
                        save(
                            self.base_dir, fit.parameters, fit.chi_squared,
                            signs, self.N, self.fit_type)

                    if chi_up > lim:
                        break
                up_int += 1

            for i in range(len(chi_squared)):
                if chi_squared[i] == min(chi_squared):
                    Optimum_params = parameters[i]
                    Optimum_sign_combination = tested_signs[i]
                    Optimum_chi_squared = chi_squared[i]

            y_fit = Models_class(
                Optimum_params, x, y, self.N, pivot_point,
                self.model_type, self.new_basis).y_sum
            der = derivative_class(
                x, y, Optimum_params, self.N,
                pivot_point, self.model_type, self.zero_crossings,
                self.constraints, self.new_basis)
            derivatives, Optimum_zc_dict = der.derivatives, der.zc_dict

            end = time.time()

            if self.print_output in [1, 2]:
                print('#'*50)
                print('-'*20 + 'OPTIMUM RESULT' + '-'*20)
                print('Time:', end-start)
                print('Polynomial Order:', self.N)
                if self.zero_crossings is not None:
                    print(
                        'Number of Constrained Derivatives:',
                        self.N-self.constraints-len(self.zero_crossings))
                else:
                    print(
                        'Number of Constrained Derivatives:',
                        self.N-self.constraints)
                print('Signs :', Optimum_sign_combination)
                print('Objective Function Value:', Optimum_chi_squared)
                print('Parameters:', Optimum_params.T)
                print('Method:', self.fit_type)
                print('Model:', self.model_type)
                print('Constraints: m >=', self.constraints)
                if self.zero_crossings is None:
                    print(
                        'Zero Crossings Used?' +
                        ' (0 signifies Yes\n in derivative order "i"):',
                        Optimum_zc_dict)
                if self.zero_crossings is not None:
                    print('Zero Crossing Derivatives:', self.zero_crossings)
                    print(
                        'Zero Crossings Used?' +
                        ' (0 signifies Yes\n in derivative order "i"):',
                        Optimum_zc_dict)
                print('-'*50)
                print('#'*50)

            save_optimum(
                self.base_dir, end-start, self.N,
                Optimum_sign_combination, Optimum_chi_squared,
                Optimum_params, self.fit_type, self.model_type,
                self.zero_crossings, Optimum_zc_dict, self.constraints)

            return y_fit, derivatives, Optimum_chi_squared, Optimum_params, \
                Optimum_sign_combination, Optimum_zc_dict

        if self.fit_type == 'qp':
            y_fit, derivatives, Optimum_chi_squared, Optimum_params, \
                Optimum_sign_combination, Optimum_zc_dict = \
                qp(self.x, self.y, self.pivot_point)
        if self.fit_type == 'qp-sign_flipping':
            y_fit, derivatives, Optimum_chi_squared, Optimum_params, \
                Optimum_sign_combination, Optimum_zc_dict = \
                qp_sign_flipping(self.x, self.y, self.pivot_point)

        rms = (np.sqrt(np.sum((self.y-y_fit)**2)/len(self.y)))

        return y_fit, Optimum_sign_combination, Optimum_params, derivatives, \
            Optimum_chi_squared, rms, Optimum_zc_dict
