diff --git a/nafuma/auxillary.py b/nafuma/auxillary.py index 8044914..9d30948 100644 --- a/nafuma/auxillary.py +++ b/nafuma/auxillary.py @@ -163,4 +163,23 @@ def swap_values(options: dict, key1, key2): options[k1], options[k2] = options[k2], options[k1] - return options \ No newline at end of file + return options + + + +def find_neighbours(value, df, colname, start=0, end=-1): + ''' Finds closest match to a given value in colname of df. If there is an exact match, returns index of this value. Else, it returns the nearest neighbors (upper and lower)''' + + df = df.iloc[start:end] + + exactmatch = df[df[colname] == value] + if not exactmatch.empty: + return exactmatch.index + else: + lower_df = df[df[colname] < value][colname] + upper_df = df[df[colname] > value][colname] + + lowerneighbour_ind = lower_df.idxmax() + upperneighbour_ind = upper_df.idxmin() + + return [lowerneighbour_ind, upperneighbour_ind] \ No newline at end of file diff --git a/nafuma/eds/io.py b/nafuma/eds/io.py index 8d1b34f..91c8c51 100644 --- a/nafuma/eds/io.py +++ b/nafuma/eds/io.py @@ -1,6 +1,7 @@ from PIL import Image import numpy as np import cv2 +import pandas as pd def read_image(path, weight=None, colour=None, crop=None, resize=None, brightness=None): @@ -125,3 +126,27 @@ def change_colour(image, new_colour): return image + + +def read_spectrum(path): + + headers = find_start(path) + + spectrum = pd.read_csv(path, skiprows=headers, delim_whitespace=True) + + + return spectrum + + + +def find_start(path): + + with open(path, 'r') as f: + line = f.readline() + i = 0 + while not line.startswith('Energy'): + line = f.readline() + i += 1 + + return i + diff --git a/nafuma/eds/plot.py b/nafuma/eds/plot.py index 7726f75..be8ee39 100644 --- a/nafuma/eds/plot.py +++ b/nafuma/eds/plot.py @@ -71,3 +71,65 @@ def show_image(data, options={}): else: return data['image'], None, None + + +def plot_spectrum(data: dict, options={}): + + default_options = { + 'deconvolutions': None, + 'lines': None, + 'colours': None, + 'xlabel': 'Energy', 'xunit': 'keV', 'xlim': None, + 'ylabel': 'Counts', 'yunit': 'arb. u.', 'ylim': None, 'hide_y_ticklabels': True, 'hide_y_ticks': True, + } + + options = aux.update_options(options=options, default_options=default_options) + + fig, ax = btp.prepare_plot(options=options) + + + spectrum = io.read_spectrum(data['path']) + + if options['deconvolutions']: + + deconvolutions = [] + if not isinstance(options['deconvolutions'], list): + options['deconvolutions'] = [options['deconvolutions']] + + if options['colours'] and (len(options['colours']) != len(options['deconvolutions'])): + options['colours'] = None + + for deconv in options['deconvolutions']: + df = io.read_spectrum(deconv) + deconvolutions.append(df) + + + + spectrum.plot(x='Energy', y='Counts', ax=ax, color='black') + + if options['deconvolutions']: + if options['colours']: + for deconv, colour in zip(deconvolutions, options['colours']): + ax.fill_between(x=deconv['Energy'], y1=deconv['Counts'], y2=0, color=colour, alpha=0.4) + else: + for deconv in deconvolutions: + ax.fill_between(x=deconv['Energy'], y1=deconv['Counts'], y2=0, alpha=0.4) + + + if not options['xlim']: + options['xlim'] = [spectrum['Energy'].min(), spectrum['Energy'].max()] + + if not options['ylim']: + options['ylim'] = [0, 1.1*spectrum['Counts'].max()] + + if options['lines']: + for i, (line, energy) in enumerate(options['lines'].items()): + ax.axvline(x=energy, ls='--', lw=0.5, c='black') + ax.text(s=line, x=energy, y=(0.9-0.1*i)*options['ylim'][1], fontsize=8) + + + + fig, ax = btp.adjust_plot(fig=fig, ax=ax, options=options) + + + return spectrum, fig, ax \ No newline at end of file diff --git a/nafuma/electrochemistry/io.py b/nafuma/electrochemistry/io.py index d46a1d5..3bc93d0 100644 --- a/nafuma/electrochemistry/io.py +++ b/nafuma/electrochemistry/io.py @@ -41,11 +41,10 @@ def read_neware(path, options={}): # Convert from .xlsx to .csv to make readtime faster if path.endswith('xlsx'): csv_details = ''.join(path[:-5]) + '_details.csv' - csv_summary = ''.join(path[:-5]) + '_summary.csv' + csv_summary = os.path.abspath(''.join(path[:-5]) + '_summary.csv') - if not os.path.isfile(csv_summary): - Xlsx2csv(path, outputencoding="utf-8").convert(csv_summary, sheetid=3) + Xlsx2csv(path, outputencoding="utf-8").convert(os.path.abspath(csv_summary), sheetid=3) if not os.path.isfile(csv_details): Xlsx2csv(path, outputencoding="utf-8").convert(csv_details, sheetid=4) @@ -357,6 +356,12 @@ def process_neware_data(df, options={}): max_capacity = dchg_df['ions'].max() dchg_df['ions'] = np.abs(dchg_df['ions'] - max_capacity) + + if not chg_df.empty: + chg_df.reset_index(inplace=True) + if not dchg_df.empty: + dchg_df.reset_index(inplace=True) + cycles.append((chg_df, dchg_df)) @@ -426,15 +431,17 @@ def process_biologic_data(df, options=None): df = df[headers].copy() + # Complete set of new units and get the units used in the dataset, and convert values in the DataFrame from old to new. set_units(options) options['old_units'] = get_old_units(df=df, options=options) df = add_columns(df=df, options=options) - df = unit_conversion(df=df, options=options) + df = unit_conversion(df=df, options=options) + # Creates masks for charge and discharge curves if options['mode'] == 'GC': chg_mask = (df['status'] == 1) & (df['status_change'] != 1) @@ -447,8 +454,13 @@ def process_biologic_data(df, options=None): # Initiate cycles list cycles = [] + if df['cycle'].max() == 0: + no_cycles = 1 + else: + no_cycles = int(df['cycle'].max()) + # Loop through all the cycling steps, change the current and capacities in the - for i in range(int(df["cycle"].max())): + for i in range(no_cycles): sub_df = df.loc[df['cycle'] == i].copy() diff --git a/nafuma/electrochemistry/plot.py b/nafuma/electrochemistry/plot.py index f8ef088..9ab421f 100644 --- a/nafuma/electrochemistry/plot.py +++ b/nafuma/electrochemistry/plot.py @@ -23,7 +23,6 @@ def plot_gc(data, options=None): # Update options - required_options = ['force_reload', 'x_vals', 'y_vals', 'which_cycles', 'limit', 'exclude_cycles', 'show_plot', 'summary', 'charge', 'discharge', 'colours', 'differentiate_charge_discharge', 'gradient', 'interactive', 'interactive_session_active', 'rc_params', 'format_params', 'save_gif', 'save_path', 'fps'] default_options = { 'force_reload': False, 'x_vals': 'capacity', 'y_vals': 'voltage', @@ -34,6 +33,7 @@ def plot_gc(data, options=None): 'summary': False, 'charge': True, 'discharge': True, 'colours': None, + 'markers': None, 'differentiate_charge_discharge': True, 'gradient': False, 'interactive': False, @@ -42,17 +42,19 @@ def plot_gc(data, options=None): 'format_params': {}, 'save_gif': False, 'save_path': 'animation.gif', - 'fps': 1 + 'fps': 1, + 'fig': None, 'ax': None, + 'edgecolor': plt.rcParams['lines.markeredgecolor'], + 'plot_every': 1, } - options = aux.update_options(options=options, required_options=required_options, default_options=default_options) + options = aux.update_options(options=options, default_options=default_options) # Read data if not already loaded if not 'cycles' in data.keys() or options['force_reload']: data['cycles'] = ec.io.read_data(data=data, options=options) - # Update list of cycles to correct indices update_cycles_list(data=data, options=options) @@ -63,7 +65,9 @@ def plot_gc(data, options=None): - colours = generate_colours(cycles=data['cycles'], options=options) + colours = generate_colours(options=options) + markers = generate_markers(options=options) + if not options['summary']: if options['show_plot']: @@ -74,13 +78,13 @@ def plot_gc(data, options=None): else: fig, ax = options['fig'], options['ax'] - for i, cycle in enumerate(data['cycles']): - if i in options['which_cycles']: + + for i, cycle in enumerate(options['which_cycles']): if options['charge']: - cycle[0].plot(x=options['x_vals'], y=options['y_vals'], ax=ax, c=colours[i][0]) + data['cycles'][cycle][0].plot(x=options['x_vals'], y=options['y_vals'], ax=ax, c=colours[i][0]) if options['discharge']: - cycle[1].plot(x=options['x_vals'], y=options['y_vals'], ax=ax, c=colours[i][1]) + data['cycles'][cycle][1].plot(x=options['x_vals'], y=options['y_vals'], ax=ax, c=colours[i][1]) if options['interactive_session_active']: @@ -134,7 +138,10 @@ def plot_gc(data, options=None): elif options['summary'] and options['show_plot']: # Prepare plot - fig, ax = btp.prepare_plot(options=options) + if not options['fig'] and not options['ax']: + fig, ax = btp.prepare_plot(options=options) + else: + fig, ax = options['fig'], options['ax'] mask = [] for i in range(data['cycles'].shape[0]): @@ -152,18 +159,18 @@ def plot_gc(data, options=None): # FIXME To begin, the default is that y-values correspond to x-values. This should probably be implemented in more logical and consistent manner in the future. if options['x_vals'] in ['coulombic_efficiency', 'energy_efficiency']: - data['cycles'].loc[mask].plot(x='cycle', y=options['x_vals'], ax=ax, color=colours[0][0], kind='scatter', marker="$\u25EF$", s=plt.rcParams['lines.markersize']) + data['cycles'].loc[mask].plot(x='cycle', y=options['x_vals'], ax=ax, color=colours[0][1], kind='scatter', s=plt.rcParams['lines.markersize']*10, marker=markers[0], edgecolor=plt.rcParams['lines.markeredgecolor']) if options['limit']: ax.axhline(y=options['limit'], ls='--', c='black') else: if options['charge']: yval = 'charge_' + options['x_vals'] - data['cycles'].loc[mask].plot(x='cycle', y=yval, ax=ax, color=colours[0][0], kind='scatter', marker="$\u25EF$", s=plt.rcParams['lines.markersize']) + data['cycles'].loc[mask].plot(x='cycle', y=yval, ax=ax, color=colours[0][0], kind='scatter', s=plt.rcParams['lines.markersize']*10, marker=markers[0], edgecolor=plt.rcParams['lines.markeredgecolor']) if options['discharge']: yval = 'discharge_' + options['x_vals'] - data['cycles'].loc[mask].plot(x='cycle', y=yval, ax=ax, color=colours[0][1], kind='scatter', marker="$\u25EF$", s=plt.rcParams['lines.markersize']) + data['cycles'].loc[mask].plot(x='cycle', y=yval, ax=ax, color=colours[0][1], kind='scatter', s=plt.rcParams['lines.markersize']*10, marker=markers[1], edgecolor=plt.rcParams['lines.markeredgecolor']) if options['limit']: @@ -202,7 +209,6 @@ def plot_gc_interactive(data, options): def plot_cv(data, options): # Update options - required_options = ['force_reload', 'x_vals', 'y_vals', 'which_cycles', 'limit', 'exclude_cycles', 'show_plot', 'charge', 'discharge', 'colours', 'differentiate_charge_discharge', 'gradient', 'interactive', 'interactive_session_active', 'rc_params', 'format_params', 'save_gif', 'save_path', 'fps'] default_options = { 'force_reload': False, 'x_vals': 'voltage', 'y_vals': 'current', @@ -220,10 +226,13 @@ def plot_cv(data, options): 'format_params': {}, 'save_gif': False, 'save_path': 'animation.gif', - 'fps': 1 + 'fps': 1, + 'plot_every': 1, + 'fig': None, + 'ax': None } - options = aux.update_options(options=options, required_options=required_options, default_options=default_options) + options = aux.update_options(options=options, default_options=default_options) # Read data if not already loaded @@ -234,18 +243,29 @@ def plot_cv(data, options): # Update list of cycles to correct indices update_cycles_list(data=data, options=options) - colours = generate_colours(cycles=data['cycles'], options=options) + colours = generate_colours(options=options) if options['show_plot']: # Prepare plot - fig, ax = btp.prepare_plot(options=options) - for i, cycle in enumerate(data['cycles']): - if i in options['which_cycles']: - if options['charge']: - cycle[0].plot(x=options['x_vals'], y=options['y_vals'], ax=ax, c=colours[i][0]) + if not options['fig'] and not options['ax']: + fig, ax = btp.prepare_plot(options=options) + else: + fig, ax = options['fig'], options['ax'] - if options['discharge']: - cycle[1].plot(x=options['x_vals'], y=options['y_vals'], ax=ax, c=colours[i][1]) + for i, cycle in enumerate(options['which_cycles']): + if options['charge']: + data['cycles'][cycle][0].plot(x=options['x_vals'], y=options['y_vals'], ax=ax, c=colours[i][0]) + + if options['discharge']: + data['cycles'][cycle][1].plot(x=options['x_vals'], y=options['y_vals'], ax=ax, c=colours[i][1]) + + # for i, cycle in enumerate(data['cycles']): + # if i in options['which_cycles']: + # if options['charge']: + # cycle[0].plot(x=options['x_vals'], y=options['y_vals'], ax=ax, c=colours[i][0]) + + # if options['discharge']: + # cycle[1].plot(x=options['x_vals'], y=options['y_vals'], ax=ax, c=colours[i][1]) update_labels(options) @@ -261,6 +281,7 @@ def plot_cv(data, options): options['format_params']['dpi'] = 200 + for i, cycle in enumerate(data['cycles']): if i in options['which_cycles']: @@ -360,13 +381,15 @@ def update_cycles_list(data, options: dict) -> None: options['which_cycles'] = [i-1 for i in range(which_cycles[0], which_cycles[1]+1)] - for i, cycle in enumerate(options['which_cycles']): if cycle in options['exclude_cycles']: del options['which_cycles'][i] + options['which_cycles'] = options['which_cycles'][::options['plot_every']] + + def prettify_gc_plot(fig, ax, options=None): @@ -581,16 +604,27 @@ def prettify_labels(label): -def generate_colours(cycles, options): +def generate_colours(options): + + default_options = { + 'gradient_colours': None, + } + + aux.update_options(options=options, default_options=default_options) # Assign colours from the options dictionary if it is defined, otherwise use standard colours. if options['colours']: charge_colour = options['colours'][0] discharge_colour = options['colours'][1] + + if isinstance(charge_colour, tuple): + charge_colour = [charge_colour] + if isinstance(discharge_colour, tuple): + discharge_colour = [discharge_colour] else: - charge_colour = (40/255, 70/255, 75/255) # Dark Slate Gray #28464B, coolors.co - discharge_colour = (239/255, 160/255, 11/255) # Marigold #EFA00B, coolors.co + charge_colour = [(40/255, 70/255, 75/255)] # Dark Slate Gray #28464B, coolors.co + discharge_colour = [(239/255, 160/255, 11/255)] # Marigold #EFA00B, coolors.co if not options['differentiate_charge_discharge']: discharge_colour = charge_colour @@ -600,28 +634,56 @@ def generate_colours(cycles, options): # If gradient is enabled, find start and end points for each colour if options['gradient']: - add_charge = min([(1-x)*0.75 for x in charge_colour]) - add_discharge = min([(1-x)*0.75 for x in discharge_colour]) + if not options['gradient_colours']: + + options['gradient_colours'] = [[None, None], [None, None]] + + add_charge = min([(1-x)*0.75 for x in charge_colour]) + add_discharge = min([(1-x)*0.75 for x in discharge_colour]) + + options['gradient_colours'][0][0] = charge_colour + options['gradient_colours'][0][1] = [x+add_charge for x in charge_colour] + + options['gradient_colours'][1][0] = discharge_colour + options['gradient_colours'][1][1] = [x+add_discharge for x in discharge_colour] - charge_colour_start = charge_colour - charge_colour_end = [x+add_charge for x in charge_colour] - discharge_colour_start = discharge_colour - discharge_colour_end = [x+add_discharge for x in discharge_colour] # Generate lists of colours colours = [] - - for cycle_number in range(0, len(cycles)): + + if len(charge_colour) != len(options['which_cycles']): if options['gradient']: - weight_start = (len(cycles) - cycle_number)/len(cycles) - weight_end = cycle_number/len(cycles) + options['number_of_colours'] = len(options['which_cycles']) - charge_colour = [weight_start*start_colour + weight_end*end_colour for start_colour, end_colour in zip(charge_colour_start, charge_colour_end)] - discharge_colour = [weight_start*start_colour + weight_end*end_colour for start_colour, end_colour in zip(discharge_colour_start, discharge_colour_end)] + charge_colours = btp.mix_colours(colour1=options['gradient_colours'][0][0], colour2=options['gradient_colours'][0][1], options=options) + discharge_colours = btp.mix_colours(colour1=options['gradient_colours'][1][0], colour2=options['gradient_colours'][1][1], options=options) + + for chg, dchg in zip(charge_colours, discharge_colours): + colours.append([chg, dchg]) + + else: + for i in options['which_cycles']: + colours.append([charge_colour, discharge_colour]) + + + else: + for chg, dchg in zip(charge_colour, discharge_colour): + colours.append([chg, dchg]) - colours.append([charge_colour, discharge_colour]) return colours + + + +def generate_markers(options): + + if not options['markers']: + markers = ['o', 'v'] + + else: + markers = [options['markers'][0], options['markers'][1]] + + return markers \ No newline at end of file diff --git a/nafuma/plotting.py b/nafuma/plotting.py index 6d9bf09..c0105b5 100644 --- a/nafuma/plotting.py +++ b/nafuma/plotting.py @@ -485,4 +485,30 @@ def make_animation(paths, options={}): frames[0].save(os.path.join(options['save_folder'], options['save_filename']), format='GIF', append_images=frames[1:], save_all=True, duration=(1/options['fps'])*1000, loop=0) - \ No newline at end of file + + + +def mix_colours(colour1, colour2, options): + + default_options = { + 'number_of_colours': 10, + 'weights': None + } + + options = aux.update_options(options=options, default_options=default_options) + + if not options['weights']: + options['weights'] = [x/options['number_of_colours'] for x in range(options['number_of_colours'])] + + colours = [] + for weight in options['weights']: + colour = [] + + for c1, c2 in zip(colour1, colour2): + colour.append(np.round(((1-weight)*c1 + weight*c2), 5)) + + colours.append(colour) + + + return colours + \ No newline at end of file