def sens_analysis(func, bounds, n_samples=2**7, 
                  kwargs=None, param_names=None,
                  verbose=True, log_scale=True):
    '''
    Objective:
        - Perform Sobol sensitivity analysis on the vectorized objective function.
    Inputs:
        - func: Objective function (Problem) to analyze.
        - bounds: Parameter space bounds, as an array of tuples.
        - n_samples: Number of Sobol samples to generate.
        - kwargs: Keyword arguments (dictionary) for objective function.
        - param_names: Optional parameter names for each dimension.
        - verbose: Boolean to display plots.
        - log_scale: Boolean to log-scale plots.
    Outputs:
        - Si: Full sensitivity indices and confidences.
        - S2_matrix: Matrix of S2 relationship sensitivity indices.
    '''

    # imports
    try:
        import numpy as np
        from SALib.sample import sobol as sobol_sample
        from SALib.analyze import sobol as sobol_analyze
        import pandas as pd
        from functools import partial
        import time
    except ImportError as e:
        raise ImportError(f'Sensitivity analysis requires dependencies: (SALib, pandas, functools).') from e
    start_time = time.time()
    
    # define input parameters and their ranges
    bounds = np.array(bounds)
    n_params = bounds.shape[0]
    if param_names == None:
        param_names = range(0,n_params)

    # define problem
    problem = {
        'num_vars': n_params,
        'names': param_names,
        'bounds' : bounds
        }

    # generate samples
    if verbose:
        print(f'Generating {n_samples:,.0f} Sobol samples for sensitivity analysis.')
    param_values = sobol_sample.sample(problem, n_samples)

    # kwargs for the objective function
    if kwargs:
        func = partial(func, **kwargs)
        
    # evaluate the samples
    values = func(param_values) 
    
    # running sensitivity analysis
    print('Running sensitivity analysis.')
    Si = sobol_analyze.analyze(problem, values, calc_second_order=True, print_to_console=False)

    # calculate S2 sensitivities
    # convert S2 indices to dataframe to process easier
    S2_matrix = Si['S2']
    S2_df = pd.DataFrame(S2_matrix, index=param_names, columns=param_names)
    S2_df = S2_df.fillna(S2_df.T)
    mask = np.tril(np.ones_like(S2_df, dtype=bool))

    end_time = time.time()
    run_time = end_time - start_time
    if verbose:
        print(f'\nRun time: {run_time:.2f}s')
        # import
        try:
            import matplotlib.pyplot as plt
            import seaborn as sns
        except ImportError as e:
            raise ImportError(f'Plotting requires dependencies: (matplotlib, seaborn).') from e

        # sort by S1 values
        sort_idx = np.argsort(Si['S1'])
        s1_sorted = Si['S1'][sort_idx]
        st_sorted = Si['ST'][sort_idx]
        s1_conf_sorted = Si['S1_conf'][sort_idx]
        st_conf_sorted = Si['ST_conf'][sort_idx]
        names_sorted = [np.array(param_names)[i] for i in sort_idx]
    
        
        bar_width = 0.35
        index = np.arange(n_params)
        
        # plot 1: first-order (S1) and total-order (ST) indices
        sens_plot, axs = plt.subplots(2,1,figsize=(9, 13)) 
        
        # define bar width and positions
        bar_width = 0.35
        index = np.arange(n_params)
        
        # plot S1 (first order) sensitivities
        axs[0].barh(index - bar_width/2, s1_sorted, bar_width,
                   xerr=s1_conf_sorted, 
                   label='First-order ($S_1$)',
                   alpha=1,
                   capsize=2.5)
        
        axs[0].barh(index + bar_width/2, st_sorted, bar_width,
                   xerr=st_conf_sorted, 
                   label='Total-order ($S_T$)',
                   alpha=0.75, 
                   capsize=2.5)

        axs[0].set_title('Sensitivity Indices ($S_1$, $S_T$)')
        if log_scale:
            axs[0].set_xscale('log')
        
        axs[0].set_yticks(index)
        axs[0].set_yticklabels(names_sorted)
        axs[0].legend()
        
        # plot 2: heatmap of second order indices
        sns.heatmap(data=S2_df, mask=mask, cbar_kws={'label': 'Second-order Index ($S_2$)'},ax=axs[1]) # magma
        axs[1].set_title('Second-order Interactions ($S_2$)')
        axs[1].invert_yaxis()
        sens_plot.tight_layout() 
        plt.show()
    
    return Si, S2_matrix