import numpy as np
#used for the histograms, will remove when move bokeh histo to 
#to use Plot():
import bokeh.plotting as bp
import bokeh.io as bi
#used for array and number types:
import qexpy.utils as qu
#used to check plot_engine:
import qexpy as q
import re
import pandas as pd


class ExperimentalValue:
    '''Root class of objects which contains a mean and standard deviation.
    From this class, objects with properties pertaining to their use or
    formulation can be instanced. (ie. the result of an operation of
    measured values, called Function and Measurement respectivly)
    '''
    error_method = "Derivative"  # Default error propogation method
    print_style = "Default"  # Default printing style
    mc_trial_number = 10000  # number of trial in Monte Carlo simulation
    minmax_n = 100 # grid size in MinMax calculation

    figs = None
    figs_on_uncertainty = False
    register = {}
    formula_register = {}
    id_register = {}

    # Defining common types under single arrayclear
    from numpy import int64, float64, ndarray, int32, float32
    CONSTANT = qu.number_types #(int, float, int64, float64, int32, float32)
    ARRAY = qu.array_types #(list, tuple, ndarray)

    def __init__(self, *args, name=None):
        '''Creates a variable that contains a mean, standard deviation,
        and name for inputted data.
        '''
        self.der = [0, 0]
        self.MinMax = [0, 0]
        self.MC = [0, 0]

        self.info = {
                'ID': '', 'Formula': '', 'Method': '', 'Data': [],\
                'Function': {
                        'operation': (), 'variables': ()}, }

        if len(args) ==1:
            if isinstance(args[0], qu.array_types):
                data = np.ndarray(len(args[0]))
                self.info['Data'] = data
                for index in range(len(args[0])):
                    data[index] = args[0][index]
                self.mean = data.mean()
                self.std = data.std(ddof=1)
                self.error_on_mean = 0 if data.size==0 else self.std/np.sqrt(data.size)             
            else:
                raise TypeError('''Input must be either a single array of values,
                      or the central value and uncertainty in one measurement''')
        elif len(args)==2:
            if isinstance(args[0], qu.number_types) and isinstance(args[1], qu.number_types):
                self.mean = float(args[0])
                data = np.ndarray(1)
                error_data = np.ndarray(1)
                data[0] = self.mean
                self.info['Data'] = data
                self.std = float(args[1])
            elif isinstance(args[0], qu.array_types) and isinstance(args[1], qu.array_types):
                raise TypeError('''Input must be either a single array of values,
                      or the central value and uncertainty in one measurement.
                      
                      The feature of passing a list of measurements and their corresponding
                      uncertanties is now deprecated. Please use a MeasurementArray insted.
                      More info: 
                      https://github.com/Queens-Physics/qexpy/blob/master/examples/jupyter/1_Intro_to_Error_Propagation.ipynb''')
            else:
                raise TypeError('''Input must be either a single array of values,
                      or the central value and uncertainty in one measurement''')
        else:
            raise TypeError('''Input must be either a single array of values,
                  or the central value and uncertainty in one measurement''')

        ExperimentalValue.id_register[id(self)] = self
        self.print_style = ExperimentalValue.print_style
        
        if name is not None:
            self.user_name = True
        else:
            self.user_name = False

        self._units = {}
        self.MC_list = None
        '''The list of values generated by the Monte Carlo simulation.'''
        
