import inspect
import os

import numpy as np
import statsmodels.api as sm
from calendar import monthrange

from GHOST_standards import standard_parameters
import unit_converter


class Data_Products(object):

    """Provides functions for calculation of data products"""

    @staticmethod
    def calculate_aod(new_wavelength, calculation_params, calculation_arr):
        """ Calculate aerosol optical depth (AOD) at a given wavelength using following the Ångström power law

            :param new_wavelength: numerical value of the wavelength at which the new component is computed for
            :type new_wavelength: int
            :param calculation_params: GHOST names of the input components 
            :type calculation_params: list
            :param calculation_arr: input component values of shape (N components, time)
            :type calculation_arr: numpy.ndarray
            :return: new component array
            :rtype: numpy.ndarray
        """

        #filter out input data outside valid bounds
        for calculation_param_ii, calculation_param in enumerate(calculation_params):
            out_of_bounds = np.where(np.logical_or(calculation_arr[calculation_param_ii, :] < standard_parameters[calculation_param]['extreme_lower_limit'], calculation_arr[calculation_param_ii, :] > standard_parameters[calculation_param]['extreme_upper_limit']))[0]
            calculation_arr[calculation_param_ii, out_of_bounds] = np.NaN

        #get indices for aod and angstrom exponent parameters 
        aod_param_indices = np.array([calculation_param_ii for calculation_param_ii, calculation_param in enumerate(calculation_params) if 'AOD_' in calculation_param], dtype=np.uint32)
        #ae_param_index = [calculation_param_ii for calculation_param_ii, calculation_param in enumerate(calculation_params) if 'AE_' in calculation_param][0]
        
        #get specific parameters / angstrom exponent for aod and angstrom exponent 
        aod_params = [calculation_params[aod_param_index] for aod_param_index in aod_param_indices]
        aod_arr = calculation_arr[aod_param_indices, :]
        #ae_param = calculation_params[ae_param_index]
        #ae_arr = calculation_arr[ae_param_index, :]

        #get numerical values from aod params
        aod_wavelengths = [int(aod_param.split('_')[-1][:-2]) for aod_param in aod_params]

        #calculate new AOD component (Sara's method)
        #factors = np.full(aod_arr.shape, np.NaN, dtype=np.float32)
        #for aod_wavelength_ii, aod_wavelength in enumerate(aod_wavelengths):
        #    factors[aod_wavelength_ii, :] = (aod_arr[aod_wavelength_ii, :]/(aod_wavelength**(-ae_arr[:])))*(new_wavelength**(-ae_arr[:]))            
        #new_aod = np.mean(factors, axis=0)

        #calculate new AOD component (logfit method)
        new_aod = np.full((aod_arr.shape[1]), np.NaN, dtype=np.float32)
        log_aod_wavelengths = np.log(aod_wavelengths) 
        log_aod_wavelengths = np.column_stack((log_aod_wavelengths, log_aod_wavelengths**2))   
        log_aod_wavelengths = sm.add_constant(log_aod_wavelengths)
        log_aod_arr = np.log(aod_arr)  
        new_wavelength_log = np.log(new_wavelength)
        #iterate through time points
        for time_ii in range(len(new_aod)):
            log_aod_vals = log_aod_arr[:,time_ii]
            #only proceed with calculation if have all values for calculation
            if np.isfinite(log_aod_vals).all():
                model = sm.OLS(log_aod_vals, log_aod_wavelengths).fit()
                A0 = model.params[2]
                B0 = model.params[1]
                C0 = model.params[0]
                #model = LinearRegression().fit(log_aod_wavelengths, log_aod_vals)
                #A0 = model.coef_[1]
                #B0 = model.coef_[0] 
                #C0 = model.intercept_
                new_aod[time_ii] = np.exp(A0*new_wavelength_log**2 + B0*new_wavelength_log + C0)

        return new_aod
    
    @staticmethod
    def calculate_aod_bc(calculation_params, calculation_arr):
        """ Calculate the aerosol optical depth (AOD) for black carbon (BC) at 440nm

            :param calculation_params: GHOST names of the input components 
            :type calculation_params: list
            :param calculation_arr: input component values of shape (N components, time)
            :type calculation_arr: numpy.ndarray
            :return: new component array
            :rtype: numpy.ndarray
        """

        #filter out input data outside valid bounds
        for calculation_param_ii, calculation_param in enumerate(calculation_params):
            out_of_bounds = np.where(np.logical_or(calculation_arr[calculation_param_ii, :] < standard_parameters[calculation_param]['extreme_lower_limit'], calculation_arr[calculation_param_ii, :] > standard_parameters[calculation_param]['extreme_upper_limit']))[0]
            calculation_arr[calculation_param_ii, out_of_bounds] = np.NaN

        # get required parameter indices
        aaod_index = [calculation_param_ii for calculation_param_ii, calculation_param in enumerate(calculation_params) if 'ABSAOD_870nm' in calculation_param][0]
        sae_index = [calculation_param_ii for calculation_param_ii, calculation_param in enumerate(calculation_params) if 'SCATAE_440-675nm' in calculation_param][0]
        aae_440_675_index = [calculation_param_ii for calculation_param_ii, calculation_param in enumerate(calculation_params) if 'ABSAE_440-675nm' in calculation_param][0]
        aae_440_870_index = [calculation_param_ii for calculation_param_ii, calculation_param in enumerate(calculation_params) if 'ABSAE_440-870nm' in calculation_param][0]

        # filter out dust measurements, so that calculated AOD is specific for black carbon
        dust_indices = (calculation_arr[sae_index, :] < 0.5) & (calculation_arr[aae_440_675_index, :] > 1.5)
        calculation_arr[:, dust_indices] = np.NaN 

        #calculate new aod bc component
        new_aod = calculation_arr[aaod_index, :] * (870/440)**calculation_arr[aae_440_870_index, :]

        return new_aod
    
    @staticmethod
    def calculate_aod_brc(calculation_params, calculation_arr):
        """ Calculate the aerosol optical depth (AOD) for brown carbon (BrC) at 440nm

            :param calculation_params: GHOST names of the input components 
            :type calculation_params: list
            :param calculation_arr: input component values of shape (N components, time)
            :type calculation_arr: numpy.ndarray
            :return: new component array
            :rtype: numpy.ndarray
        """

        #filter out input data outside valid bounds
        for calculation_param_ii, calculation_param in enumerate(calculation_params):
            out_of_bounds = np.where(np.logical_or(calculation_arr[calculation_param_ii, :] < standard_parameters[calculation_param]['extreme_lower_limit'], calculation_arr[calculation_param_ii, :] > standard_parameters[calculation_param]['extreme_upper_limit']))[0]
            calculation_arr[calculation_param_ii, out_of_bounds] = np.NaN

        # get required parameter indices
        aaod_index = [calculation_param_ii for calculation_param_ii, calculation_param in enumerate(calculation_params) if 'ABSAOD_440nm' in calculation_param][0]
        aaod_bc_index = [calculation_param_ii for calculation_param_ii, calculation_param in enumerate(calculation_params) if 'BC_ABSAOD_440nm' in calculation_param][0]

        #calculate new aod brc component
        new_aod = calculation_arr[aaod_index, :] - calculation_arr[aaod_bc_index, :]

        return new_aod

    @staticmethod
    def calculate_ae(calculation_params, calculation_arr):
        """ Calculate angstrom exponent (AE) between 2 wavelengths

            :param calculation_params: GHOST names of the input components 
            :type calculation_params: list
            :param calculation_arr: input component values of shape (N components, time)
            :type calculation_arr: numpy.ndarray
            :return: new component array
            :rtype: numpy.ndarray
        """

        #filter out input data outside valid bounds
        for calculation_param_ii, calculation_param in enumerate(calculation_params):
            out_of_bounds = np.where(np.logical_or(calculation_arr[calculation_param_ii, :] < standard_parameters[calculation_param]['extreme_lower_limit'], calculation_arr[calculation_param_ii, :] > standard_parameters[calculation_param]['extreme_upper_limit']))[0]
            calculation_arr[calculation_param_ii, out_of_bounds] = np.NaN

        #get wavelengths from calculation params 
        wavelengths = [int(calculation_param.split('_')[-1][:-2]) for calculation_param in calculation_params]
        log_wavelengths = np.log(wavelengths)

        #calculate new AE component
        new_ae = np.full((calculation_arr.shape[1]), np.NaN, dtype=np.float32)
        #iterate through time points
        for time_ii in range(len(new_ae)):
            calculation_vals_log = np.log(calculation_arr[:,time_ii])
            if np.isfinite(calculation_vals_log).all():
                new_ae[time_ii] = -np.polyfit(log_wavelengths,calculation_vals_log,1)[0]

        return new_ae

    @staticmethod
    def calculate_ae_alt(calculation_params, calculation_arr):
        """ Alternative calculation method for the angstrom exponent (AE) between 2 wavelengths.
            Done by adding the AE of the SSA and extinction AOD respectively.

            :param calculation_params: GHOST names of the input components 
            :type calculation_params: list
            :param calculation_arr: input component values of shape (N components, time)
            :type calculation_arr: numpy.ndarray
            :return: new component array
            :rtype: numpy.ndarray
        """

        #filter out input data outside valid bounds
        for calculation_param_ii, calculation_param in enumerate(calculation_params):
            out_of_bounds = np.where(np.logical_or(calculation_arr[calculation_param_ii, :] < standard_parameters[calculation_param]['extreme_lower_limit'], calculation_arr[calculation_param_ii, :] > standard_parameters[calculation_param]['extreme_upper_limit']))[0]
            calculation_arr[calculation_param_ii, out_of_bounds] = np.NaN

        #calculate new AE component
        new_ae = calculation_arr[0, :] + calculation_arr[1, :]

        return new_ae

    @staticmethod
    def calculate_ae_bc(calculation_params, calculation_arr):
        """ Calculate the angstrom exponent (AE) of black carbon (BC) between 440 and 870nm.
            This is done using Mie theory following Wang et al., 2016

            :param calculation_params: GHOST names of the input components 
            :type calculation_params: list
            :param calculation_arr: input component values of shape (N components, time)
            :type calculation_arr: numpy.ndarray
            :return: new component array
            :rtype: numpy.ndarray
        """

        #dict for information taken from Figure 2 of Wang et al., 2016 
        #wang_dict = {'aae_675_870_start': [0,     0.1,   0.3,   0.5,  0.7,  0.9,   1.1,   1.3],
        #             'aae_675_870_end':   [0.1,   0.3,   0.5,   0.7,  0.9,  1.1,   1.3,   1.6],
        #             'wda_median':        [-0.09, -0.14, -0.18, -0.2, -0.2, -0.12, -0.09, -0.09]}

        #load Mie theory information generated by Vincenzo
        mie_theory = np.genfromtxt('{}/mie_calculation/aae_wda_mie_obiso.csv'.format(os.path.abspath(inspect.getsourcefile(lambda:0)).split('/data_product_formulae')[0]), delimiter=',', names=True)

        #filter out input data outside valid bounds
        for calculation_param_ii, calculation_param in enumerate(calculation_params):
            out_of_bounds = np.where(np.logical_or(calculation_arr[calculation_param_ii, :] < standard_parameters[calculation_param]['extreme_lower_limit'], calculation_arr[calculation_param_ii, :] > standard_parameters[calculation_param]['extreme_upper_limit']))[0]
            calculation_arr[calculation_param_ii, out_of_bounds] = np.NaN
   
        #iterate through AAE 675-870 bins, if any observations are in bin calculate AAE 440-870nm using associated WDA median
        new_ae = np.full((calculation_arr.shape[1]), np.NaN, dtype=np.float32)
        for aae_start, aae_end, wda in zip(mie_theory['AAEstr'], mie_theory['AAEend'], mie_theory['WDAmed']):
            relevant_inds = (calculation_arr[0, :] >= aae_start) & (calculation_arr[0, :] < aae_end)
            new_ae[relevant_inds] = calculation_arr[0, relevant_inds] + wda  

        return new_ae

    @staticmethod
    def calculate_wdae(calculation_params, calculation_arr):
        """ Calculate wavelength dependence of the angstrom exponent

            :param calculation_params: GHOST names of the input components 
            :type calculation_params: list
            :param calculation_arr: input component values of shape (N components, time)
            :type calculation_arr: numpy.ndarray
            :return: new component array
            :rtype: numpy.ndarray
        """

        #filter out input data outside valid bounds
        for calculation_param_ii, calculation_param in enumerate(calculation_params):
            out_of_bounds = np.where(np.logical_or(calculation_arr[calculation_param_ii, :] < standard_parameters[calculation_param]['extreme_lower_limit'], calculation_arr[calculation_param_ii, :] > standard_parameters[calculation_param]['extreme_upper_limit']))[0]
            calculation_arr[calculation_param_ii, out_of_bounds] = np.NaN

        #get correct indices for calculation
        wavelengths_start = [int(calculation_param.split('_')[-1].split('-')[0]) for calculation_param in calculation_params]
        wavelengths_end = [int(calculation_param.split('_')[-1].split('-')[1][:-2]) for calculation_param in calculation_params]
        wavelengths_diffs = np.array(wavelengths_end) - np.array(wavelengths_start)
        wavelength_top_index = np.argmax(wavelengths_diffs)
        wavelength_bottom_index = np.argmin(wavelengths_diffs)

        #calculate wdae
        wdae = calculation_arr[wavelength_top_index, :] - calculation_arr[wavelength_bottom_index, :]

        return wdae

    @staticmethod
    def calculate_bf(calculation_params, calculation_arr):
        """ Calculate backscatter fraction at a given wavelength

            :param calculation_params: GHOST names of the input components 
            :type calculation_params: list
            :param calculation_arr: input component values of shape (N components, time)
            :type calculation_arr: numpy.ndarray
            :return: new component array
            :rtype: numpy.ndarray
        """

        #filter out input data outside valid bounds
        for calculation_param_ii, calculation_param in enumerate(calculation_params):
            out_of_bounds = np.where(np.logical_or(calculation_arr[calculation_param_ii, :] < standard_parameters[calculation_param]['extreme_lower_limit'], calculation_arr[calculation_param_ii, :] > standard_parameters[calculation_param]['extreme_upper_limit']))[0]
            calculation_arr[calculation_param_ii, out_of_bounds] = np.NaN

        #get indices for backscatter and scattering coefficient parameters
        backscat_param_index = [calculation_param_ii for calculation_param_ii, calculation_param in enumerate(calculation_params) if '_BACKSCAT_' in calculation_param][0]
        scat_param_index = [calculation_param_ii for calculation_param_ii, calculation_param in enumerate(calculation_params) if '_SCAT_' in calculation_param][0]

        #calculate new backscatter fraction
        total = calculation_arr[backscat_param_index,:] + calculation_arr[scat_param_index,:]
        bf = calculation_arr[backscat_param_index,:] / total

        return bf

    @staticmethod
    def calculate_asy(calculation_params, calculation_arr):
        """ Calculate asymmetry factor at a given wavelength

            :param calculation_params: GHOST names of the input components 
            :type calculation_params: list
            :param calculation_arr: input component values of shape (N components, time)
            :type calculation_arr: numpy.ndarray
            :return: new component array
            :rtype: numpy.ndarray
        """

        #filter out input data outside valid bounds
        for calculation_param_ii, calculation_param in enumerate(calculation_params):
            out_of_bounds = np.where(np.logical_or(calculation_arr[calculation_param_ii, :] < standard_parameters[calculation_param]['extreme_lower_limit'], calculation_arr[calculation_param_ii, :] > standard_parameters[calculation_param]['extreme_upper_limit']))[0]
            calculation_arr[calculation_param_ii, out_of_bounds] = np.NaN

        #calculate asymmetry factor 
        asy = -7.14*calculation_arr[0,:]**3 + 7.46*calculation_arr[0,:]**2 - 3.96*calculation_arr[0,:] + 0.9893

        return asy

    @staticmethod
    def calculate_ssa_absaod_extaod(calculation_params, calculation_arr):
        """ Calculate single scattering albedo (SSA) at a given wavelength,
            using absorption and extinction AOD.

            :param calculation_params: GHOST names of the input components 
            :type calculation_params: list
            :param calculation_arr: input component values of shape (N components, time)
            :type calculation_arr: numpy.ndarray
            :return: new component array
            :rtype: numpy.ndarray
        """

        #filter out input data outside valid bounds
        for calculation_param_ii, calculation_param in enumerate(calculation_params):
            out_of_bounds = np.where(np.logical_or(calculation_arr[calculation_param_ii, :] < standard_parameters[calculation_param]['extreme_lower_limit'], calculation_arr[calculation_param_ii, :] > standard_parameters[calculation_param]['extreme_upper_limit']))[0]
            calculation_arr[calculation_param_ii, out_of_bounds] = np.NaN

        #get indices for absorption and extinction parameters
        abs_param_index = [calculation_param_ii for calculation_param_ii, calculation_param in enumerate(calculation_params) if 'ABSAOD_' in calculation_param][0]
        ext_param_index = [calculation_param_ii for calculation_param_ii, calculation_param in enumerate(calculation_params) if 'EXTAOD_' in calculation_param][0]

        #calculate new SSA component
        new_ssa = (calculation_arr[ext_param_index,:] - calculation_arr[abs_param_index,:]) / calculation_arr[ext_param_index,:]

        return new_ssa

    @staticmethod
    def calculate_ssa_scat_abs(calculation_params, calculation_arr):
        """ Calculate single scattering albedo (SSA) at a given wavelength,
            using aerosol scattering and absorption coefficients. 

            :param calculation_params: GHOST names of the input components 
            :type calculation_params: list
            :param calculation_arr: input component values of shape (N components, time)
            :type calculation_arr: numpy.ndarray
            :return: new component array
            :rtype: numpy.ndarray
        """

        #filter out input data outside valid bounds
        for calculation_param_ii, calculation_param in enumerate(calculation_params):
            out_of_bounds = np.where(np.logical_or(calculation_arr[calculation_param_ii, :] < standard_parameters[calculation_param]['extreme_lower_limit'], calculation_arr[calculation_param_ii, :] > standard_parameters[calculation_param]['extreme_upper_limit']))[0]
            calculation_arr[calculation_param_ii, out_of_bounds] = np.NaN

        #get indices for absorption and extinction parameters
        scat_param_indices = [calculation_param_ii for calculation_param_ii, calculation_param in enumerate(calculation_params) if 'AERO_LIGHT_SCAT_COEFF' in calculation_param]
        abs_param_index = [calculation_param_ii for calculation_param_ii, calculation_param in enumerate(calculation_params) if 'AERO_ABS_COEFF' in calculation_param][0]
        ae_param_indices = [calculation_param_ii for calculation_param_ii, calculation_param in enumerate(calculation_params) if 'SCATAE_' in calculation_param]

        # get absorption wavelength nms for calculation
        abs_nm = float(calculation_params[abs_param_index].split('_')[-1][:-2])

        # define array for calculated SSA
        ssa = np.full((calculation_arr.shape[1]), np.NaN, dtype=np.float32)

        # iterate through all scattering wavelengths
        for scat_param_index in scat_param_indices:

            # get scattering wavelength nm for calculation
            scat_nm = float(calculation_params[scat_param_index].split('_')[-1][:-2])

            # iterate through AE params
            for ae_param_index in ae_param_indices:

                # calculate scattering coefficient wavelength at required wavelength
                new_scat = calculation_arr[scat_param_index,:] * (scat_nm / abs_nm) ** calculation_arr[ae_param_index,:]

                #calculate new SSA component
                new_ssa = new_scat / (new_scat + calculation_arr[abs_param_index,:])

                # get valid SSA indices
                valid_inds = ~np.isnan(new_ssa) 

                # place valid calculated SSA in full array
                ssa[valid_inds] = new_ssa[valid_inds]

        return ssa

    @staticmethod
    def calculate_schuster_bc_brc(calculation_params, calculation_arr):
        """ Calculate schuster filter for the removal of black and brown carbon aerosols,
            by taking the mean of the imaginary part of the refractive indices 
            at 675, 870 and 1020nm.

            :param calculation_params: GHOST names of the input components 
            :type calculation_params: list
            :param calculation_arr: input component values of shape (N components, time)
            :type calculation_arr: numpy.ndarray
            :return: new component array
            :rtype: numpy.ndarray
        """

        #filter out input data outside valid bounds
        for calculation_param_ii, calculation_param in enumerate(calculation_params):
            out_of_bounds = np.where(np.logical_or(calculation_arr[calculation_param_ii, :] < standard_parameters[calculation_param]['extreme_lower_limit'], calculation_arr[calculation_param_ii, :] > standard_parameters[calculation_param]['extreme_upper_limit']))[0]
            calculation_arr[calculation_param_ii, out_of_bounds] = np.NaN

        #calculate schuster filter for removing black and brown carbon, by taking the mean of the imaginary part of the refractive indices at 675, 870 and 1020nm
        schuster_filter = np.mean(calculation_arr[:,:], axis=0)

        return schuster_filter

    @staticmethod
    def calculate_schuster_ss(calculation_params, calculation_arr):
        """ Calculate schuster filter for the removal of sea salt aerosols,
            by subtracting the single scattering albedo at 440nm from that at 675nm.

            :param calculation_params: GHOST names of the input components 
            :type calculation_params: list
            :param calculation_arr: input component values of shape (N components, time)
            :type calculation_arr: numpy.ndarray
            :return: new component array
            :rtype: numpy.ndarray
        """

        #filter out input data outside valid bounds
        for calculation_param_ii, calculation_param in enumerate(calculation_params):
            out_of_bounds = np.where(np.logical_or(calculation_arr[calculation_param_ii, :] < standard_parameters[calculation_param]['extreme_lower_limit'], calculation_arr[calculation_param_ii, :] > standard_parameters[calculation_param]['extreme_upper_limit']))[0]
            calculation_arr[calculation_param_ii, out_of_bounds] = np.NaN

        # get required parameter indices
        ssa_440_index = [calculation_param_ii for calculation_param_ii, calculation_param in enumerate(calculation_params) if 'SSA_440nm' in calculation_param][0]
        ssa_675_index = [calculation_param_ii for calculation_param_ii, calculation_param in enumerate(calculation_params) if 'SSA_675nm' in calculation_param][0]

        #calculate schuster filter for removing sea salt aerosol, by subtracting the single scattering albedo at 440nm from that at 675nm.
        schuster_filter = calculation_arr[ssa_675_index,:] - calculation_arr[ssa_440_index,:]

        return schuster_filter

    @staticmethod
    def calculate_mac(calculation_params, calculation_arr):
        """ Calculate mass absorption cross section at a given wavelength

            :param calculation_params: GHOST names of the input components 
            :type calculation_params: list
            :param calculation_arr: input component values of shape (N components, time)
            :type calculation_arr: numpy.ndarray
            :return: new component array
            :rtype: numpy.ndarray
        """

        #filter out input data outside valid bounds
        for calculation_param_ii, calculation_param in enumerate(calculation_params):
            out_of_bounds = np.where(np.logical_or(calculation_arr[calculation_param_ii, :] < standard_parameters[calculation_param]['extreme_lower_limit'], calculation_arr[calculation_param_ii, :] > standard_parameters[calculation_param]['extreme_upper_limit']))[0]
            calculation_arr[calculation_param_ii, out_of_bounds] = np.NaN

        # get required parameter indices
        abs_index = [calculation_param_ii for calculation_param_ii, calculation_param in enumerate(calculation_params) if 'AERO_ABS_COEFF' in calculation_param][0]
        ec_index = [calculation_param_ii for calculation_param_ii, calculation_param in enumerate(calculation_params) if 'EC' in calculation_param][0]

        # calculate mass absorption cross section at wavelength
        mac = calculation_arr[abs_index,:] / calculation_arr[ec_index,:]

        return mac
    
    @staticmethod
    def calculate_wetdep(calculation_params, calculation_arr, year_month, temporal_resolution_code, parameter_units):
        """ Calculate wet deposition from precipitation amount and concentration

            :param calculation_params: GHOST names of the input components 
            :type calculation_params: list
            :param calculation_arr: input component values of shape (N components, time)
            :type calculation_arr: numpy.ndarray
            :return: new component array
            :rtype: numpy.ndarray
        """

        # minutes per given time resolution
        if (temporal_resolution_code == 'H') or (temporal_resolution_code == 'HI'):
            minutes = 60
        elif (temporal_resolution_code == '3H') or (temporal_resolution_code == '3HI'):
            minutes = 180
        elif (temporal_resolution_code == '6H') or (temporal_resolution_code == '6HI'):
            minutes = 360
        elif temporal_resolution_code == 'D':
            minutes = 360*24
        elif temporal_resolution_code == 'M':
            minutes = monthrange(int(year_month[:4]), int(year_month[-2:].strip('0')))[1] * 24 * 60 # days per month * hours per day * minutes per hour
        print(minutes)

        #filter out input data outside valid bounds
        for calculation_param_ii, calculation_param in enumerate(calculation_params):
            out_of_bounds = np.where(np.logical_or(calculation_arr[calculation_param_ii, :] < standard_parameters[calculation_param]['extreme_lower_limit'], calculation_arr[calculation_param_ii, :] > standard_parameters[calculation_param]['extreme_upper_limit']))[0]
            calculation_arr[calculation_param_ii, out_of_bounds] = np.NaN

        #get indices for precip and concentration
        precip_param_index = [calculation_param_ii for calculation_param_ii, calculation_param in enumerate(calculation_params) if 'PREC_ACCUM' in calculation_param][0]
        conc_param_index = [calculation_param_ii for calculation_param_ii, calculation_param in enumerate(calculation_params) if '_PREC' in calculation_param][0]

        #calculate new SSA component
        wetdep = (calculation_arr[precip_param_index,:] * calculation_arr[conc_param_index,:]) / float(minutes) # flux

        # convert wetdep unit to standard wetdep unit, easy unit conversion
        conv_obj = unit_converter.convert_units(parameter_units['concentration_standard_unit'][:-3]+'min-1 m-2', parameter_units['deposition_standard_unit'],1.0)
        wetdep = wetdep*conv_obj.conversion_factor

        return wetdep
    

    @staticmethod
    def calculate_pH(calculation_params, calculation_arr, parameter_details, parameter_units, input_parameter_details):
        """ Calculate pH value from H+ concentration in precipitation

            :param calculation_params: GHOST names of the input components 
            :type calculation_params: list
            :param calculation_arr: input component values of shape (N components, time)
            :type calculation_arr: numpy.ndarray
            :return: new component array
            :rtype: numpy.ndarray
        """

        #filter out input data outside valid bounds
        for calculation_param_ii, calculation_param in enumerate(calculation_params):
            out_of_bounds = np.where(np.logical_or(calculation_arr[calculation_param_ii, :] < standard_parameters[calculation_param]['extreme_lower_limit'], calculation_arr[calculation_param_ii, :] > standard_parameters[calculation_param]['extreme_upper_limit']))[0]
            calculation_arr[calculation_param_ii, out_of_bounds] = np.NaN

        # convert H+ concentration to mol l-1
        input_units = {'molar_mass':'kg mol-1', 'mass_density':'mg l-1'}
        input_vals = {'molar_mass':unit_converter.get_molecular_mass(input_parameter_details['chemical_formula']), 'mass_density':1.0}
        conv_obj = unit_converter.convert_units(input_units, parameter_units['pH_calculation_standard_unit'],input_vals,measured_species=input_parameter_details['chemical_formula'], conversion_input_quantity='mass_density')

        #get indices
        conc_param_index = [calculation_param_ii for calculation_param_ii, calculation_param in enumerate(calculation_params) if 'H+_PREC' in calculation_param][0]

        #calculate new component
        pH = -np.log10(calculation_arr[conc_param_index,:]*conv_obj.conversion_factor)

        return pH