###############################################################################
# Methods for printing or returning Measurement paramters
###############################################################################

    def __str__(self):
        '''Method called when printing measurement objects.'''
        if self.user_name:
            string = self.name+' = '
        else:
            string = ''

        if ExperimentalValue.print_style == "Latex":
            string += _tex_print(self)
        elif ExperimentalValue.print_style == "Default":
            string += _def_print(self)
        elif ExperimentalValue.print_style == "Scientific":
            string += _sci_print(self)

        unit_string = self.get_units_str()
        if unit_string != '':
            if ExperimentalValue.print_style == "Latex":
                string += '\,'+unit_string
            else:
                string += ' ['+unit_string+']'

        return string

    def print_mc_error(self):
        '''Prints the result of a Monte Carlo error propagation.

        The purpose of this method is to easily compare the results of a
        Monte Carlo propagation with whatever method is chosen.

        .. code-block:: python

            q.set_sigfigs(4)
            x = q.Measurement(4, 0.5)
            y = q.Measurement(9.8, 0.1)
            z = x**2*y

            z.print_mc_error()

        .. nboutput:: ipython3

           159.90 +/- 39.04
        '''
        if self.print_style == "Latex":
            string = _tex_print(self, method="Monte Carlo")
        elif self.print_style == "Default":
            string = _def_print(self, method="Monte Carlo")
        elif self.print_style == "Scientific":
            string = _sci_print(self, method="Monte Carlo")
        print(string)

    def print_min_max_error(self):
        '''Prints the result of a Min-Max method error propagation.

        The purpose of this method is to easily compare the results of a
        Min-Max propagation with whatever method is chosen to confirm that
        the Min-Max is the upper bound of the error.

        .. code-block:: python

            q.set_sigfigs(4)
            x = q.Measurement(4, 0.5)
            y = q.Measurement(9.8, 0.1)
            z = x**2*y

            z.print_min_max_error()

        .. nboutput:: ipython3

           159.65 +/- 40.82
        '''
        if self.print_style == "Latex":
            string = _tex_print(self, method="Min Max")
        elif self.print_style == "Default":
            string = _def_print(self, method="Min Max")
        elif self.print_style == "Scientific":
            string = _sci_print(self, method="Min Max")
        print(string)

    def print_deriv_error(self):
        '''Prints the result of a derivative method error propagation.

        The purpose of this method is to easily compare the results of a
        derivative propagation with whatever method is chosen.

        .. code-block:: python

            q.set_sigfigs(4)
            x = q.Measurement(4, 0.5)
            y = q.Measurement(9.8, 0.1)
            z = x**2*y

            z.print_deriv_error()

        .. nboutput:: ipython3

           156.80 +/- 39.23
        '''
        if self.print_style == "Latex":
            string = _tex_print(self, method="Derivative")
        elif self.print_style == "Default":
            string = _def_print(self, method="Derivative")
        elif self.print_style == "Scientific":
            string = _sci_print(self, method="Derivative")
        print(string)

    def get_derivative(self, variable=None):
        '''Function to find the derivative of a measurement or measurement like
        object. By default, derivative is with respect to itself, which will
        always yeild 1. Operator acts by acessing the self.derivative
        dictionary and returning the value stored there under the specific
        variable inputted (ie. differentiating with respect to variable = ???)

        :param variable: The variable that the derivative is to be taken with respect to.
        :type variable: Measurement

        :returns: the numerical value of the derivative with respect to the variable
        :rtype: float

        .. code-block:: python

            x = q.Measurement(4, 0.5)
            y = 2*(x**2)

            print('dy/dx =', y.get_derivative(x))

        .. nboutput:: ipython3

           dy/dx = 16.0
        '''
        if variable is not None \
                and type(variable) is not Measurement:
            print('''The derivative of a Measurement with respect to anything
                  other than a Measurement is zero.''')
            return 0

        elif variable is None:
            raise TypeError('''The object must be differentiated with respect to another
            Measurement.''')

        if variable.info['ID'] not in self.derivative:
            self.derivative[variable.info['ID']] = 0

        derivative = self.derivative[variable.info["ID"]]
        return derivative

    @property
    def mean(self):
        '''The mean of a Measurement object.

        :setter: Sets the mean of the Measurement.
        :getter: Returns the mean of the Measurement.
        :type: float

        .. code-block:: python

            x = q.Measurement(0, 1)

            x.mean = 10
            print(x)

        .. nboutput:: ipython3

           10 +/- 1
        '''
        return self._mean

    @mean.setter
    def mean(self, mean):
        '''Sets the mean of a Measurement object.
        '''
        if(type(mean) in ExperimentalValue.CONSTANT):
            self.der[0] = mean
            self.MinMax[0] = mean
            self.MC[0] = mean
            self._mean = mean
        else:
            print("Mean must be a number")
            self._mean = 0

    @property
    def std(self):
        '''Gets the standard deviation of a Measurement object.

        :setter: Sets the error of the Measurement.
        :getter: Returns the error of the Measurement.
        :type: float

        .. code-block:: python

            x = q.Measurement(5, 0)

            x.std = 0.5
            print(x)

        .. nboutput:: ipython3

           5.0 +/- 0.5
        '''
        return self._std

    @std.setter
    def std(self, std):
        '''Sets the standard deviation of a Measurement object.
        '''
        if(type(std) in ExperimentalValue.CONSTANT):
            self.der[1] = std
            self.MinMax[1] = std
            self.MC[1] = std
            self._error_on_mean = std/np.sqrt(len(self.get_data_array()))
            self._std = std
        else:
            print("Standard deviation must be a number")
            self._std = 0

    @property
    def error_on_mean(self):
        '''The error on the mean of a Measurement object.

        :setter: Sets the error on the mean of the Measurement object. Also updates the error on the Measurement.
        :getter: Returns the error on the mean of the Measurement.
        :type: float

        .. code-block:: python

            x = q.Measurement(5, 0)

            x.error_on_mean = 0.5
            print('Error on mean:', x.error_on_mean)

        .. nboutput:: ipython3

            Error on mean: 0.5
        '''
        if self._error_on_mean:
            return self._error_on_mean
        else:
            print("Error: error on mean not calculated")
            return 0

    @error_on_mean.setter
    def error_on_mean(self, error_on_mean):
        '''Sets the error on the mean of a Measurement object.
        '''
        if(type(error_on_mean) in ExperimentalValue.CONSTANT):
            self._error_on_mean = error_on_mean
            self._std = error_on_mean*np.sqrt(len(self.get_data_array()))
        else:
            print("Error on mean must be a number")
            self._error_on_mean = 0

    @property
    def name(self):
        '''The name of a Measurement object.

        :setter: Sets the name of the Measurement object.
        :getter: Returns the name of the Measurement object.
        :type: str

        .. code-block:: python

            x = q.Measurement(5, 0.2, name='mass')

            x.name = 'length'
            print(x)

        .. nboutput:: ipython3

            length = 5.0 +/- 0.2
        '''
        return self._name

    @name.setter
    def name(self, name):
        '''Sets the name of a Measurement object.
        '''
        if isinstance(name, str):
            self._name = name
        else:
            print("Name must be a string")
            self._name = 'unnamed_var%d' % (Measurement.id_number)

    @property
    def units(self):
        '''The units of a Measurement object.

        :setter: Sets the units of the Measurement object.
        :getter: Returns the name of the Measurement object.
        :type: str

        .. code-block:: python

            x = q.Measurement(5, 0.2, name='length')

            x.units = 'm'
            print(x)

        .. nboutput:: ipython3

           length = 5.0 +/- 0.2 [m]
        '''
        return self._units

    @units.setter
    def units(self, units):
        '''Sets the units of a Measurement object.
        '''
        if units is not None:
            if type(units) is str:
                self._units = _parse_units(units)
            elif type(units) is dict:
                self._units = units
            else:
                for i in range(len(units)//2):
                    self._units[units[2*i]] = units[2*i+1]

    @property
    def relative_error(self):
        '''Gets the relative error (error/mean) of a Measurement object.

        :setter: Sets the error of the Measurement to a fraction of it's mean. 
        :getter: Returns the error as a fraction of the mean 
                 (ie. 0.1 corresponds to a 10% error).
        :type: float

        .. code-block:: python

            x = q.Measurement(5, 0)

            x.relative_error = 0.1
            print(x)

        .. nboutput:: ipython3

           5.0 +/- 0.5
        '''
        return self.std/self.mean if self.mean !=0 else 0. 

    @relative_error.setter
    def relative_error(self, rel_error):
        '''Sets the relative error (error/mean) of a Measurement object.
        '''
        if(type(rel_error) in qu.number_types):
            self.std = self.mean*rel_error
        else:
            print("Relative error must be a number")

    def get_data_array(self):
        '''Returns the underlying data array used to create the Measurement object.

        :returns: A python list of the data used to create the Measurement object.
        :rtype: list

        .. code-block:: python

            x = q.Measurement([11, 12, 13])
            
            print(x.get_data_array())

        .. nboutput:: ipython3

           [ 11.  12.  13.]
        '''
        if self.info['Data'] is None:
            print('No data array exists.')
            return None
        return self.info['Data']

    def get_units_str(self):
        '''Gets the units of the associated Measurement.

        :returns: The units of the Measurement.
        :rtype: str

        .. code-block:: python

            mass = q.Measurement(4, 0.5, units='kg')
            accel = q.Measurement(9.8, 0.1, units='m/s^2')
            force = mass*accel

            print(force.get_units_str())

        .. nboutput:: ipython3

           s^-2 kg^1 m^1
        '''
        unit_string = ''
        unit_dict = self.units
        if unit_dict != {}:
            for key in unit_dict:
                if unit_dict[key] == 1 and len(unit_dict.keys()) is 1:
                    unit_string = key + unit_string
                else:
                    unit_string += key+'^%d' % (unit_dict[key])
                    unit_string += ' '

                if unit_string == '':
                    unit_string = 'unitless'
        return unit_string
    
    def show_histogram(self, bins=50, color="#036564", title=None, output='inline'):
        '''Creates a histogram of the inputted data using Bokeh or mpl.

        :param bins: The number of bins for the histogram.
        :type bins: int
        :param color: The color of the bars.
        :type color: str
        :param title: The title that will appear at the top of the histogram.
        :type title: str
        :param output: How the histogram is to be output. Can be 'inline' or 'file'.
        :type output: str

        :returns: The Plot object used to create the histogram.
        :rtype: Plot

        .. bokeh-plot::
           :source-position: above

           import qexpy as q
           import numpy as np

           rand_norm = np.random.normal(10, 1, 1000)
           x = q.Measurement(rand_norm)

           x.show_histogram(title='Mean 10, std. 1 random normal')
        '''
        if self.info['Data'] is None:
            print("no data to histogram")
            return None

        if type(title) is str:
            hist_title = title
        elif title is None:
            hist_title = self.name+' Histogram'
        else:
            print('Histogram title must be a string.')
            hist_title = self.name+' Histogram'                     
        
        data_arr = self.info['Data']

        data = q.XYDataSet(xdata = data_arr, is_histogram = True, data_name=hist_title)
        fig = q.MakePlot()
        fig.add_dataset(data, color=color)
        fig.x_range = [min(data_arr)*.95,max(data_arr)*1.05]
        fig.y_range = [0,max(data.ydata)*1.2]

        mean = self.mean
        std = self.std
        fig.add_line(x=mean, dashed=False, color='red')
        fig.add_line(x=mean+std, dashed=True, color='red')
        fig.add_line(x=mean-std, dashed=True, color='red')

        fig.show(output=output)
        return fig
    
    def show_MC_histogram(self, bins=50, color="#036564", title=None, output='inline'):
        '''Creates and shows a Bokeh plot of a histogram of the values
        calculated by Monte Carlo error simulation.

        :param bins: The number of bins for the histogram.
        :type bins: int
        :param color: The color of the bars.
        :type color: str
        :param title: The title that will appear at the top of the histogram.
        :type title: str
        :param output: How the histogram is to be output. Can be 'inline' or 'file'.
        :type output: str

        :returns: The Plot object used to create the histogram.
        :rtype: Plot

        .. bokeh-plot::
           :source-position: above

           import qexpy as q

           x = q.Measurement(10, 0.2)
           y = q.Measurement(8, 0.4)
           z = y**2*x

           z.show_MC_histogram(title='Monte Carlo histogram')
        '''
        MC_data = self.MC_list
        if MC_data is None:
            print("no MC data to histogram")
            return None

        if type(title) is str:
            hist_title = title
        elif title is None:
            hist_title = self.name+' Histogram'
        else:
            print('Histogram title must be a string.')
            hist_title = self.name+' Histogram'

        data = q.XYDataSet(xdata = MC_data, is_histogram = True, data_name=hist_title)
        fig = q.MakePlot()
        fig.add_dataset(data, color = color)
        fig.x_range = [min(MC_data)*.95,max(MC_data)*1.05]
        fig.y_range = [0,max(data.ydata)*1.2]

        # Adds a line at the mean and at the range corresponding to 68% coverage.
        MC_mean, MC_std = self.MC
        fig.add_line(x=MC_mean, dashed=False, color='red')
        fig.add_line(x=MC_mean+MC_std, dashed=True, color='red')
        fig.add_line(x=MC_mean-MC_std, dashed=True, color='red')

        fig.show(output=output)
        return fig

    def show_error_contribution(self, color="#036564", title=None, output='inline'):
        '''Creates and shows a Bokeh or mpl plot of a histogram of the relative
        contribution of individual measurements to the variance of a calculated value.
        
        :param color: The color of the bars.
        :type color: str
        :param title: The title that will appear at the top of the histogram.
        :type title: str
        :param output: How the histogram is to be output. Can be 'inline' or 'file'.
        :type output: str

        :returns: The Plot object used to create the histogram.
        :rtype: Plot

        .. bokeh-plot::
           :source-position: above

           import qexpy as q
           
           w = q.Measurement(0.0255, 0.0001, name='width')
           t = q.Measurement(0.0050, 0.0001, name='thickness')
           m = q.Measurement(0.156, 0.001, name='mass')
           P = q.Measurement(6.867, 0, name='P')

           E = 4*P/(m**2*w*t)

           E.show_error_contribution(title='Error contribution')
        '''
        terms = {}
        formula = self.info['Formula']

        # For each measurement that goes into the calculation, evaluate the calculation
        # at that measurement +/- the std. Take the output of that and do .5*(output-mean)^2.
        # Add the +std and -std term. 
        # This process is described in this paper: http://pubs.acs.org/doi/abs/10.1021/ed1004307 
        for i in self.root:
            maxx = formula
            minn = formula
            name = ""
            for j in self.root:
                meas = q.error.ExperimentalValue.register[j]
                if j == i:
                    name = meas.name
                    maxx = maxx.replace(j, str(meas.mean+meas.std))
                    minn = minn.replace(j, str(meas.mean-meas.std))
                else:
                    maxx = maxx.replace(j, str(meas.mean))
                    minn = minn.replace(j, str(meas.mean))
            terms[name] = .5*(eval(maxx)-self.mean)**2+.5*(eval(minn)-self.mean)**2
            
        N = len(terms)
        names = []
        vals = []

        for k, v in sorted(terms.items()):
            names.append(k)
            vals.append(v)

        # Change the absolute terms into relative terms.
        summ = sum(vals)
        for index in range(N):
            vals[index] = vals[index]/summ

        # Add spacing to make the histogram look like a bar chart.
        new_vals = []
        new_names = []
        for index in range(N):
            new_vals.append(vals[index])
            new_vals.append(0)
            new_names.append('')
            new_names.append(names[index])
        new_vals = new_vals[0:-1]

        if title is not None:
            plot_title = title
        else:
            plot_title = 'Relative contribution to variance of {}'.format(self.name)

        data = q.XYDataSet(xdata=np.arange(2*N-1), ydata=new_vals, is_histogram = True, bins=N,
                    data_name=plot_title)

        # Populates the mpl figure in case it is plotted.
        fig = q.MakePlot()
        fig.add_dataset(data, color=color)
        fig.x_range = [-1,2*N-1]
        fig.y_range = [0,1]
        fig.set_labels(xtitle="", ytitle="")
        fig.populate_mpl_figure()
        fig.mplfigure_main_ax.axes.set_xticklabels(new_names)
        fig.mplfigure_main_ax.axes.grid(False, which='both', axis='x')

        # Populates the boken figure in case it is plotted.
        # The messy stuff comes from the fact that mpl boxes, 
        # mpl labels and bokeh boxes are 0-indexed, but mpl labels are 1 indexed.
        fig.axes['xscale'] = 'auto'
        fig.datasets[0].ydata = np.insert(fig.datasets[0].ydata, [0, 0, 2*N-1], [0, 0, 0])
        fig.datasets[0].xdata = np.append(fig.datasets[0].xdata, [2*N-1, 2*N, 2*N+1])
        fig.x_range = new_names+['']
        fig.populate_bokeh_figure()
        fig.bkfigure.xgrid.grid_line_color = None
        fig.bkfigure.xaxis.major_tick_line_color = None
        fig.bkfigure.xaxis.minor_tick_line_color = None

        # Will use whatever plotting engine is in use.
        fig.show(populate_figure=False, refresh = True)

        return fig

###############################################################################
# Methods for Correlation and Covariance
###############################################################################

    def _find_covariance(x, y):
        '''Uses the data from which x and y were generated to calculate
        covariance and add this informaiton to x and y.

        Requires data arrays to be stored in the .info of both objects
        and that these arrays are of the same length, as the covariance is
        only defined in these cases.

        :param x: A Measurement with an underlying data array the same length as y.
        :type x: Measurement
        :param y: A Measurement with an underlying data array the same length as x.
        :type y: Measurement
        '''
        try:
            x.covariance[y.info['ID']]
            return
        except KeyError:
            pass

        data_x = x.info["Data"]
        data_y = y.info["Data"]

        if data_x is None or data_y is None:
            raise TypeError(
                "Data arrays must exist for both quantities " +
                "to define covariance.")

        if len(data_x) != len(data_y):
            raise TypeError('Lengths of data arrays must be equal to\
                      define a covariance')
            
        sigma_xy = np.sum((data_x-x.mean)*(data_y-y.mean))  
        nmin1 = len(data_x)-1
        if nmin1 != 0:
            sigma_xy /= nmin1
        x.covariance[y.info['ID']] = sigma_xy
        y.covariance[x.info['ID']] = sigma_xy

        factor = sigma_xy
        if x.std != 0:
            factor /= x.std
        if y.std != 0:
            factor /= y.std
              
        x.correlation[y.info['ID']] = factor
        y.correlation[x.info['ID']] = factor

    def set_correlation(self, y, factor):
        '''Manually set the correlation between two quantities

        Given a correlation factor, the covariance and correlation
        between two variables is added to both objects.

        :param y: The variable to set the correlation with respect to.
        :type y: str
        :param factor: The correlation term between the two variables.
                       Must be between -1 and 1 inclusive.
        :type factor: float

        .. code-block:: python

            x = q.Measurement(4, 0.1, units='m')
            y = q.Measurement(12, 0.2, units='m')
            x.set_correlation(y, 0.8)

            print(x.get_correlation(y))

        .. nboutput:: ipython3

           0.8
        '''
        if factor > 1 or factor < -1:
            raise ValueError('Correlation factor must be between -1 and 1.')

        x = self
        rho_xy = factor
        sigma_xy = rho_xy*x.std*y.std

        x.correlation[y.info['ID']] = factor
        y.correlation[x.info['ID']] = factor

        x.covariance[y.info['ID']] = sigma_xy
        y.covariance[x.info['ID']] = sigma_xy

    def set_covariance(self, y, sigma_xy):
        '''Manually set the covariance between two quantities

        Given a covariance value, the covariance and correlation
        between two variables is added to both objects.

        :param y: The variable to set the covariance with respect to.
        :type y: Measurement
        :param sigma_xy: The covariance term between the two variables.
        :type sigma_xy: float

        .. code-block:: python

            x = q.Measurement(4, 0.1, units='m')
            y = q.Measurement(12, 0.2, units='m')
            x.set_covariance(y, 1.2)

            print(x.get_covariance(y))

        .. nboutput:: ipython3

           1.2
        '''
        x = self
        factor = sigma_xy
        if x.std != 0:
            factor /= x.std
        if y.std != 0:
            factor /= y.std

        x.correlation[y.info['ID']] = factor
        y.correlation[x.info['ID']] = factor

        x.covariance[y.info['ID']] = sigma_xy
        y.covariance[x.info['ID']] = sigma_xy

    def get_covariance(self, y):
        '''Returns the covariance of the object and a specified variable.

        This funciton checks for the existance of a data array in each
        object and that the covariance of the two objects is not already
        specified. In each case, the covariance is returned, unless
        the data arrays are of different lengths or do not exist, in that
        case a covariance of zero is returned.

        :param y: The variable to get the covariance with respect to.
        :type y: Measurement

        :returns: The covariance term between the two variables.
        :rtype: float

        .. code-block:: python

            x = q.Measurement([11, 12, 13])
            y = q.Measurement([11, 12.1, 12.9])

            print('xy covariance: ', x.get_covariance(y))

        .. nboutput:: ipython3

           xy covariance:  0.95
        '''
        if isinstance(self, Constant) or isinstance(y, Constant):
            return 0
        if self.info['ID'] in y.covariance:
            return self.covariance[y.info['ID']]

        elif self.info["Data"] is not None \
                and y.info["Data"] is not None\
                and len(self) == len(y) and len(self) != 1:
            ExperimentalValue._find_covariance(self, y)
            var = self.covariance[y.info['ID']]
            return var
        elif isinstance(self, Function):
            term = 0
            for root in self.root:
                root_obj = ExperimentalValue.register[root]
                partial_der = self.derivative[root]
                term += partial_der*y.get_covariance(root_obj)
            if term:
                self.set_covariance(y, term)
                return term
        elif isinstance(y, Function):
            term = 0
            for root in y.root:
                root_obj = ExperimentalValue.register[root]
                partial_der = y.derivative[root]
                term += partial_der*self.get_covariance(root_obj)
            if term:
                self.set_covariance(y, term)
                return term
        return 0

    def get_correlation(self, y):
        '''Returns the correlation factor of two measurements.

        Using the covariance, or finding the covariance if not defined,
        the correlation factor of two measurements is returned.

        :param y: The variable to get the covariance with respect to.
        :type y: Measurement

        :returns: The correlation term between the two variables.
        :rtype: float

        .. code-block:: python

            x = q.Measurement([11, 12, 13])
            y = q.Measurement([11, 12.4, 12.6])

            print('xy correlation: ', x.get_correlation(y))

        .. nboutput:: ipython3

           xy correlation:  0.917662935482
        '''
        x = self
        if x.std == 0 or y.std == 0:
            return 0

        if y.get_covariance(x) is not 0:
            pass
        else:
            # ExperimentalValue._find_covariance(x, y)
            return 0

        sigma_xy = x.covariance[y.info['ID']]
        sigma_x = x.std
        sigma_y = y.std
        
        factor = sigma_xy
        factor /= (x.std*y.std)

        return factor

###############################################################################
# Methods for Naming and Units
###############################################################################

    def rename(self, name=None, units=None):
        '''Used to rename an object or change its units.

        :param name: The new name of the Measurement.
        :type name: str
        :param units: The new units of the Measurement.
        :type units: str

        .. code-block:: python

           mass = q.Measurement(4, 0.5, units='kg')
           mass.rename(units='g', name='mass')

           print(mass)
        .. nboutput:: ipython3

           mass = 4.0 +/- 0.5 [g]
        '''
        if name is not None:
            self.name = name
            self.user_name = True

        if units is not None:
            self.units = units

    def _update_info(self, operation, *args, func_flag=False):
        '''
        Update the formula, name and method of an object.

        Function to update the name, formula and method of a value created
        by a measurement operation. The name is updated by combining the
        names of the object acted on with another name using whatever
        operation is being performed. The function flag is to change syntax
        for functions like sine and cosine. Method is updated by acessing
        the class property.

        :param operation: A mathematical operation on 1 or 2 Measurement objects.
        :type operation: Function
        :param *args: The 1 or 2 Measurement objects being operated upon.
        :type *args: tuple
        :param func_flag: Whether the operation is a mathematical function.
            (ie. sqrt, power, sin... are functions but +,-,**... are not).
        :type func_flag: bool
        '''
        import qexpy.error_operations as op

        if len(args) is 1:
            var1 = args[0]
            var2 = None
        elif len(args) is 2:
            var1 = args[0]
            var2 = args[1]

        op_string = {op.sin: 'sin', op.cos: 'cos', op.tan: 'tan', op.sqrt: 'sqrt',
                     op.csc: 'csc', op.sec: 'sec', op.cot: 'cot',
                     op.exp: 'exp', op.log: 'log', op.add: '+',
                     op.sub: '-', op.mul: '*', op.div: '/', op.power: '**',
                     'neg': '-', op.asin: 'asin', op.acos: 'acos',
                     op.atan: 'atan', }

        if func_flag == False and var2 is not None:
            self.rename(var1.name+op_string[operation]+var2.name)
            self.user_name = False
            self.info['Formula'] = '('+ var1.info['Formula'] + ')' + \
                op_string[operation] + '(' + var2.info['Formula'] + ')'
            self.info['Function']['variables'] += (var1, var2),
            self.info['Function']['operation'] += operation,
            ExperimentalValue.formula_register.update(
                {self.info["Formula"]: self.info["ID"]})
            self.info['Method'] += "Errors propagated by " +\
                self.error_method + ' method.\n'
            for root in var1.root:
                if root not in self.root:
                    self.root += (root,)
            for root in var2.root:
                if root not in self.root:
                    self.root += (root,)

            if var1.units == var2.units and op_string[operation] in ('+', '-'):
                self.units = var1.units

            elif op_string[operation] is '*':
                for key in var1.units:
                    self._units[key] = var1.units[key]
                for key in var2.units:
                    if key in var1.units:
                        self._units[key] = var1.units[key] + var2.units[key]
                        if self.units[key] == 0:
                            del self._units[key]
                    else:
                        self._units[key] = var2.units[key]

            elif op_string[operation] is '/':
                for key in var1.units:
                    self._units[key] = var1.units[key]
                for key in var2.units:
                    if key in var1.units:
                        self._units[key] = var1.units[key] - var2.units[key]
                        if self.units[key] == 0:
                            del self._units[key]
                    else:
                        self._units[key] = -var2.units[key]

        elif func_flag == True and var2 is None:
            self.rename(op_string[operation]+'('+var1.name+')')
            self.user_name = False
            self.info['Formula'] = op_string[operation] + '(' + \
                var1.info['Formula'] + ')'
            self.info['Function']['variables'] += (var1,),
            self.info['Function']['operation'] += operation,
            self.info['Method'] += "Errors propagated by " + \
                self.error_method + ' method.\n'
            ExperimentalValue.formula_register.update(
                {self.info["Formula"]: self.info["ID"]})
            for root in var1.root:
                if root not in self.root:
                    self.root += var1.root
            self.units = ''

        else:
            #TODO double check with Connor, but I think it was a bug above and we have to check == True
            # not is True, since 1 could also be True...
            print('Something went wrong in update_info')

###############################################################################
# Operations on measurement objects
###############################################################################

    ###########################################################################
    # ARITHMETIC OPERATIONS
    # Called whenever an operation (+, -, /, *, **) is encountered
    # Call operation_wrap() in error_operations.py
    ###########################################################################

    def __add__(self, other):
        '''Handles addition with Measurements.

        Ensures that the two objects can be added and then sends them to 
        error_operations.operation_wrap, which handles the addition and
        error propagation. Called with: Measurement+other 

        :param other: The object to be added.
        :type other: Measurement, constant, array

        :returns: The sum with propagated mean and error.
        :rtype: Measurement
        '''
        #TODO: is this the correct implementation??? or should ARRAy create an ndarray???
        import qexpy.error_operations as op
        if type(other) in ExperimentalValue.ARRAY:
            result = Measurement_Array(len(other))
            for i in range(result.size):
                result[i]=op.operation_wrap(op.add, self, other[i])
                #result.append(op.operation_wrap(op.add, self, value))
            return result
        elif type(self) in ExperimentalValue.CONSTANT and\
                type(other) in ExperimentalValue.CONSTANT:
            return other+self
        else:
            return op.operation_wrap(op.add, self, other)

    def __radd__(self, other):
        '''Handles addition with Measurements.

        Ensures that the two objects can be added and then sends them to 
        error_operations.operation_wrap, which handles the addition and
        error propagation. Called with: other+Measurement

        :param other: The object to be added.
        :type other: Measurement, constant, array

        :returns: The sum with propagated mean and error.
        :rtype: Measurement
        '''
        import qexpy.error_operations as op
        if type(other) in ExperimentalValue.ARRAY:
            result = Measurement_Array(len(other))
            for i in range(result.size):
                result[i]=op.operation_wrap(op.add, self, other[i])
                #result.append(op.operation_wrap(op.add, self, value))
            return result
        elif type(self) in ExperimentalValue.CONSTANT and\
                type(other) in ExperimentalValue.CONSTANT:
            return other+self
        else:
            return op.operation_wrap(op.add, self, other)

    def __mul__(self, other):
        '''Handles multiplication with Measurements.

        Ensures that the two objects can be multiplied and then sends them to 
        error_operations.operation_wrap, which handles the multiplication and
        error propagation. Called with: Measurement*other

        :param other: The object to be multiplied.
        :type other: Measurement, constant, array

        :returns: The product with propagated mean and error.
        :rtype: Measurement
        '''
        import qexpy.error_operations as op
        if type(other) in ExperimentalValue.ARRAY:
            result = Measurement_Array(len(other))
            for i in range(result.size):
                result[i]=op.operation_wrap(op.mul, self, other[i])
                #result.append(op.operation_wrap(op.mul, self, value))
            return result
        elif type(self) in ExperimentalValue.CONSTANT and\
                type(other) in ExperimentalValue.CONSTANT:
            return other*self
        else:
            return op.operation_wrap(op.mul, self, other)

    def __rmul__(self, other):
        '''Handles multiplication with Measurements.

        Ensures that the two objects can be multiplied and then sends them to 
        error_operations.operation_wrap, which handles the multiplication and
        error propagation. Called with: other*Measurement

        :param other: The object to be multiplied.
        :type other: Measurement, constant, array

        :returns: The product with propagated mean and error.
        :rtype: Measurement
        '''
        import qexpy.error_operations as op
        if type(other) in ExperimentalValue.ARRAY:
            result = Measurement_Array(len(other))
            for i in range(result.size):
                result[i]=op.operation_wrap(op.mul, self, other[i])
                #result.append(op.operation_wrap(op.mul, self, value))
            return result
        elif type(self) in ExperimentalValue.CONSTANT and\
                type(other) in ExperimentalValue.CONSTANT:
            return other*self
        else:
            return op.operation_wrap(op.mul, self, other)

    def __sub__(self, other):
        '''Handles subtraction with Measurements.

        Ensures that the object can be subtracted and then sends them to 
        error_operations.operation_wrap, which handles the subtraction and
        error propagation. Called with: Measurement-other

        :param other: The object to be subtracted.
        :type other: Measurement, constant, array

        :returns: The difference with propagated mean and error.
        :rtype: Measurement
        '''
        import qexpy.error_operations as op
        if type(other) in ExperimentalValue.ARRAY:
            result = Measurement_Array(len(other))
            for i in range(result.size):
                result[i]=op.operation_wrap(op.sub, self, other[i])
                #result.append(op.operation_wrap(op.sub, self, value))
            return result
        elif type(self) in ExperimentalValue.CONSTANT and\
                type(other) in ExperimentalValue.CONSTANT:
            return self-other
        else:
            return op.operation_wrap(op.sub, self, other)

    def __rsub__(self, other):
        '''Handles subtraction with Measurements.

        Ensures that the object can be subtracted and then sends them to 
        error_operations.operation_wrap, which handles the subtraction and
        error propagation. Called with: other-Measurement

        :param other: The object that the Measurement will be subtracted from.
        :type other: Measurement, constant, array

        :returns: The difference with propagated mean and error.
        :rtype: Measurement
        '''
        import qexpy.error_operations as op
        if type(other) in ExperimentalValue.ARRAY:
            result = Measurement_Array(len(other))
            for i in range(result.size):
                result[i]=op.operation_wrap(op.sub, other[i], self)
                #result.append(op.operation_wrap(op.sub, value, self))
            return result
        elif type(self) in ExperimentalValue.CONSTANT and\
                type(other) in ExperimentalValue.CONSTANT:
            return other-self
        else:
            return op.operation_wrap(op.sub, other, self)

    def __truediv__(self, other):
        '''Handles division with Measurements.

        Ensures that the object can be divided by and then sends them to 
        error_operations.operation_wrap, which handles the division and
        error propagation. Called with: Measurement/other

        :param other: The object that the Measurement will be divided by.
        :type other: Measurement, constant, array

        :returns: The quotient with propagated mean and error.
        :rtype: Measurement
        '''
        import qexpy.error_operations as op
        if type(other) in ExperimentalValue.ARRAY:
            result = Measurement_Array(len(other))
            for i in range(result.size):
                result[i]=op.operation_wrap(op.div, self, other[i])
                #result.append(op.operation_wrap(op.div, self, value))
            return result
        elif type(self) in ExperimentalValue.CONSTANT and\
                type(other) in ExperimentalValue.CONSTANT:
            return self/other
        else:
            return op.operation_wrap(op.div, self, other)

    def __rtruediv__(self, other):
        '''Handles division with Measurements.

        Ensures that the object can be divided by and then sends them to 
        error_operations.operation_wrap, which handles the division and
        error propagation. Called with: other/Measurement

        :param other: The object that the Measurement will divide.
        :type other: Measurement, constant, array

        :returns: The quotient with propagated mean and error.
        :rtype: Measurement
        '''
        import qexpy.error_operations as op
        if type(other) in ExperimentalValue.ARRAY:
            result = Measurement_Array(len(other))
            for i in range(result.size):
                result[i]=op.operation_wrap(op.div, self, other[i])
                #result.append(op.operation_wrap(op.div, value, self))
            return result
        elif type(self) in ExperimentalValue.CONSTANT and\
                type(other) in ExperimentalValue.CONSTANT:
            return other/self
        else:
            return op.operation_wrap(op.div, other, self)

    def __pow__(self, other):
        '''Handles exponentiation with Measurements.

        Ensures that the object can be raised to the power of the other
        and then sends them to error_operations.operation_wrap, which
        handles the exponentiation and error propagation. 
        Called with: Measurement**other

        :param other: The power that the Measurement will be raised to.
        :type other: Measurement, constant, array

        :returns: The result with propagated mean and error.
        :rtype: Measurement
        '''
        import qexpy.error_operations as op
        if type(other) in ExperimentalValue.ARRAY:
            result = Measurement_Array(len(other))
            for i in range(result.size):
                result[i]=op.operation_wrap(op.power, self, other[i])
                #result.append(op.operation_wrap(op.power, self, value))
            return result
        elif type(self) in ExperimentalValue.CONSTANT and\
                type(other) in ExperimentalValue.CONSTANT:
            return self**other
        else:
            return op.operation_wrap(op.power, self, other)

    def __rpow__(self, other):
        '''Handles exponentiation with Measurements.

        Ensures that the object can be raised to the power of the other
        and then sends them to error_operations.operation_wrap, which
        handles the exponentiation and error propagation.
        Called with: other**Measurement

        :param other: The object that will be raised to the power of the
            value of the Measurement.
        :type other: Measurement, constant, array

        :returns: The result with propagated mean and error.
        :rtype: Measurement
        '''
        import qexpy.error_operations as op
        if type(other) in ExperimentalValue.ARRAY:
            result = Measurement_Array(len(other))
            for i in range(result.size):
                result[i]=op.operation_wrap(op.power, self, other[i])
                #result.append(op.operation_wrap(op.power, value, self))
            return result
        elif type(self) in ExperimentalValue.CONSTANT and\
                type(other) in ExperimentalValue.CONSTANT:
            return other**self
        else:
            return op.operation_wrap(op.power, other, self)

    # Calls neg() in error_operations, which returns the negative of the value
    def __neg__(self):
        '''Returns the negative of a Measurement object.

        :returns: The negative of the Measurement.
        :rtype: Measurement
        '''
        import qexpy.error_operations as op
        return op.neg(self)

    # Returns the length of the ExperimentalValue
    def __len__(self):
        '''Returns the length of a the array used to create the Measurement object.

        :returns: The length of the data array used to create the Measurement.
        :rtype: int
        '''
        return self.info['Data'].size

    ###########################################################################
    # COMPARISON OPERATIONS
    # Called whenever a comparison (>, <, >=, ==, ...) is made
    # Makes the relevant comparison and return a boolean
    ###########################################################################
    def __eq__(self, other):
        '''Checks if two Measurements are the same.

        :returns: True if the means of the two Measurements are the same,
                  False otherwise.
        :rtype: bool
        '''
        if type(other) in ExperimentalValue.CONSTANT:
            return self.mean == other
        else:
            try:
                other.type
            except AttributeError:
                raise TypeError
            else:
                return self.mean == other.mean

    def __req__(self, other):
        '''Checks if two Measurements are the same.

        :returns: True if the means of the two Measurements are the same,
                  False otherwise.
        :rtype: bool
        '''
        if type(other) in ExperimentalValue.CONSTANT:
            return self.mean == other
        else:
            try:
                other.type
            except AttributeError:
                raise TypeError
            else:
                return self.mean == other.mean
            
    def __gt__(self, other):
        '''Checks if a Measurement is greater than another Measurement.

        :returns: True if the mean of the Measurement is greater than the mean
                  of the other Measurement, False otherwise.
        :rtype: bool
        '''
        if type(other) in ExperimentalValue.CONSTANT:
            return self.mean > other
        else:
            try:
                other.type
            except AttributeError:
                raise TypeError
            else:
                return self.mean > other.mean
            
    def __rgt__(self, other):
        '''Checks if a Measurement is less than another Measurement.

        :returns: True if the mean of the Measurement is less than the mean
                  of the other Measurement, False otherwise.
        :rtype: bool
        '''
        if type(other) in ExperimentalValue.CONSTANT:
            return self.mean < other
        else:
            try:
                other.type
            except AttributeError:
                raise TypeError
            else:
                return self.mean < other.mean 
            
    def __ge__(self, other):
        '''Checks if a Measurement is greater than or equal to another Measurement.

        :returns: True if the mean of the Measurement is greater than or equal to
                  the mean of the other Measurement, False otherwise.
        :rtype: bool
        '''
        if type(other) in ExperimentalValue.CONSTANT:
            return self.mean >=other
        else:
            try:
                other.type
            except AttributeError:
                raise TypeError
            else:
                return self.mean >= other.mean
            
    def __rge__(self, other):
        '''Checks if a Measurement is less than or equal to another Measurement.

        :returns: True if the mean of the Measurement is less than or equal to
                  the mean of the other Measurement, False otherwise.
        :rtype: bool
        '''
        if type(other) in ExperimentalValue.CONSTANT:
            return self.mean <= other
        else:
            try:
                other.type
            except AttributeError:
                raise TypeError
            else:
                return self.mean <= other.mean
            
    def __lt__(self, other):
        '''Checks if a Measurement is less than another Measurement.

        :returns: True if the mean of the Measurement is less than the mean
                  of the other Measurement, False otherwise.
        :rtype: bool
        '''
        if type(other) in ExperimentalValue.CONSTANT:
            return self.mean < other
        else:
            try:
                other.type
            except AttributeError:
                raise TypeError
            else:
                return self.mean < other.mean
            
    def __rlt__(self, other):
        '''Checks if a Measurement is greater than another Measurement.

        :returns: True if the mean of the Measurement is greater than the mean
                  of the other Measurement, False otherwise.
        :rtype: bool
        '''
        if type(other) in ExperimentalValue.CONSTANT:
            return self.mean > other
        else:
            try:
                other.type
            except AttributeError:
                raise TypeError
            else:
                return self.mean > other.mean
            
    def __le__(self, other):
        '''Checks if a Measurement is less than or equal to another Measurement.

        :returns: True if the mean of the Measurement is less than or equal to
                  the mean of the other Measurement, False otherwise.
        :rtype: bool
        '''
        if type(other) in ExperimentalValue.CONSTANT:
            return self.mean <= other
        else:
            try:
                other.type
            except AttributeError:
                raise TypeError
            else:
                return self.mean <= other.mean
            
    def __rle__(self, other):
        '''Checks if a Measurement is greater than or equal to another Measurement.

        :returns: True if the mean of the Measurement is greater than or equal to
                  the mean of the other Measurement, False otherwise.
        :rtype: bool
        '''
        if type(other) in ExperimentalValue.CONSTANT:
            return self.mean >= other
        else:
            try:
                other.type
            except AttributeError:
                raise TypeError
            else:
                return self.mean >= other.mean

    def sqrt(x):
        return sqrt(x)

    def log(x):
        return log(x)

    def exp(x):
        return exp(x)

    def e(x):
        return exp(x)

    def sin(x):
        return sin(x)

    def cos(x):
        return cos(x)

    def tan(x):
        return tan(x)

    def csc(x):
        return csc(x)

    def sec(x):
        return sec(x)

    def cot(x):
        return cot(x)

    def asin(x):
        return asin(x)

    def acos(x):
        return acos(x)

    def atan(x):
        return atan(x)

###############################################################################
# Miscellaneous Methods
###############################################################################

    def _check_der(self, b):
        '''
        Checks for a derivative with respect to b, else zero is defined as
        the derivative.

        Checks the existance of the derivative of an object in the
        dictionary of said object with respect to another variable, given
        the variable itself, then checking for the ID of said variable
        in the .derivative dictionary. If non exists, the deriviative is
        assumed to be zero.

        :param b: The variable to check the derivative with respect to.
        :type b: Measurement
        '''
        for key in b.derivative:
            if key in self.derivative:
                pass
            else:
                self.derivative[key] = 0


###############################################################################
# Measurement Sub-Classes
###############################################################################


class Measurement(ExperimentalValue):
    '''
    Subclass of ExperimentalValue, specified by the user and treated as variables
    or arguments of functions when propagating error. Stores the mean and
    standard deviation of an experimentally measured value.

    :param name: The name of the Measurement object.
    :type name: str
    :param units: The units of the Measurement.
    :type units: str
    '''
    id_number = 0

    def __init__(self, *args, name=None, units=None):
        super().__init__(*args, name=name)
        if name is not None:
            self.name = name
        else:
            self.name = 'unnamed_var%d' % (Measurement.id_number)

        if units is not None:
            if type(units) is str:
                self.units = _parse_units(units)
            elif type(units) is dict:
                self.units = units
            else:
                for i in range(len(units)//2):
                    self._units[units[2*i]] = units[2*i+1]

        self.type = "ExperimentalValue"
        self.info['ID'] = 'var%d' % (Measurement.id_number)
        self.info['Formula'] = 'var%d' % (Measurement.id_number)
        Measurement.id_number += 1
        self.derivative = {self.info['ID']: 1}
        self.covariance = {self.info['ID']: self.std**2}
        self.correlation = {self.info['ID']: 1}
        ExperimentalValue.register.update({self.info["ID"]: self})
        self.root = (self.info["ID"],)
        self.der = [self.mean, self.std]
        self.MC = [self.mean, self.std]
        self.MinMax = [self.mean, self.std]

class Function(ExperimentalValue):
    '''
    Subclass of ExperimentalValue, which are measurements created by operations or
    functions of other Measurement type objects. They contain the mean and propagated
    error of the function.
    '''
    id_number = 0

    def __init__(self, *args, name=None):
        super().__init__(*args, name=name)

        self.name = 'obj%d' % (Function.id_number)
        self.info['ID'] = 'obj%d' % (Function.id_number)
        self.type = "Function"
        Function.id_number += 1
        self.derivative = {self.info['ID']: 1}
        ExperimentalValue.register.update({self.info["ID"]: self})
        self.covariance = {self.info['ID']: self.std**2}
        self.correlation = {self.info['ID']: 1}
        self.root = ()
        self.der = None
        #These are set by super()
        #self.MC = None
        #self.MinMax = None
        self.error_flag = False


class Constant(ExperimentalValue):
    '''
    Subclass of ExperimentalValue, not neccesarily specified by the user,
    called when a consant (int, float, etc.) is used in operation with a
    measurement. This class is called before calculating operations to
    ensure objects can be combined. The mean of a constant is the specified
    value, the standard deviation is zero, and the derivarive with respect
    to anything is zero.
    '''
    def __init__(self, arg, name=None, units=None):
        super().__init__(arg, 0)

        if name is not None:
            self.name = name
        else:
            self.name = '%d' % (arg)

        if units is not None:
            if type(units) is str:
                self._units[units] = 1
            else:
                for i in range(len(units)//2):
                    self._units[units[2*i]] = units[2*i+1]

        self.info['ID'] = 'Constant'
        self.info["Formula"] = '%f' % arg
        self.derivative = {}
        self.info["Data"] = np.array([arg])
        self.type = "Constant"
        self.covariance = {self.name: 0}
        self.root = ()


class Measurement_Array(np.ndarray):
    '''A numpy-based array of Measurement objects. It is better to call
       MeasurementArray() and let it create a Measurement_Array object
       behind the scenes.'''
    id_number = 0
    error_method = "Derivative"
    def __new__(subtype, shape, dtype=Measurement, buffer=None, offset=0,
          strides=None, order=None, name = None, units = None, error_method='Derivative'):
        obj = np.ndarray.__new__(subtype, shape, dtype, buffer, offset, strides,
                         order)
        if name is not None:
            obj.name = name
        else:
            obj.name = 'unnamed_arr%d' % (Measurement_Array.id_number)

        obj.error_method = error_method

        obj._units = {}
        if units is not None:
            obj.units = units

        Measurement_Array.id_number += 1
        
        return obj

    def __array_finalize__(self, obj):
        '''Sets the name and units of the MeasurementArray during creation.
        '''
        if obj is None: return
        self.units = getattr(obj, 'units', None)
        self.name = getattr(obj, 'name', None)
        
    def __array_wrap__(self, out_arr, context=None):
        '''Used to make sure that numpy functions work on MeasurementArrays
        and they return MeasurementArrays.
        '''
        # then just call the parent
        return np.ndarray.__array_wrap__(self, out_arr, context)

    def append(self, meas):
        '''Appends a value to the end of a MeasurementArray.

        :param meas: A measurement to be appended to the MeasurementArray.
        :type meas: Measurement, constant

        :returns: A MeasurementArray with the new value added to the end.
        :rtype: Measurement_Array

        .. code-block:: python

           import qexpy as q

           x = q.MeasurementArray([12, 10], error=0.5)
           y = q.Measurement(11, 0.2)
           x = x.append(y)

           print(x)

        .. nboutput:: ipython3

           12.0 +/- 0.5,
           10.0 +/- 0.5,
           11.0 +/- 0.2
        '''
        data_name = "{}_{}".format(self.name,len(self))
        if type(meas) in ExperimentalValue.CONSTANT:
            meas = Constant(meas)
        if isinstance(meas, ExperimentalValue):
            if self[0].user_name:
                meas.rename(name=data_name)
            meas.units = self.units
            return self.__array_wrap__(np.append(self, meas))
        else:
            print("Object to append must be a Measurement or a number")
            return self

    def insert(self, pos, meas):
        '''Inserts a value in a position in a MeasurementArray.

        :param pos: The 0-indexed position in the MeasurementArray that the
            measurement will be inserted into. Must be between 0 and the
            length of the MeasurementArray.
        :type pos: int
        :param meas: A measurement to be inserted into the MeasurementArray.
        :type meas: Measurement, constant

        :returns: A MeasurementArray with the new value inserted into the position
                  specified by the pos parameter.
        :rtype: Measurement_Array

        .. code-block:: python

           import qexpy as q

           x = q.MeasurementArray([12, 11], error=0.5)
           y = q.Measurement(10, 0.2)
           x = x.insert(1, y)

           print(x)

        .. nboutput:: ipython3

           12.0 +/- 0.5,
           10.0 +/- 0.2
           11.0 +/- 0.5
        '''
        if pos not in range(len(self)):
            print("Index for inserting out of bounds.")
            return self
        data_name = "{}_{}".format(self.name,len(self))
        if type(meas) in ExperimentalValue.CONSTANT:
            meas = Constant(meas)
        if isinstance(meas, ExperimentalValue):
            if (self[0] and self[0].user_name):
                meas.rename(name=data_name)
            meas.units = self.units
            result = np.insert(self, pos, meas)
            for index in range(len(result)):
                result[index].rename(name="{}_{}".format(self.name,index))
            return self.__array_wrap__(result)
        else:
            print("Object to insert must be a Measurement or a number")
            return self

    def delete(self, pos):
        '''Removes an element from a MeasurementArray.

        :param pos: The 0-indexed position in the MeasurementArray of the
            Measurement that will be deleted. Must be between 0 and the
            length of the MeasurementArray.
        :type pos: int

        :returns: A MeasurementArray with the Measurement at the specified position
                  removed.
        :rtype: Measurement_Array

        .. code-block:: python

           import qexpy as q

           x = q.MeasurementArray([12, 10, 11], error=0.5)
           x = x.delete(1)

           print(x)

        .. nboutput:: ipython3

           12.0 +/- 0.5,
           11.0 +/- 0.5
        '''
        if pos not in range(len(self)):
            print("Index for deleting out of bounds.")
            return self
        result = np.delete(self, pos)
        for index in range(len(result)):
            result[index].name = "{}_{}".format(self.name,index)
        return self.__array_wrap__(result)

    def show_table(self, latex=False):
        '''Prints the data of the Measurement_Array in a formatted table.

        :param latex: Whether to print the data using Latex formatting.
        :type show: bool

        .. code-block:: python

            import qexpy as q

            mass = q.MeasurementArray([4.2, 4.1, 4.3], error=0.5, name='mass')

            mass.show_table()

        .. nboutput:: ipython3

                    mass
             4.2 +/- 0.5
             4.1 +/- 0.5
             4.3 +/- 0.5
        '''

        x = self.means
        stds = self.stds

        if latex:
            s = ('\\begin{table}[htp]\n'
                 '\\begin{center}\n'
                 '\\begin{tabular}{|c|} \hline\n')
            err_const = np.all(stds == stds[0])
            xtitle = '{\\bf '+self.name+'} ('+(self.get_units_str() if self.get_units_str() else 'units')+')'
            xtitle += ' $\pm$ ' + str(stds[0]) if err_const else ''
            s += xtitle + ' \\\\ \hline\hline \n'
            for ind in range(len(x)):
                s += str(x[ind]) + ('' if err_const else ' $\pm$ '+str(stds[ind])) + ' \\\\ \hline \n'
            s += ('\end{tabular}\n'
                  '\end{center}\n'
                  '\caption{} \n'
                  '\end{table}')
            print(s)

        else:
            data = []
            for ind in range(len(x)):
                data.append([str(x[ind]) + ' +/- ' + str(stds[ind])])
            df = pd.DataFrame(data, columns=[self.name], index=['']*len(x))
            print(df)

    @property
    def means(self):
        '''Returns a numpy array with the means of each value in the MeasurementArray,
        as calculated by the method (der, MC, MinMax).

        :getter: Returns a numpy array of the means of the Measurements
                 in the MeasurementArray.
        :type: numpy.ndarray

        .. code-block:: python

            x = q.MeasurementArray([11, 12, 13], error=0.5)

            print(x.means)

        .. nboutput:: ipython3

           [ 11.  12.  13.]
        '''
        if self.size == 0:
            return np.ndarray(0)

        means = np.ndarray(shape=self.shape)

        for index, item in np.ndenumerate(self):
            if item is not None and isinstance(item, ExperimentalValue):
                if self.error_method == "MinMax":
                    means[index] = item.MinMax[0]
                elif self.error_method == "MC":
                    means[index] = item.MC[0]
                else:
                    means[index] = item.mean
            else:
                means[index] = 0
        return means

    @property
    def stds(self):
        '''Returns a numpy array with the standard deviations of each value
        in the MeasurementArray, as calculated by the method (der, MC, MinMax).

        :setter: Sets the errors of the Measurement_Array to either a single
                 value if a number is provided, or to  different errors if a
                 list is provided.
        :getter: Returns a numpy array of the standard deviations of 
                 the Measurements in the MeasurementArray.
        :type: numpy.ndarray

        .. code-block:: python

            x = q.MeasurementArray([11, 12, 13], error=[0.1, 0.1, 0.8])

            print(x.stds)

        .. nboutput:: ipython3

           [ 0.1  0.1  0.8]
        '''
        if self.size == 0:
            return np.ndarray(0)
        
        stds = np.ndarray(shape=self.shape)
        
        for index, item in np.ndenumerate(self):
            if item is not None:              
                if self.error_method == "MinMax":
                    stds[index] = 9
                elif self.error_method == "MC":
                    stds[index] = item.MC[1]
                else:
                    stds[index] = item.std
            else:
                stds[index] = 0
        return stds

    @stds.setter
    def stds(self, error):
        '''Sets the standard deviations of each value in the MeasurementArray,
        either to the same value for all Measurements or to a different value
        for each Measurement.
        '''
        n = self.size

        if isinstance(error, qu.number_types):#MA([,,,,], error = x)
            for i in range(n):
                self[i].std=error

        elif isinstance(error, qu.array_types):#MA([,,,,], error = [])
            if len(error)==n:#MA([,,,,], error = [,,,,])
                for i in range(n):
                    self[i].std=error[i]

            elif len(error)==1: #MA([,,,,], error = [x])
                for i in range(n):
                    self[i].std=error[0]
            else:
                print("Error list must be the same length as the original list")

    @property
    def mean(self):
        '''Gets the mean of the means of the Measurements in the MeasurementArray.

        :getter: Returns the mean of the means of all the Measurements in the
                 MeasurementArray.
        :type: numpy.ndarray

        .. code-block:: python

            x = q.MeasurementArray([11, 12, 13], error=0.5)

            print(x.mean)

        .. nboutput:: ipython3

           12.0
        '''
        nparr = self.means
        self._mean = nparr.mean()
        return self._mean

    @property
    def error_weighted_mean(self):
        '''Gets the error weighted mean and error of a MeasurementArray.
        The error weighted mean gives more weight to more precise Measurements.

        :getter: Returns the weighted mean of all the Measurements in the
                 MeasurementArray.
        :type: tuple

        .. code-block:: python

            x = q.MeasurementArray([11, 12, 13], error=[0.1, 0.1, 0.8])

            # Will be less than 12, since 11 +/- 0.1 is more precise than 13 +/- 0.8
            print(x.error_weighted_mean)

        .. nboutput:: ipython3

           11.51 +/- 0.07
        '''
        means = self.means
        stds = self.stds
        stds2 = stds**2
        sumw2=0
        mean=0

        #Needs to be a loop to catch the case of std == 0
        for i in range(means.size):
            if stds[i] == 0.0:
                continue
            w2 = 1./stds2[i]
            mean += w2*means[i]
            sumw2 += w2

        self._error_weighted_mean =  (0. if sumw2==0 else mean/sumw2)
        self._error_weighted_std =  (0. if sumw2==0 else np.sqrt(1./sumw2))

        return Measurement(self._error_weighted_mean, self._error_weighted_std)
    
    def std(self, ddof=1):
        '''Calculates the standard deviation of the means of 
        all the Measurements in the MeasurementArray.

        :param ddof: The degrees of freedom of the MeasurementArray.
        :type ddof: int

        :returns: The standard deviation of the means of all the 
                  Measurements in the MeasurementArray. 
        :rtype: numpy.ndarray

        .. code-block:: python

            x = q.MeasurementArray([11, 12, 13], error=[0.1, 0.1, 0.8])

            print(x.std(ddof=1))

        .. nboutput:: ipython3

           1.0
        '''
        nparr = self.means
        return nparr.std(ddof=ddof)

    @property
    def units(self):
        '''The units of a Measurement_Array.

        :setter: Sets the units of the Measurement object.
        :getter: Returns the name of the Measurement object.
        :type: str

        .. code-block:: python

            x = q.MeasurementArray(5, 0.2, name='length')

            x.units = 'm'
            print(x)

        .. nboutput:: ipython3

           11.0 +/- 0.1 [m],
           12.0 +/- 0.1 [m],
           13.0 +/- 0.1 [m]
        '''
        return self._units

    @units.setter
    def units(self, units):
        '''Sets the units of a Measurement_Array.
        '''
        if units is not None:
            if type(units) is str:
                self._units = _parse_units(units)
            elif type(units) is dict:
                self._units = units
            else:
                for i in range(len(units)//2):
                    self._units[units[2*i]] = units[2*i+1]
            for mes in self:
                if mes:
                    if type(units) is str:
                        mes._units = _parse_units(units)
                    elif type(units) is dict:
                        mes._units = units
                    else:
                        for i in range(len(units)//2):
                            mes._units[units[2*i]] = units[2*i+1]
    
    def get_units_str(self):
        '''Generates a string representation of the units.

        :returns: A string representation of the units of the MeasurementArray.
        :rtype: str

        .. code-block:: python

            import qexpy as q

            mass = q.MeasurementArray([4.2, 4.1, 4.3], error=0.5, units='kg')
            accel = q.MeasurementArray([9.8, 9.9, 9.5], error=0.1, units='m/s^2')
            force = mass*accel

            print(force.get_units_str())

        .. nboutput:: ipython3

           kg^1 s^-2 m^1
        '''
        
        return self[0].get_units_str()
    
    def __str__(self):
        '''Returns a string representation of the MeasurementArray.

        :returns: A representation of the MeasurementArray.
        :rtype: str
        '''
        theString=''
        for i in range(self.size):
            theString += self[i].__str__()
            if i != self.size-1:
                theString += ',\n'
        return theString
            
def MeasurementArray(data, error=None, name=None, units=None, error_method='Derivative'):
    '''Function to easily construct a Measurement_Array object. It is better to call this
    function and let it create the Measurement_Array object behind the scenes.

    :param error: Either a single value representing the error on all
        the Measurements in the MeasurementArray, or a list of errors 
        corresponding to all the Measurements in the MeasurementArray.
    :type error: constant, array
    :param name: A string of the name of the MeasurementArray.
    :type name: str
    :param units: A string of the units of the MeasurementArray.
    :type units: str
    :param error_method: Which method to use when propagating errors through
                         calculations on the MeasurementArray. 
                         Can be 'Monte Carlo', 'Min Max' or'Derivative'
    :type error_method: str

    :returns: A Measurement_Array object.
    :rtype: Measurement_Array
    '''

    array = Measurement_Array(0, name=name, units=units, error_method=error_method)
    user_name= True if name != None else False
        
    if error is None: #MA(data)
        if isinstance(data, Measurement_Array):# MA(MA)
            #TODO should this be a copy?? This will just 
            #make array be a reference to data...
            array = data
            array.units = units
            #allow the name to be updated:
            if name is not None:
                array.name = name  
                
        elif isinstance(data, qu.array_types): #MA([...])
            n = len(data)       
            array.resize(n)
            if isinstance(data[0], qu.array_types) and len (data[0]) == 2: #MA([ (,), (,), (,)])
                for i in range(n):
                    data_name = "{}_{}".format(array.name,i)
                    array[i]=Measurement(data[i][0],data[i][1], units=units, name=data_name)
                    array[i].user_name=user_name
                    
            elif isinstance(data[0], qu.number_types): #MA([,,,])
                for i in range(n):
                    data_name = "{}_{}".format(array.name,i)
                    array[i]=Measurement(float(data[i]),0., units=units, name=data_name)
                    array[i].user_name=user_name
            elif isinstance(data[0], ExperimentalValue): #MA([Measurement,Measurement,Measurement,])
                for i in range(n):
                    data[i].rename(units=units)
                    array[i]=data[i]
                    array[i].user_name=user_name
            else:
                print("unsupported type for data")
             
        elif isinstance(data, qu.int_types): #MA(n)
            array.resize(data)
            for i in range(data):
                data_name = "{}_{}".format(array.name,i)
                array[i]=Measurement(0.,0., units=units, name=data_name)
                array[i].user_name=user_name
        else:
            print("unsupported type for data")
            
    else: #error is not None
        if isinstance(data, Measurement_Array):
            array = data
            array.stds = error
            array.units = units
            #allow the name to be updated:
            if name is not None:
                array.name = name

            
        elif isinstance(data, qu.array_types): #MA([], error = ...)
            n = len(data)       
            array.resize(n)
            
            if isinstance(data[0], qu.number_types):#MA([,,,,], error = ...)
                
                if isinstance(error, qu.number_types):#MA([,,,,], error = x)
                    for i in range(n):
                        data_name = "{}_{}".format(array.name,i)
                        array[i]=Measurement(float(data[i]),error, units=units, name=data_name)
                        array[i].user_name=user_name
                elif isinstance(error, qu.array_types):#MA([,,,,], error = [])
                    if len(error)==len(data):#MA([,,,,], error = [,,,,])
                        for i in range(n):
                            data_name = "{}_{}".format(array.name,i)
                            array[i]=Measurement(float(data[i]),error[i], units=units, name=data_name)    
                            array[i].user_name=user_name
                    elif len(error)==1: #MA([,,,,], error = [x])
                        for i in range(n):
                            data_name = "{}_{}".format(array.name,i)
                            array[i]=Measurement(float(data[i]),error[0], units=units, name=data_name)
                            array[i].user_name=user_name
                    else:
                        print("error array must be same length as data")
                else:
                    print("unsupported type for error")
                    
            else: # data[0] must be a float
                print("unsupported type for data:", type(data[0]))
                
        elif isinstance(data, qu.number_types): #MA(x,error=...)
            array.resize(1)
            if isinstance(error, qu.number_types):#MA(x, error = y)
                data_name = "{}_{}".format(array.name,0)
                array[0]=Measurement(float(data),error, units=units, name=data_name)
                array[0].user_name=user_name
            elif isinstance(error, qu.array_types) and len(error)==1:#MA(x, error = [u])
                data_name = "{}_{}".format(array.name,0)
                array[0]=Measurement(float(data),error[0], units=units, name=data_name)
                array[0].user_name=user_name
            else:
                print("unsupported type for error")
        else:
            print("unsupported type of data")
        
    return array        
        
        

###############################################################################
# Mathematical Functions
# These are called for functions in the form: error.func(ExperimentalValue)
# They call operation_wrap() in the error_operations.py file
###############################################################################

ExperimentalValue.ARRAY = ExperimentalValue.ARRAY +(Measurement_Array,)

def sqrt(x):
    import qexpy.error_operations as op      
    if type(x) in ExperimentalValue.ARRAY:
        if len(x) <1:
            return []
        if isinstance(x[0],ExperimentalValue):
            result = Measurement_Array(len(x))
            for index in range(len(x)):
                result[index]=op.operation_wrap(op.sqrt, x[index], func_flag=True)
        else:
            result = np.ndarray(len(x), dtype=type(x[0]))
            for index in range(len(x)):
                result[index]=op.operation_wrap(op.sqrt, x[index], func_flag=True)  
        return result
    else:
        return op.operation_wrap(op.sqrt, x, func_flag=True)

def sin(x):
    import qexpy.error_operations as op
    if type(x) in ExperimentalValue.ARRAY:
        if len(x) <1:
            return np.ndarray(0, dtype=float)
        if isinstance(x[0],ExperimentalValue):
            result = Measurement_Array(len(x))
            for index in range(len(x)):
                result[index]=op.operation_wrap(op.sin, x[index], func_flag=True)
        else:
            result = np.ndarray(len(x), dtype=type(x[0]))
            for index in range(len(x)):
                result[index]=op.operation_wrap(op.sin, x[index], func_flag=True)    
        #result = []      
        #for value in x:
            #result.append(op.operation_wrap(op.sin, value, func_flag=True))
        
        return result
    elif type(x) in ExperimentalValue.CONSTANT:
        import math as m
        return m.sin(x)
    else:
        return op.operation_wrap(op.sin, x, func_flag=True)


def cos(x):
    import qexpy.error_operations as op
    if type(x) in ExperimentalValue.ARRAY:
          
        if len(x) <1:
            return []
        if isinstance(x[0],ExperimentalValue):
            result = Measurement_Array(len(x))
            for index in range(len(x)):
                result[index]=op.operation_wrap(op.cos, x[index], func_flag=True)
        else:
            result = np.ndarray(len(x), dtype=type(x[0]))
            for index in range(len(x)):
                result[index]=op.operation_wrap(op.cos, x[index], func_flag=True)
        #result = []
        #for value in x:
        #    result.append(op.operation_wrap(op.cos, value, func_flag=True))
        return result
    elif type(x) in ExperimentalValue.CONSTANT:
        import math as m
        return m.cos(x)
    else:
        return op.operation_wrap(op.cos, x, func_flag=True)


def tan(x):
    import qexpy.error_operations as op
    if type(x) in ExperimentalValue.ARRAY:
        if len(x) <1:
            return []
        if isinstance(x[0],ExperimentalValue):
            result = Measurement_Array(len(x))
            for index in range(len(x)):
                result[index]=op.operation_wrap(op.tan, x[index], func_flag=True)
        else:
            result = np.ndarray(len(x), dtype=type(x[0]))
            for index in range(len(x)):
                result[index]=op.operation_wrap(op.tan, x[index], func_flag=True)
        #result = []
        #for value in x:
        #    result.append(op.operation_wrap(op.tan, value, func_flag=True))
        return result
    elif type(x) in ExperimentalValue.CONSTANT:
        import math as m
        return m.tan(x)
    else:
        return op.operation_wrap(op.tan, x, func_flag=True)


def sec(x):
    import qexpy.error_operations as op
    if type(x) in ExperimentalValue.ARRAY:
        if len(x) <1:
            return []
        if isinstance(x[0],ExperimentalValue):
            result = Measurement_Array(len(x))
            for index in range(len(x)):
                result[index]=op.operation_wrap(op.sec, x[index], func_flag=True)
        else:
            result = np.ndarray(len(x), dtype=type(x[0]))
            for index in range(len(x)):
                result[index]=op.operation_wrap(op.sec, x[index], func_flag=True)
        #result = []
        #for value in x:
        #    result.append(op.operation_wrap(op.sec, value, func_flag=True))
        return result
    elif type(x) in ExperimentalValue.CONSTANT:
        import math as m
        return 1/m.cos(x)
    else:
        return op.operation_wrap(op.sec, x, func_flag=True)


def csc(x):
    import qexpy.error_operations as op
    if type(x) in ExperimentalValue.ARRAY:
        if len(x) <1:
            return []
        if isinstance(x[0],ExperimentalValue):
            result = Measurement_Array(len(x))
            for index in range(len(x)):
                result[index]=op.operation_wrap(op.csc, x[index], func_flag=True)
        else:
            result = np.ndarray(len(x), dtype=type(x[0]))
            for index in range(len(x)):
                result[index]=op.operation_wrap(op.csc, x[index], func_flag=True)
        #result = []
        #for value in x:
        #    result.append(op.operation_wrap(op.csc, value, func_flag=True))
        return result
    elif type(x) in ExperimentalValue.CONSTANT:
        import math as m
        return 1/m.sin(x)
    else:
        return op.operation_wrap(op.csc, x, func_flag=True)


def cot(x):
    import qexpy.error_operations as op
    if type(x) in ExperimentalValue.ARRAY:
        if len(x) <1:
            return []
        if isinstance(x[0],ExperimentalValue):
            result = Measurement_Array(len(x))
            for index in range(len(x)):
                result[index]=op.operation_wrap(op.cot, x[index], func_flag=True)
        else:
            result = np.ndarray(len(x), dtype=type(x[0]))
            for index in range(len(x)):
                result[index]=op.operation_wrap(op.cot, x[index], func_flag=True)
        #result = []
        #for value in x:
        #    result.append(op.operation_wrap(op.cot, value, func_flag=True))
        return result
    elif type(x) in ExperimentalValue.CONSTANT:
        import math as m
        return 1/m.tan(x)
    else:
        return op.operation_wrap(op.cot, x, func_flag=True)


def log(x):
    import qexpy.error_operations as op
    if type(x) in ExperimentalValue.ARRAY:
        if len(x) <1:
            return []
        if isinstance(x[0],ExperimentalValue):
            result = Measurement_Array(len(x))
            for index in range(len(x)):
                result[index]=op.operation_wrap(op.log, x[index], func_flag=True)
        else:
            result = np.ndarray(len(x), dtype=type(x[0]))
            for index in range(len(x)):
                result[index]=op.operation_wrap(op.log, x[index], func_flag=True)
        #result = []
        #for value in x:
        #    result.append(op.operation_wrap(op.log, value, func_flag=True))
        return result
    elif type(x) in ExperimentalValue.CONSTANT:
        import math as m
        return m.log(x)
    else:
        return op.operation_wrap(op.log, x, func_flag=True)


def exp(x):
    import qexpy.error_operations as op
    if type(x) in ExperimentalValue.ARRAY:
        if len(x) <1:
            return []
        if isinstance(x[0],ExperimentalValue):
            result = Measurement_Array(len(x))
            for index in range(len(x)):
                result[index]=op.operation_wrap(op.exp, x[index], func_flag=True)
        else:
            result = np.ndarray(len(x), dtype=type(x[0]))
            for index in range(len(x)):
                result[index]=op.operation_wrap(op.exp, x[index], func_flag=True)
        #result = []
        #for value in x:
        #    result.append(op.operation_wrap(op.exp, value, func_flag=True))
        return result
    elif type(x) in ExperimentalValue.CONSTANT:
        import math as m
        return m.exp(x)
    else:
        return op.operation_wrap(op.exp, x, func_flag=True)


def e(x):
    import qexpy.error_operations as op
    if type(x) in ExperimentalValue.ARRAY:
        if len(x) <1:
            return []
        if isinstance(x[0],ExperimentalValue):
            result = Measurement_Array(len(x))
            for index in range(len(x)):
                result[index]=op.operation_wrap(op.exp, x[index], func_flag=True)
        else:
            result = np.ndarray(len(x), dtype=type(x[0]))
            for index in range(len(x)):
                result[index]=op.operation_wrap(op.exp, x[index], func_flag=True)
        #result = []
        #for value in x:
        #    result.append(op.operation_wrap(op.exp, value, func_flag=True))
        return result
    elif type(x) in ExperimentalValue.CONSTANT:
        import math as m
        return m.exp(x)
    else:
        return op.operation_wrap(op.exp, x, func_flag=True)


def asin(x):
    import qexpy.error_operations as op
    if type(x) in ExperimentalValue.ARRAY:
        if len(x) <1:
            return []
        if isinstance(x[0],ExperimentalValue):
            result = Measurement_Array(len(x))
            for index in range(len(x)):
                result[index]=op.operation_wrap(op.asin, x[index], func_flag=True)
        else:
            result = np.ndarray(len(x), dtype=type(x[0]))
            for index in range(len(x)):
                result[index]=op.operation_wrap(op.asin, x[index], func_flag=True)
        #result = []
        #for value in x:
        #    result.append(op.operation_wrap(op.asin, value, func_flag=True))
        return result
    elif type(x) in ExperimentalValue.CONSTANT:
        import math as m
        return m.asin(x)
    else:
        return op.operation_wrap(op.asin, x, func_flag=True)


def acos(x):
    import qexpy.error_operations as op
    if type(x) in ExperimentalValue.ARRAY:
        if len(x) <1:
            return []
        if isinstance(x[0],ExperimentalValue):
            result = Measurement_Array(len(x))
            for index in range(len(x)):
                result[index]=op.operation_wrap(op.acos, x[index], func_flag=True)
        else:
            result = np.ndarray(len(x), dtype=type(x[0]))
            for index in range(len(x)):
                result[index]=op.operation_wrap(op.acos, x[index], func_flag=True)
        #result = []
        #for value in x:
        #    result.append(op.operation_wrap(op.acos, value, func_flag=True))
        return result
    elif type(x) in ExperimentalValue.CONSTANT:
        import math as m
        return m.acos(x)
    else:
        return op.operation_wrap(op.acos, x, func_flag=True)


def atan(x):
    import qexpy.error_operations as op
    if type(x) in ExperimentalValue.ARRAY:
        if len(x) <1:
            return []
        if isinstance(x[0],ExperimentalValue):
            result = Measurement_Array(len(x))
            for index in range(len(x)):
                result[index]=op.operation_wrap(op.atan, x[index], func_flag=True)
        else:
            result = np.ndarray(len(x), dtype=type(x[0]))
            for index in range(len(x)):
                result[index]=op.operation_wrap(op.atan, x[index], func_flag=True)
        #result = []
        #for value in x:
        #    result.append(op.operation_wrap(op.atan, value, func_flag=True))
        return result
    elif type(x) in ExperimentalValue.CONSTANT:
        import math as m
        return m.atan(x)
    else:
        return op.operation_wrap(op.atan, x, func_flag=True)


###############################################################################
# Printing Methods
###############################################################################


def set_print_style(style=None, sigfigs=None):
    '''Change style ("default","latex","scientific") of printout for
    Measurement objects.

    The default style prints as the user might write a value, that is
    'x = 10 +/- 1'.

    Latex style prints in the form of 'x = (10\pm 1)\e0' which is ideal for
    pasting values into a Latex document as will be the case for lab reports.

    The scientific style prints the value in reduced scientific notation
    such that the error is a single digit, 'x = (10 +/- 1)*10**0'.

    :param style: The style that the Measurement objects are printed with.
                  Can be 'Latex', 'Scientific' or 'Default'.
    :type style: str
    :param sigfigs: The number of significant figures to use when printing.
    :type sigfigs: int
    '''
    latex = ("Latex", "latex", 'Tex', 'tex',)
    Sci = ("Scientific", "Sci", 'scientific', 'sci', 'sigfigs',)
    Default = ('default', 'Default',)

    if sigfigs is not None:
        set_sigfigs(sigfigs)
        ExperimentalValue.figs_on_uncertainty = False

    if style in latex:
        ExperimentalValue.print_style = "Latex"
    elif style in Sci:
        ExperimentalValue.print_style = "Scientific"
    elif style in Default:
        ExperimentalValue.print_style = "Default"
    else:
        print('''A style must be a string of either: Scientific notation,
        Latex, or the default style. Using default.''')
        ExperimentalValue.print_style = "Default"


def set_error_method(chosen_method):
    '''Choose the method of error propagation to be used. Enter a string.

    Function to change default error propogation method used in
    measurement functions.

    :param chosen_method: The method to be used to propagate errors.
                          Can be 'Monte Carlo', 'Min Max' or 'Default'.
    :type chosen_method: str
    '''
    mc_list = (
        'MC', 'mc', 'montecarlo', 'Monte Carlo', 'MonteCarlo',
        'monte carlo',)
    min_max_list = ('Min Max', 'MinMax', 'minmax', 'min max',)
    derr_list = ('Derivative', 'derivative', 'diff', 'der', 'Default',
                 'default',)

    if chosen_method in mc_list:
        ExperimentalValue.error_method = "Monte Carlo"
    elif chosen_method in min_max_list:
        ExperimentalValue.error_method = "Min Max"
    elif chosen_method in derr_list:
        ExperimentalValue.error_method = "Derivative"
    else:
        print("Method not recognized, using derivative method.")
        ExperimentalValue.error_method = "Derivative"


def set_sigfigs_error(sigfigs=3):
    '''Change the number of significant figures shown in print()
    based on the number of sig figs in the error.

    :param sigfigs: The number of significant figures to use when printing.
    :type sigfigs: int
    '''
    if type(sigfigs) is None:
        ExperimentalValue.figs = None
    elif type(sigfigs) is int and sigfigs > 0:
        ExperimentalValue.figs = sigfigs
        ExperimentalValue.figs_on_uncertainty = True
    else:
        raise TypeError('''Specified number of significant figures must be
                        and interger greater than zero.''')


def set_sigfigs_centralvalue(sigfigs=3):
    '''Change the number of significant figures shown in print()
    based on the number of sig figs in the central value.

    :param sigfigs: The number of significant figures to use when printing.
    :type sigfigs: int
    '''
    if type(sigfigs) is None:
        ExperimentalValue.figs = None
    elif sigfigs > 0 and type(sigfigs) is int:
        ExperimentalValue.figs = sigfigs
        ExperimentalValue.figs_on_uncertainty = False
    else:
        raise TypeError('''Specified number of significant figures must be
                        an interger greater than zero.''')


def set_sigfigs(sigfigs=3):
    '''Change the number of significant figures shown in print()
    based on the number of sig figs in the error.

    :param sigfigs: The number of significant figures to use when printing.
    :type sigfigs: int
    '''
    set_sigfigs_error(sigfigs)


def _return_exponent(value):
    '''Calculates the exponent of the argument in reduced scientific notation.

    :param value: The value to get the exponent for.
    :type value: float

    :returns: The exponent of the order of magnitude of the value.
    :rtype: int
    '''
    value = abs(value)
    flag = True
    i = 0

    while(flag):
        if value == 0:
            flag = False
        elif value == float('inf'):
            return float("inf")
        elif value < 1:
            value *= 10
            i -= 1
        elif value >= 10:
            value /= 10
            i += 1
        elif value >= 1 and value < 10:
            flag = False
    return i


def _return_print_values(variable, method):
    '''Function for returning the correct mean and std for the method
    selected.

    :param variable: The variable we want to find the mean+/-error for
    :type variable: Measurement
    :param method: The method used to propagate error we want to find 
                   the mean and error for.
    :type method: str

    :returns: The mean and standard deviation of the variable.
    :rtype: tuple
    '''
    if isinstance(variable, Constant):
        return (variable.mean, 0,)
    if ExperimentalValue.error_method == 'Derivative':
        [mean, std] = variable.der
    elif ExperimentalValue.error_method == 'Monte Carlo':
        [mean, std] = variable.MC
    elif ExperimentalValue.error_method == 'Min Max':
        [mean, std] = variable.MinMax

    if method is not None:
        if method == 'Derivative':
            [mean, std] = variable.der
        elif method == 'Monte Carlo':
            [mean, std] = variable.MC
        elif method == 'Min Max':
            [mean, std] = variable.MinMax
    return (mean, std,)


def _tex_print(self, method=None):
    '''Creates string used by __str__ in a style useful for printing in Latex,
    as a value with error, in brackets multiplied by a power of ten. (ie.
    15+/-0.3 is (150 \pm 3)\e-1. Where Latex parses \pm as +\- and \e as
    *10**-1)

    :param method: The method used to propagate error we want to find 
                   the mean and error for.
    :type method: str

    :returns: The mean+/-error of the variable, formatted using tex style.
    :rtype: str
    '''
    mean, std = _return_print_values(self, method)

    if ExperimentalValue.figs is not None and\
            ExperimentalValue.figs_on_uncertainty == False:

        if mean == float('inf'):
            return "inf"

        figs = ExperimentalValue.figs - 1
        i = _return_exponent(mean)
        mean = int(round(mean*10**(figs - i), 0))
        std = int(round(std*10**(figs - i), 0))

        if i - figs != 0:
            return "(%d \pm %d)*10^{%d}" % (mean, std, i - figs)
        else:
            return "(%d \pm %d)" % (mean, std)

    elif ExperimentalValue.figs is not None and\
            ExperimentalValue.figs_on_uncertainty == True:

        if mean == float('inf'):
            return "inf"

        figs = ExperimentalValue.figs - 1
        i = _return_exponent(std)
        mean = int(round(mean*10**(figs - i), 0))
        std = int(round(std*10**(figs - i), 0))

        if i - figs != 0:
            return "(%d \pm %d)*10^{%d}" % (mean, std, i - figs)
        else:
            return "(%d \pm %d)" % (mean, std)

    else:
        i = _return_exponent(std)
        mean = int(round(mean*10**-i, 0))
        std = int(round(std*10**-i, 0))

        if i != 0:
            return "(%d \pm %d)*10^{%d}" % (mean, std, i)
        else:
            return "(%d \pm %d)" % (mean, std)


def _def_print(self, method=None):
    '''Returns string used by __str__ as two numbers representing mean and error
    to either the first non-zero digit of error or to a specified number of
    significant figures.

    :param method: The method used to propagate error we want to find 
                   the mean and error for.
    :type method: str

    :returns: The mean+/-error of the variable, formatted using the default style.
    :rtype: str
    '''
    method = self.error_method if not method else method
    mean, std = _return_print_values(self, method)

    if ExperimentalValue.figs is not None and\
            ExperimentalValue.figs_on_uncertainty == False:

        if mean == float('inf'):
            return "inf"

        figs = ExperimentalValue.figs - 1
        i = _return_exponent(mean)

        decimal_places = figs - i
        if decimal_places > 0:
            n = '%d' % (decimal_places)
            n = "%."+n+"f"
        else:
            n = '%.0f'
        std = float(round(std, decimal_places))
        mean = float(round(mean, decimal_places))
        return n % (mean)+" +/- "+n % (std)

    elif ExperimentalValue.figs is not None and\
            ExperimentalValue.figs_on_uncertainty == True:

        if mean == float('inf'):
            return "inf"

        figs = ExperimentalValue.figs - 1
        i = _return_exponent(std)

        decimal_places = figs - i
        if decimal_places > 0:
            n = '%d' % (decimal_places)
            n = "%."+n+"f"
        else:
            n = '%.0f'
        std = float(round(std, decimal_places))
        mean = float(round(mean, decimal_places))
        return n % (mean)+" +/- "+n % (std)

    else:
        if mean == float('inf') and std == float('inf'):
            return "inf +/- inf"
        
        if mean == float('inf'):
            return "inf"

        if std == 0:
            return str(mean)+" +/- "+str(std)

        i = _return_exponent(std)

        if i < 0:
            n = '%d' % (-i)
            n = "%."+n+"f"
        else:
            n = '%.0f'

        mean = float(round(mean, -i))
        if std == float('inf'):
            return  n % (mean)+" +/- inf"
        
        std = float(round(std, -i))
        return n % (mean)+" +/- "+n % (std)


def _sci_print(self, method=None):
    '''Returns string used by __str__ as two numbers representing mean and
    error, each in scientific notation to a specified numebr of significant
    figures, or 3 if none is given.

    :param method: The method used to propagate error we want to find 
                   the mean and error for.
    :type method: str

    :returns: The mean+/-error of the variable, formatted using scientific notation.
    :rtype: str
    '''
    mean, std = _return_print_values(self, method)

    if ExperimentalValue.figs is not None and\
            ExperimentalValue.figs_on_uncertainty == False:

        if mean == float('inf'):
            return "inf"

        figs = ExperimentalValue.figs - 1
        i = _return_exponent(mean)
        mean = int(round(mean*10**(figs - i), 0))
        std = int(round(std*10**(figs - i), 0))

        if i - figs != 0:
            return "(%d +/- %d)*10^(%d)" % (mean, std, i - figs)
        else:
            return "(%d +/- %d)" % (mean, std)

    elif ExperimentalValue.figs is not None and\
            ExperimentalValue.figs_on_uncertainty == True:

        if mean == float('inf'):
            return "inf"

        figs = ExperimentalValue.figs - 1
        i = _return_exponent(std)
        mean = int(round(mean*10**(figs - i), 0))
        std = int(round(std*10**(figs - i), 0))

        if i - figs != 0:
            return "(%d +/- %d)*10^(%d)" % (mean, std, i - figs)
        else:
            return "(%d +/- %d)" % (mean, std)

    else:
        i = _return_exponent(std)
        mean = int(round(mean*10**-i, 0))
        std = int(round(std*10**-i, 0))

        if i != 0:
            return "(%d +/- %d)*10^(%d)" % (mean, std, i)
        else:
            return "(%d +/- %d)" % (mean, std)


###############################################################################
# Random Methods
###############################################################################

def _parse_units(unit_str):
    '''Parses the string representation of an objects units and
    breaks it into it's constituent parts and their powers.

    :param unit_str: A string representation of an object's units.
    :type unit_str: str

    :returns: A dictionary with the unit names as keys and their powers as values.
    :rtype: dict
    '''
    unit_dict = {}
    div_split = unit_str.split("/")
    for index in range(len(div_split)):
        mult_split = re.findall('[a-zA-Z]+\^[+-]?\d+|[a-zA-Z]+', div_split[index]) # regex that finds [string^(+/- number)] or [string]
        for term2 in mult_split:
            pow_split = term2.split("^")
            power_factor = -1 if index else 1
            power = power_factor*int(pow_split[1]) if len(pow_split)>1 else power_factor*1
            unit_dict[pow_split[0]] = power
    return unit_dict

def show_histogram(data, title=None, output='inline', bins=50, color="#036564"):
    '''Creates a histogram of the inputted data using Bokeh or mpl.
    
    :param title: The title that will appear at the top of the histogram.
    :type title: str
    :param output: How the histogram is to be output. Can be 'inline' or 'file'.
    :type output: str

    :returns: The Plot object used to create the histogram.
    :rtype: Plot
    '''
    if type(data) not in ExperimentalValue.ARRAY:
        print('''Input histogram data must be an array''')
        return

    if type(title) is str:
        hist_title = title
    elif title is None:
        hist_title = 'Histogram'
    else:
        print('Histogram title must be a string.')
        hist_title = 'Histogram'

    mean, std = _variance(data)

    xy_data = q.XYDataSet(xdata = data, is_histogram = True, data_name=hist_title)
    fig = q.MakePlot()
    fig.add_dataset(xy_data, color = color)
    fig.x_range = [min(data)*.95,max(data)*1.05]
    fig.y_range = [0,max(xy_data.ydata)*1.2]

    # Draws lines at the mean and location of the mean +/- standard deviation.
    fig.add_line(x=mean, dashed=False, color='red')
    fig.add_line(x=mean+std, dashed=True, color='red')
    fig.add_line(x=mean-std, dashed=True, color='red')

    fig.show()
    return fig


def numerical_partial_derivative(func, var, *args):
    '''Returns the parital derivative of a dunction with respect to var.

    This function wraps the inputted function to become a function
    of only one variable, the derivative is taken with respect to said
    variable.

    :param func: The function to take the derivative of.
    :type func:
    :param var: The variable to take the derivative with respect to.
    :type var: Measurement

    :returns: The derivative of the function with respect to the specified variable.
    :rtype: float
    '''
    def restrict_dimension(x):
        partial_args = list(args)
        partial_args[var] = x
        return func(*partial_args)
    return numerical_derivative(restrict_dimension, args[var])


def numerical_derivative(function, point, dx=1e-10):
    '''Calculates the first order derivative of a function.

    :param function: The function to take the derivative of.
    :type function: Function
    :param point: The point on the function to take the derivative at.
    :type point: float

    :returns: The derivative of the function.
    :rtype: float
    '''
    return (function(point+dx)-function(point))/dx


def _variance(*args, ddof=1):
    '''Uses a more sophisticated variance calculation to speed up
    calculation of mean and standard deviation.

    :param *args: The variables we want to find the variance of.
    :type *args: tuple
    :param ddof: The degrees of freedom of the system.
    :type ddof: int

    :returns: The mean and standard deviation of a data array.
    :rtype: tuple
    '''
    args = args[0]
    Sum = 0
    SumSq = 0
    N = len(args)
    for i in range(N):
        Sum += args[i]
        SumSq += args[i]*args[i]

    std = ((SumSq-Sum**2/N)/(N-1))**(1/2)
    mean = Sum/N

    return (mean, std, )

def _weighted_variance(mean, std, ddof=1):
    '''Calculates the variance weighted mean and standard deviation.

    :param mean: The mean of the variable.
    :type mean: float
    :param std: The standard deviation of the variable.
    :type std: float
    :param ddof: The degrees of freedom of the system.
    :type ddof: int

    :returns: The variance weighted mean and standard deviation.
    :rtype: tuple
    '''
    from math import sqrt

    w = np.power(std, -2)
    w_mean = sum(np.multiply(w, mean))/sum(w)
    w_std = 1/sqrt(sum(w))
    return (w_mean, w_std)


def reset_variables():
    '''Resets the ID number, directories and methods to their original values.
    Useful in Jupyter Notebooks if variables were unintentionally repeated.
    '''
    Measurement.id_number = 0
    Function.id_number = 0
    ExperimentalValue.register = {}
    ExperimentalValue.formula_register = {}
    ExperimentalValue.error_method = "Derivative"
    ExperimentalValue.mc_trial_number = 10000
    ExperimentalValue.print_style = "Default"
    ExperimentalValue.figs = 3
