From 574f633db0f337366b9f81177d975de87cdca35b Mon Sep 17 00:00:00 2001 From: rasmusvt Date: Mon, 1 Aug 2022 14:50:10 +0200 Subject: [PATCH 01/22] Add correct unit conversion of Neware summaries --- nafuma/electrochemistry/io.py | 177 +++++++++++++++++++++++----------- 1 file changed, 120 insertions(+), 57 deletions(-) diff --git a/nafuma/electrochemistry/io.py b/nafuma/electrochemistry/io.py index 1ebc7f8..108c5eb 100644 --- a/nafuma/electrochemistry/io.py +++ b/nafuma/electrochemistry/io.py @@ -1,4 +1,3 @@ -from email.policy import default import pandas as pd import numpy as np import matplotlib.pyplot as plt @@ -50,7 +49,7 @@ def read_neware(path, options={}): else: df = pd.read_csv(csv_details) - elif path.split('.')[-1] == 'csv': + elif path.endswith('csv'): df = pd.read_csv(path) @@ -237,56 +236,66 @@ def process_neware_data(df, options={}): options['kind'] = 'neware' - # 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) # sets options['units'] - options['old_units'] = get_old_units(df=df, options=options) - - df = add_columns(df=df, options=options) # adds columns to the DataFrame if active material weight and/or molecular weight has been passed in options + if not options['summary']: + # 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) # sets options['units'] + options['old_units'] = get_old_units(df=df, options=options) + + df = add_columns(df=df, options=options) # adds columns to the DataFrame if active material weight and/or molecular weight has been passed in options - df = unit_conversion(df=df, options=options) # converts all units from the old units to the desired units + df = unit_conversion(df=df, options=options) # converts all units from the old units to the desired units - # Creates masks for charge and discharge curves - chg_mask = df['status'] == 'CC Chg' - dchg_mask = df['status'] == 'CC DChg' + # Creates masks for charge and discharge curves + chg_mask = df['status'] == 'CC Chg' + dchg_mask = df['status'] == 'CC DChg' - # Initiate cycles list - cycles = [] + # Initiate cycles list + cycles = [] - # Loop through all the cycling steps, change the current and capacities in the - for i in range(df["cycle"].max()): + # Loop through all the cycling steps, change the current and capacities in the + for i in range(df["cycle"].max()): - sub_df = df.loc[df['cycle'] == i+1].copy() + sub_df = df.loc[df['cycle'] == i+1].copy() - #sub_df.loc[dchg_mask, 'current'] *= -1 - #sub_df.loc[dchg_mask, 'capacity'] *= -1 + #sub_df.loc[dchg_mask, 'current'] *= -1 + #sub_df.loc[dchg_mask, 'capacity'] *= -1 - chg_df = sub_df.loc[chg_mask] - dchg_df = sub_df.loc[dchg_mask] + chg_df = sub_df.loc[chg_mask] + dchg_df = sub_df.loc[dchg_mask] - # Continue to next iteration if the charge and discharge DataFrames are empty (i.e. no current) - if chg_df.empty and dchg_df.empty: - continue + # Continue to next iteration if the charge and discharge DataFrames are empty (i.e. no current) + if chg_df.empty and dchg_df.empty: + continue - # Reverses the discharge curve if specified - if options['reverse_discharge']: - max_capacity = dchg_df['capacity'].max() - dchg_df['capacity'] = np.abs(dchg_df['capacity'] - max_capacity) + # Reverses the discharge curve if specified + if options['reverse_discharge']: + max_capacity = dchg_df['capacity'].max() + dchg_df['capacity'] = np.abs(dchg_df['capacity'] - max_capacity) - if 'specific_capacity' in df.columns: - max_capacity = dchg_df['specific_capacity'].max() - dchg_df['specific_capacity'] = np.abs(dchg_df['specific_capacity'] - max_capacity) + if 'specific_capacity' in df.columns: + max_capacity = dchg_df['specific_capacity'].max() + dchg_df['specific_capacity'] = np.abs(dchg_df['specific_capacity'] - max_capacity) - if 'ions' in df.columns: - max_capacity = dchg_df['ions'].max() - dchg_df['ions'] = np.abs(dchg_df['ions'] - max_capacity) + if 'ions' in df.columns: + max_capacity = dchg_df['ions'].max() + dchg_df['ions'] = np.abs(dchg_df['ions'] - max_capacity) - cycles.append((chg_df, dchg_df)) + cycles.append((chg_df, dchg_df)) + + return cycles + elif options['summary']: + set_units(options=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) + + return df - return cycles def process_biologic_data(df, options=None): @@ -403,30 +412,84 @@ def unit_conversion(df, options): if options['kind'] == 'neware': - df['Current({})'.format(options['old_units']['current'])] = df['Current({})'.format(options['old_units']['current'])] * unit_tables.current()[options['old_units']['current']].loc[options['units']['current']] - df['Voltage({})'.format(options['old_units']['voltage'])] = df['Voltage({})'.format(options['old_units']['voltage'])] * unit_tables.voltage()[options['old_units']['voltage']].loc[options['units']['voltage']] - df['Capacity({})'.format(options['old_units']['capacity'])] = df['Capacity({})'.format(options['old_units']['capacity'])] * unit_tables.capacity()[options['old_units']['capacity']].loc[options['units']['capacity']] - df['Energy({})'.format(options['old_units']['energy'])] = df['Energy({})'.format(options['old_units']['energy'])] * unit_tables.energy()[options['old_units']['energy']].loc[options['units']['energy']] - df['CycleTime({})'.format(options['units']['time'])] = df.apply(lambda row : convert_time_string(row['Relative Time(h:min:s.ms)'], unit=options['units']['time']), axis=1) - df['RunTime({})'.format(options['units']['time'])] = df.apply(lambda row : convert_datetime_string(row['Real Time(h:min:s.ms)'], reference=df['Real Time(h:min:s.ms)'].iloc[0], unit=options['units']['time']), axis=1) - columns = ['status', 'jump', 'cycle', 'steps', 'current', 'voltage', 'capacity', 'energy'] + + if options['summary']: + # Add the charge and discharge energy columns to get a single energy column + df[f'Energy({options["old_units"]["energy"]})'] = df[f'Chg Eng({options["old_units"]["energy"]})'] + df[f'DChg Eng({options["old_units"]["energy"]})'] + + df[f'Starting current({options["old_units"]["current"]})'] = df[f'Starting current({options["old_units"]["current"]})'] * unit_tables.current()[options['old_units']['current']].loc[options['units']['current']] + df[f'Start Volt({options["old_units"]["voltage"]})'] = df[f'Start Volt({options["old_units"]["voltage"]})'] * unit_tables.voltage()[options['old_units']['voltage']].loc[options['units']['voltage']] + df[f'Capacity({options["old_units"]["capacity"]})'] = df[f'Capacity({options["old_units"]["capacity"]})'] * unit_tables.capacity()[options['old_units']['capacity']].loc[options['units']['capacity']] + df[f'Energy({options["old_units"]["energy"]})'] = df[f'Energy({options["old_units"]["energy"]})'] * unit_tables.energy()[options['old_units']['energy']].loc[options['units']['energy']] + df[f'CycleTime({options["units"]["time"]})'] = df.apply(lambda row : convert_time_string(row['Relative Time(h:min:s.ms)'], unit=options['units']['time']), axis=1) + df[f'RunTime({options["units"]["time"]})'] = df.apply(lambda row : convert_datetime_string(row['Real Time'], reference=df['Real Time'].iloc[0], ref_time=df[f'CycleTime({options["units"]["time"]})'].iloc[0],unit=options['units']['time']), axis=1) - if 'SpecificCapacity({}/mg)'.format(options['old_units']['capacity']) in df.columns: - df['SpecificCapacity({}/mg)'.format(options['old_units']['capacity'])] = df['SpecificCapacity({}/mg)'.format(options['old_units']['capacity'])] * unit_tables.capacity()[options['old_units']['capacity']].loc[options['units']['capacity']] / unit_tables.mass()['mg'].loc[options['units']["mass"]] - columns.append('specific_capacity') + + # Drop all undesireable columns + df.drop( + [ + 'Chnl', + 'Original step', + f'End Volt({options["old_units"]["voltage"]})', + f'Termination current({options["old_units"]["current"]})', + 'Relative Time(h:min:s.ms)', + 'Real Time', + 'Continuous Time(h:min:s.ms)', + f'Net discharge capacity({options["old_units"]["capacity"]})', + f'Chg Cap({options["old_units"]["capacity"]})', + f'DChg Cap({options["old_units"]["capacity"]})', + f'Net discharge energy({options["old_units"]["energy"]})', + f'Chg Eng({options["old_units"]["energy"]})', + f'DChg Eng({options["old_units"]["energy"]})' + ], + axis=1, inplace=True + ) + + columns = ['cycle', 'steps', 'status', 'voltage', 'current', 'capacity'] + + + + + # Add column labels for specific capacity and ions if they exist + if 'SpecificCapacity({}/mg)'.format(options['old_units']['capacity']) in df.columns: + df['SpecificCapacity({}/mg)'.format(options['old_units']['capacity'])] = df['SpecificCapacity({}/mg)'.format(options['old_units']['capacity'])] * unit_tables.capacity()[options['old_units']['capacity']].loc[options['units']['capacity']] / unit_tables.mass()['mg'].loc[options['units']["mass"]] + columns.append('specific_capacity') if 'IonsExtracted' in df.columns: columns.append('ions') + # Append energy column label here as it was the last column to be generated + columns.append('energy') + columns.append('cycle_time') + columns.append('runtime') + + # Apply new column labels + df.columns = columns + + + else: + df['Current({})'.format(options['old_units']['current'])] = df['Current({})'.format(options['old_units']['current'])] * unit_tables.current()[options['old_units']['current']].loc[options['units']['current']] + df['Voltage({})'.format(options['old_units']['voltage'])] = df['Voltage({})'.format(options['old_units']['voltage'])] * unit_tables.voltage()[options['old_units']['voltage']].loc[options['units']['voltage']] + df['Capacity({})'.format(options['old_units']['capacity'])] = df['Capacity({})'.format(options['old_units']['capacity'])] * unit_tables.capacity()[options['old_units']['capacity']].loc[options['units']['capacity']] + df['Energy({})'.format(options['old_units']['energy'])] = df['Energy({})'.format(options['old_units']['energy'])] * unit_tables.energy()[options['old_units']['energy']].loc[options['units']['energy']] + df['CycleTime({})'.format(options['units']['time'])] = df.apply(lambda row : convert_time_string(row['Relative Time(h:min:s.ms)'], unit=options['units']['time']), axis=1) + df['RunTime({})'.format(options['units']['time'])] = df.apply(lambda row : convert_datetime_string(row['Real Time(h:min:s.ms)'], reference=df['Real Time(h:min:s.ms)'].iloc[0], ref_time=df[f'CycleTime({options["units"]["time"]})'].iloc[0], unit=options['units']['time']), axis=1) + columns = ['status', 'jump', 'cycle', 'steps', 'current', 'voltage', 'capacity', 'energy'] + + if 'SpecificCapacity({}/mg)'.format(options['old_units']['capacity']) in df.columns: + df['SpecificCapacity({}/mg)'.format(options['old_units']['capacity'])] = df['SpecificCapacity({}/mg)'.format(options['old_units']['capacity'])] * unit_tables.capacity()[options['old_units']['capacity']].loc[options['units']['capacity']] / unit_tables.mass()['mg'].loc[options['units']["mass"]] + columns.append('specific_capacity') + + if 'IonsExtracted' in df.columns: + columns.append('ions') + + columns.append('cycle_time') + columns.append('time') - columns.append('cycle_time') - columns.append('time') + df.drop(['Record number', 'Relative Time(h:min:s.ms)', 'Real Time(h:min:s.ms)'], axis=1, inplace=True) - - df.drop(['Record number', 'Relative Time(h:min:s.ms)', 'Real Time(h:min:s.ms)'], axis=1, inplace=True) - - df.columns = columns + df.columns = columns if options['kind'] == 'biologic': df['time/{}'.format(options['old_units']['time'])] = df["time/{}".format(options['old_units']["time"])] * unit_tables.time()[options['old_units']["time"]].loc[options['units']['time']] @@ -488,13 +551,13 @@ def get_old_units(df: pd.DataFrame, options: dict) -> dict: if options['kind']=='neware': for column in df.columns: - if 'Voltage' in column: + if 'Voltage' in column or 'Start Volt' in column: voltage = column.split('(')[-1].strip(')') - elif 'Current' in column: + elif 'Current' in column or 'Starting current' in column: current = column.split('(')[-1].strip(')') elif 'Capacity' in column: capacity = column.split('(')[-1].strip(')') - elif 'Energy' in column: + elif 'Energy' in column or 'Eng' in column: energy = column.split('(')[-1].strip(')') old_units = {'voltage': voltage, 'current': current, 'capacity': capacity, 'energy': energy} @@ -521,7 +584,7 @@ def get_old_units(df: pd.DataFrame, options: dict) -> dict: def convert_time_string(time_string, unit='ms'): ''' Convert time string from Neware-data with the format hh:mm:ss.xx to any given unit''' - h, m, s = time_string.split(':') + h, m, s = time_string.split(':') ms = float(s)*1000 + int(m)*1000*60 + int(h)*1000*60*60 factors = {'ms': 1, 's': 1/1000, 'min': 1/(1000*60), 'h': 1/(1000*60*60)} @@ -532,7 +595,7 @@ def convert_time_string(time_string, unit='ms'): -def convert_datetime_string(datetime_string, reference, unit='s'): +def convert_datetime_string(datetime_string, reference, ref_time, unit='s'): ''' Convert time string from Neware-data with the format yyy-mm-dd hh:mm:ss to any given unit''' from datetime import datetime @@ -555,7 +618,7 @@ def convert_datetime_string(datetime_string, reference, unit='s'): factors = {'ms': 1000, 's': 1, 'min': 1/(60), 'h': 1/(60*60)} - time = s * factors[unit] + time = s * factors[unit] + ref_time return time From 9ebab7d6ee979a7bf61dbdd8e3602d8d70c014c1 Mon Sep 17 00:00:00 2001 From: rasmusvt Date: Mon, 1 Aug 2022 15:47:46 +0200 Subject: [PATCH 02/22] Add splice cycles for Neware (summary + cycles) --- nafuma/electrochemistry/io.py | 47 +++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/nafuma/electrochemistry/io.py b/nafuma/electrochemistry/io.py index 108c5eb..71fa93b 100644 --- a/nafuma/electrochemistry/io.py +++ b/nafuma/electrochemistry/io.py @@ -206,6 +206,47 @@ def splice_cycles(df, options: dict) -> pd.DataFrame: add = df['specific_capacity'].iloc[i-1] df['specific_capacity'].iloc[i:last_chg] = df['specific_capacity'].iloc[i:last_chg] + add + + if options['kind'] == 'neware': + + if options['summary']: + for i in range(df['cycle'].max()): + sub_df = df.loc[df['cycle'] == i+1].copy() + + if sub_df['status'].loc[sub_df['status'] == 'CC Chg'].count() > 1: + indices = sub_df.index[sub_df['status'] == 'CC Chg'] + + add_columns = ['capacity', 'specific_capacity', 'ions', 'energy', 'cycle_time'] + + for column in add_columns: + if column in df.columns: + df[column].iloc[indices[-1]] = df[column].iloc[indices[-1]] + df[column].iloc[indices[0]] + + df.drop(index=indices[0], inplace=True) + df.reset_index(inplace=True, drop=True) + + else: + for i in range(df['cycle'].max()): + sub_df = df.loc[df['cycle'] == i+1].copy() + sub_chg_df = sub_df.loc[sub_df['status'] == 'CC Chg'].copy() + + steps_indices = sub_chg_df['steps'].unique() + + if len(steps_indices) > 1: + + add_columns = ['capacity', 'specific_capacity', 'ions', 'energy', 'cycle_time'] + + for column in add_columns: + if column in df.columns: + # Extract the maximum value from the first of the two cycles by accessing the column value of the highest index of the first cycle + add = df[column].iloc[df.loc[df['steps'] == steps_indices[0]].index.max()] + + df[column].loc[df['steps'] == steps_indices[1]] += add + + + + + return df @@ -245,6 +286,9 @@ def process_neware_data(df, options={}): df = unit_conversion(df=df, options=options) # converts all units from the old units to the desired units + if options['splice_cycles']: + df = splice_cycles(df=df, options=options) + # Creates masks for charge and discharge curves chg_mask = df['status'] == 'CC Chg' @@ -294,6 +338,9 @@ def process_neware_data(df, options={}): df = add_columns(df=df, options=options) df = unit_conversion(df=df, options=options) + if options['splice_cycles']: + df = splice_cycles(df=df, options=options) + return df From 0699399d1a1592202c3eda856e91f6a9d57e4f61 Mon Sep 17 00:00:00 2001 From: rasmusvt Date: Mon, 1 Aug 2022 16:37:37 +0200 Subject: [PATCH 03/22] Mix intervals and ints in update_cycles --- nafuma/electrochemistry/plot.py | 90 ++++++++++++++++++++------------- 1 file changed, 55 insertions(+), 35 deletions(-) diff --git a/nafuma/electrochemistry/plot.py b/nafuma/electrochemistry/plot.py index 9e80471..7b1f000 100644 --- a/nafuma/electrochemistry/plot.py +++ b/nafuma/electrochemistry/plot.py @@ -19,10 +19,11 @@ def plot_gc(data, options=None): # Update options - required_options = ['x_vals', 'y_vals', 'which_cycles', 'charge', 'discharge', 'colours', 'differentiate_charge_discharge', 'gradient', 'interactive', 'interactive_session_active', 'rc_params', 'format_params'] + required_options = ['x_vals', 'y_vals', 'which_cycles', 'exclude_cycles', 'charge', 'discharge', 'colours', 'differentiate_charge_discharge', 'gradient', 'interactive', 'interactive_session_active', 'rc_params', 'format_params'] default_options = { 'x_vals': 'capacity', 'y_vals': 'voltage', - 'which_cycles': 'all', + 'which_cycles': 'all', + 'exclude_cycles': [], 'charge': True, 'discharge': True, 'colours': None, 'differentiate_charge_discharge': True, @@ -35,43 +36,45 @@ def plot_gc(data, options=None): options = aux.update_options(options=options, required_options=required_options, default_options=default_options) - - if not 'cycles' in data.keys(): data['cycles'] = ec.io.read_data(data=data, options=options) - # Update list of cycles to correct indices - update_cycles_list(cycles=data['cycles'], options=options) - - colours = generate_colours(cycles=data['cycles'], options=options) - - if options['interactive']: - options['interactive'], options['interactive_session_active'] = False, True - plot_gc_interactive(data=data, options=options) - return - - - # Prepare plot, and read and process data - fig, ax = btp.prepare_plot(options=options) + if not options['summary']: + # Update list of cycles to correct indices + update_cycles_list(data=data, 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]) + colours = generate_colours(cycles=data['cycles'], options=options) - if options['discharge']: - cycle[1].plot(x=options['x_vals'], y=options['y_vals'], ax=ax, c=colours[i][1]) + if options['interactive']: + options['interactive'], options['interactive_session_active'] = False, True + plot_gc_interactive(data=data, options=options) + return - if options['interactive_session_active']: - update_labels(options, force=True) - else: - update_labels(options) + # Prepare plot, and read and process data + + fig, ax = btp.prepare_plot(options=options) - fig, ax = btp.adjust_plot(fig=fig, ax=ax, 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 options['interactive_session_active']: + if options['discharge']: + cycle[1].plot(x=options['x_vals'], y=options['y_vals'], ax=ax, c=colours[i][1]) + + + if options['interactive_session_active']: + update_labels(options, force=True) + else: + update_labels(options) + + fig, ax = btp.adjust_plot(fig=fig, ax=ax, options=options) + + elif options['summary']: + + fig, ax = 0, 0 return data['cycles'], fig, ax @@ -118,29 +121,46 @@ def update_labels(options, force=False): -def update_cycles_list(cycles, options: dict) -> None: +def update_cycles_list(data, options: dict) -> None: if options['which_cycles'] == 'all': - options['which_cycles'] = [i for i in range(len(cycles))] + options['which_cycles'] = [i for i in range(len(data['cycles']))] - elif type(options['which_cycles']) == list: - options['which_cycles'] = [i-1 for i in options['which_cycles']] + elif isinstance(options['which_cycles'], list): + + cycles =[] + + for cycle in options['which_cycles']: + if isinstance(cycle, int): + cycles.append(cycle-1) + + elif isinstance(cycle, tuple): + interval = [i-1 for i in range(cycle[0], cycle[1]+1)] + cycles.extend(interval) + + + options['which_cycles'] = cycles # Tuple is used to define an interval - as elements tuples can't be assigned, I convert it to a list here. - elif type(options['which_cycles']) == tuple: + elif isinstance(options['which_cycles'], tuple): which_cycles = list(options['which_cycles']) if which_cycles[0] <= 0: which_cycles[0] = 1 elif which_cycles[1] < 0: - which_cycles[1] = len(cycles) + which_cycles[1] = len(options['which_cycles']) 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] + From 224d05e0e95b8137ae25f1ccf7bb9fcc79461ede Mon Sep 17 00:00:00 2001 From: rasmusvt Date: Mon, 1 Aug 2022 17:00:10 +0200 Subject: [PATCH 04/22] Return summary as separte chg / dchg dataframes --- nafuma/electrochemistry/io.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/nafuma/electrochemistry/io.py b/nafuma/electrochemistry/io.py index 71fa93b..b4213ee 100644 --- a/nafuma/electrochemistry/io.py +++ b/nafuma/electrochemistry/io.py @@ -52,7 +52,6 @@ def read_neware(path, options={}): elif path.endswith('csv'): df = pd.read_csv(path) - return df @@ -243,10 +242,6 @@ def splice_cycles(df, options: dict) -> pd.DataFrame: df[column].loc[df['steps'] == steps_indices[1]] += add - - - - return df @@ -297,14 +292,13 @@ def process_neware_data(df, options={}): # Initiate cycles list cycles = [] + + # Loop through all the cycling steps, change the current and capacities in the for i in range(df["cycle"].max()): sub_df = df.loc[df['cycle'] == i+1].copy() - #sub_df.loc[dchg_mask, 'current'] *= -1 - #sub_df.loc[dchg_mask, 'capacity'] *= -1 - chg_df = sub_df.loc[chg_mask] dchg_df = sub_df.loc[dchg_mask] @@ -328,7 +322,8 @@ def process_neware_data(df, options={}): cycles.append((chg_df, dchg_df)) - return cycles + + return cycles elif options['summary']: @@ -341,7 +336,13 @@ def process_neware_data(df, options={}): if options['splice_cycles']: df = splice_cycles(df=df, options=options) - return df + + chg_df = df.loc[df['status'] == 'CC Chg'] + dchg_df = df.loc[df['status'] == 'CC DChg'] + + cycles = [chg_df, dchg_df] + + return cycles From 20111a745786c5d1b5d3252e2ea84527e67014d0 Mon Sep 17 00:00:00 2001 From: rasmusvt Date: Mon, 1 Aug 2022 17:00:26 +0200 Subject: [PATCH 05/22] Allow plotting of summary through plot_gc() --- nafuma/electrochemistry/plot.py | 37 +++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/nafuma/electrochemistry/plot.py b/nafuma/electrochemistry/plot.py index 7b1f000..2d26b4b 100644 --- a/nafuma/electrochemistry/plot.py +++ b/nafuma/electrochemistry/plot.py @@ -36,26 +36,26 @@ def plot_gc(data, options=None): options = aux.update_options(options=options, required_options=required_options, default_options=default_options) + # Read data if not already loaded if not 'cycles' in data.keys(): data['cycles'] = ec.io.read_data(data=data, options=options) + # Update list of cycles to correct indices + update_cycles_list(data=data, options=options) + + if options['interactive']: + options['interactive'], options['interactive_session_active'] = False, True + plot_gc_interactive(data=data, options=options) + return + + + # Prepare plot + fig, ax = btp.prepare_plot(options=options) + colours = generate_colours(cycles=data['cycles'], options=options) + if not options['summary']: - # Update list of cycles to correct indices - update_cycles_list(data=data, options=options) - - colours = generate_colours(cycles=data['cycles'], options=options) - - if options['interactive']: - options['interactive'], options['interactive_session_active'] = False, True - plot_gc_interactive(data=data, options=options) - return - - - # Prepare plot, and read and process data - fig, ax = btp.prepare_plot(options=options) - for i, cycle in enumerate(data['cycles']): if i in options['which_cycles']: if options['charge']: @@ -70,13 +70,18 @@ def plot_gc(data, options=None): else: update_labels(options) - fig, ax = btp.adjust_plot(fig=fig, ax=ax, options=options) elif options['summary']: - fig, ax = 0, 0 + # 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['charge']: + data['cycles'][0].plot(x='cycle', y=options['x_vals'], ax=ax, color=colours[0][0], kind='scatter', s=plt.rcParams['lines.markersize']) + data['cycles'][1].plot(x='cycle', y=options['x_vals'], ax=ax, color=colours[0][1], kind='scatter', s=plt.rcParams['lines.markersize']) + + fig, ax = btp.adjust_plot(fig=fig, ax=ax, options=options) + return data['cycles'], fig, ax From 0e053ea1e2de77e403900ce13cf95b6b2119da38 Mon Sep 17 00:00:00 2001 From: rasmusvt Date: Tue, 2 Aug 2022 10:03:49 +0200 Subject: [PATCH 06/22] Make ticks face in by default --- nafuma/plotting.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/nafuma/plotting.py b/nafuma/plotting.py index 4bf0ed9..367b74c 100644 --- a/nafuma/plotting.py +++ b/nafuma/plotting.py @@ -182,8 +182,13 @@ def adjust_plot(fig, ax, options): # Hide x- and y-ticks: if options['hide_y_ticks']: ax.tick_params(axis='y', direction='in', which='both', left=False, right=False) + else: + ax.tick_params(axis='y', direction='in', which='both', left=True, right=True) + if options['hide_x_ticks']: ax.tick_params(axis='x', direction='in', which='both', bottom=False, top=False) + else: + ax.tick_params(axis='x', direction='in', which='both', bottom=True, top=True) From 130e1206903ae7740ebdc9bc00c6d3c6db30cb0c Mon Sep 17 00:00:00 2001 From: rasmusvt Date: Tue, 2 Aug 2022 10:04:49 +0200 Subject: [PATCH 07/22] Chg and dchg of summary can be plotted independent --- nafuma/electrochemistry/plot.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/nafuma/electrochemistry/plot.py b/nafuma/electrochemistry/plot.py index 2d26b4b..4a3a1ee 100644 --- a/nafuma/electrochemistry/plot.py +++ b/nafuma/electrochemistry/plot.py @@ -75,8 +75,17 @@ 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['charge']: - data['cycles'][0].plot(x='cycle', y=options['x_vals'], ax=ax, color=colours[0][0], kind='scatter', s=plt.rcParams['lines.markersize']) - data['cycles'][1].plot(x='cycle', y=options['x_vals'], ax=ax, color=colours[0][1], kind='scatter', s=plt.rcParams['lines.markersize']) + data['cycles'][0].plot(x='cycle', y=options['x_vals'], ax=ax, color=colours[0][0], kind='scatter', marker="$\u25EF$", s=plt.rcParams['lines.markersize']) + # + + if options['discharge']: + data['cycles'][1].plot(x='cycle', y=options['x_vals'], ax=ax, color=colours[0][1], kind='scatter', marker="$\u25EF$", s=plt.rcParams['lines.markersize']) + + + if options['interactive_session_active']: + update_labels(options, force=True) + else: + update_labels(options) From 3a6a000b14c89ceb64366a6084d9681b6a005156 Mon Sep 17 00:00:00 2001 From: rasmusvt Date: Tue, 2 Aug 2022 13:24:39 +0200 Subject: [PATCH 08/22] Allow older formats and manual incrementing of cycles --- nafuma/electrochemistry/io.py | 67 ++++++++++++++++++++++------------- 1 file changed, 42 insertions(+), 25 deletions(-) diff --git a/nafuma/electrochemistry/io.py b/nafuma/electrochemistry/io.py index b4213ee..328cf24 100644 --- a/nafuma/electrochemistry/io.py +++ b/nafuma/electrochemistry/io.py @@ -258,14 +258,15 @@ def process_neware_data(df, options={}): active_materiale_weight: weight of the active material (in mg) used in the cell. molecular_weight: the molar mass (in g mol^-1) of the active material, to calculate the number of ions extracted. Assumes one electron per Li+/Na+-ion """ - required_options = ['units', 'active_material_weight', 'molecular_weight', 'reverse_discharge', 'splice_cycles'] + required_options = ['units', 'active_material_weight', 'molecular_weight', 'reverse_discharge', 'splice_cycles', 'increment_cycles_from'] default_options = { 'units': None, 'active_material_weight': None, 'molecular_weight': None, 'reverse_discharge': False, - 'splice_cycles': None} + 'splice_cycles': None, + 'increment_cycles_from': None} # index aux.update_options(options=options, required_options=required_options, default_options=default_options) @@ -281,6 +282,9 @@ def process_neware_data(df, options={}): df = unit_conversion(df=df, options=options) # converts all units from the old units to the desired units + if options['increment_cycles_from']: + df['cycle'].iloc[options['increment_cycles_from']:] += 1 + if options['splice_cycles']: df = splice_cycles(df=df, options=options) @@ -460,6 +464,13 @@ def unit_conversion(df, options): if options['kind'] == 'neware': + + + record_number = 'Data serial number' if 'Data serial number' in df.columns else 'Record number' + relative_time = 'Relative Time(h:min:s.ms)' if 'Relative Time(h:min:s.ms)' in df.columns else 'Relative Time' + continuous_time = 'Continuous Time(h:min:s.ms)' if 'Continuous Time(h:min:s.ms)' in df.columns else 'Continuous Time' + real_time = 'Real Time(h:min:s:ms)' if 'Real Time(h:min:s:ms)' in df.columns else 'Real Time' + if options['summary']: # Add the charge and discharge energy columns to get a single energy column @@ -469,29 +480,31 @@ def unit_conversion(df, options): df[f'Start Volt({options["old_units"]["voltage"]})'] = df[f'Start Volt({options["old_units"]["voltage"]})'] * unit_tables.voltage()[options['old_units']['voltage']].loc[options['units']['voltage']] df[f'Capacity({options["old_units"]["capacity"]})'] = df[f'Capacity({options["old_units"]["capacity"]})'] * unit_tables.capacity()[options['old_units']['capacity']].loc[options['units']['capacity']] df[f'Energy({options["old_units"]["energy"]})'] = df[f'Energy({options["old_units"]["energy"]})'] * unit_tables.energy()[options['old_units']['energy']].loc[options['units']['energy']] - df[f'CycleTime({options["units"]["time"]})'] = df.apply(lambda row : convert_time_string(row['Relative Time(h:min:s.ms)'], unit=options['units']['time']), axis=1) - df[f'RunTime({options["units"]["time"]})'] = df.apply(lambda row : convert_datetime_string(row['Real Time'], reference=df['Real Time'].iloc[0], ref_time=df[f'CycleTime({options["units"]["time"]})'].iloc[0],unit=options['units']['time']), axis=1) + df[f'CycleTime({options["units"]["time"]})'] = df.apply(lambda row : convert_time_string(row[relative_time], unit=options['units']['time']), axis=1) + df[f'RunTime({options["units"]["time"]})'] = df.apply(lambda row : convert_datetime_string(row[real_time], reference=df[real_time].iloc[0], ref_time=df[f'CycleTime({options["units"]["time"]})'].iloc[0],unit=options['units']['time']), axis=1) + + droplist = [ + 'Chnl', + 'Original step', + f'End Volt({options["old_units"]["voltage"]})', + f'Termination current({options["old_units"]["current"]})', + relative_time, + real_time, + continuous_time, + f'Net discharge capacity({options["old_units"]["capacity"]})', + f'Chg Cap({options["old_units"]["capacity"]})', + f'DChg Cap({options["old_units"]["capacity"]})', + f'Net discharge energy({options["old_units"]["energy"]})', + f'Chg Eng({options["old_units"]["energy"]})', + f'DChg Eng({options["old_units"]["energy"]})' + ] + # Drop all undesireable columns - df.drop( - [ - 'Chnl', - 'Original step', - f'End Volt({options["old_units"]["voltage"]})', - f'Termination current({options["old_units"]["current"]})', - 'Relative Time(h:min:s.ms)', - 'Real Time', - 'Continuous Time(h:min:s.ms)', - f'Net discharge capacity({options["old_units"]["capacity"]})', - f'Chg Cap({options["old_units"]["capacity"]})', - f'DChg Cap({options["old_units"]["capacity"]})', - f'Net discharge energy({options["old_units"]["energy"]})', - f'Chg Eng({options["old_units"]["energy"]})', - f'DChg Eng({options["old_units"]["energy"]})' - ], - axis=1, inplace=True - ) + for drop in droplist: + if drop in df.columns: + df.drop(drop, axis=1, inplace=True) columns = ['cycle', 'steps', 'status', 'voltage', 'current', 'capacity'] @@ -520,8 +533,8 @@ def unit_conversion(df, options): df['Voltage({})'.format(options['old_units']['voltage'])] = df['Voltage({})'.format(options['old_units']['voltage'])] * unit_tables.voltage()[options['old_units']['voltage']].loc[options['units']['voltage']] df['Capacity({})'.format(options['old_units']['capacity'])] = df['Capacity({})'.format(options['old_units']['capacity'])] * unit_tables.capacity()[options['old_units']['capacity']].loc[options['units']['capacity']] df['Energy({})'.format(options['old_units']['energy'])] = df['Energy({})'.format(options['old_units']['energy'])] * unit_tables.energy()[options['old_units']['energy']].loc[options['units']['energy']] - df['CycleTime({})'.format(options['units']['time'])] = df.apply(lambda row : convert_time_string(row['Relative Time(h:min:s.ms)'], unit=options['units']['time']), axis=1) - df['RunTime({})'.format(options['units']['time'])] = df.apply(lambda row : convert_datetime_string(row['Real Time(h:min:s.ms)'], reference=df['Real Time(h:min:s.ms)'].iloc[0], ref_time=df[f'CycleTime({options["units"]["time"]})'].iloc[0], unit=options['units']['time']), axis=1) + df['CycleTime({})'.format(options['units']['time'])] = df.apply(lambda row : convert_time_string(row[relative_time], unit=options['units']['time']), axis=1) + df['RunTime({})'.format(options['units']['time'])] = df.apply(lambda row : convert_datetime_string(row[real_time], reference=df[real_time].iloc[0], ref_time=df[f'CycleTime({options["units"]["time"]})'].iloc[0], unit=options['units']['time']), axis=1) columns = ['status', 'jump', 'cycle', 'steps', 'current', 'voltage', 'capacity', 'energy'] if 'SpecificCapacity({}/mg)'.format(options['old_units']['capacity']) in df.columns: @@ -535,7 +548,11 @@ def unit_conversion(df, options): columns.append('time') - df.drop(['Record number', 'Relative Time(h:min:s.ms)', 'Real Time(h:min:s.ms)'], axis=1, inplace=True) + droplist = [record_number, relative_time, real_time] + + for drop in droplist: + if drop in df.columns: + df.drop(drop, axis=1, inplace=True) df.columns = columns From 116e65e0e1649d73184d7c23ad8a51d2b5e5ee16 Mon Sep 17 00:00:00 2001 From: rasmusvt Date: Tue, 2 Aug 2022 13:24:55 +0200 Subject: [PATCH 09/22] Make 'which_cycles' work for summaries --- nafuma/electrochemistry/plot.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/nafuma/electrochemistry/plot.py b/nafuma/electrochemistry/plot.py index 4a3a1ee..ee8284c 100644 --- a/nafuma/electrochemistry/plot.py +++ b/nafuma/electrochemistry/plot.py @@ -73,13 +73,21 @@ def plot_gc(data, options=None): elif options['summary']: + mask = [] + for i in range(data['cycles'][0].shape[0]): + if i+1 in options['which_cycles']: + mask.append(True) + else: + mask.append(False) + + # 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['charge']: - data['cycles'][0].plot(x='cycle', y=options['x_vals'], ax=ax, color=colours[0][0], kind='scatter', marker="$\u25EF$", s=plt.rcParams['lines.markersize']) + data['cycles'][0].loc[mask].plot(x='cycle', y=options['x_vals'], ax=ax, color=colours[0][0], kind='scatter', marker="$\u25EF$", s=plt.rcParams['lines.markersize']) # if options['discharge']: - data['cycles'][1].plot(x='cycle', y=options['x_vals'], ax=ax, color=colours[0][1], kind='scatter', marker="$\u25EF$", s=plt.rcParams['lines.markersize']) + data['cycles'][1].loc[mask].plot(x='cycle', y=options['x_vals'], ax=ax, color=colours[0][1], kind='scatter', marker="$\u25EF$", s=plt.rcParams['lines.markersize']) if options['interactive_session_active']: From e6a4e2c81f5c72544d3894b925fa4faf8eac520f Mon Sep 17 00:00:00 2001 From: rasmusvt Date: Tue, 2 Aug 2022 13:48:29 +0200 Subject: [PATCH 10/22] Allow deletion of certain datapoints from the datasets --- nafuma/electrochemistry/io.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/nafuma/electrochemistry/io.py b/nafuma/electrochemistry/io.py index 328cf24..d3d6ad9 100644 --- a/nafuma/electrochemistry/io.py +++ b/nafuma/electrochemistry/io.py @@ -266,7 +266,9 @@ def process_neware_data(df, options={}): 'molecular_weight': None, 'reverse_discharge': False, 'splice_cycles': None, - 'increment_cycles_from': None} # index + 'increment_cycles_from': None,# index + 'delete_datapoints': None, # list of indices + } aux.update_options(options=options, required_options=required_options, default_options=default_options) @@ -282,9 +284,15 @@ def process_neware_data(df, options={}): df = unit_conversion(df=df, options=options) # converts all units from the old units to the desired units + print(df.iloc[1:10]) + if options['increment_cycles_from']: df['cycle'].iloc[options['increment_cycles_from']:] += 1 + if options['delete_datapoints']: + for datapoint in options['delete_datapoints']: + df.drop(index=datapoint, inplace=True) + if options['splice_cycles']: df = splice_cycles(df=df, options=options) From 8c20c029ae0c789de2884919a21f9fe78854b30f Mon Sep 17 00:00:00 2001 From: rasmusvt Date: Tue, 2 Aug 2022 13:48:53 +0200 Subject: [PATCH 11/22] Fix bug --- nafuma/electrochemistry/io.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nafuma/electrochemistry/io.py b/nafuma/electrochemistry/io.py index d3d6ad9..a29d4a0 100644 --- a/nafuma/electrochemistry/io.py +++ b/nafuma/electrochemistry/io.py @@ -258,7 +258,7 @@ def process_neware_data(df, options={}): active_materiale_weight: weight of the active material (in mg) used in the cell. molecular_weight: the molar mass (in g mol^-1) of the active material, to calculate the number of ions extracted. Assumes one electron per Li+/Na+-ion """ - required_options = ['units', 'active_material_weight', 'molecular_weight', 'reverse_discharge', 'splice_cycles', 'increment_cycles_from'] + required_options = ['units', 'active_material_weight', 'molecular_weight', 'reverse_discharge', 'splice_cycles', 'increment_cycles_from', 'delete_datapoints'] default_options = { 'units': None, From 43663331f19addc7e97d27b8fa061dc44697b52d Mon Sep 17 00:00:00 2001 From: rasmusvt Date: Tue, 2 Aug 2022 13:49:09 +0200 Subject: [PATCH 12/22] Remove print statement --- nafuma/electrochemistry/io.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/nafuma/electrochemistry/io.py b/nafuma/electrochemistry/io.py index a29d4a0..eec472b 100644 --- a/nafuma/electrochemistry/io.py +++ b/nafuma/electrochemistry/io.py @@ -284,8 +284,6 @@ def process_neware_data(df, options={}): df = unit_conversion(df=df, options=options) # converts all units from the old units to the desired units - print(df.iloc[1:10]) - if options['increment_cycles_from']: df['cycle'].iloc[options['increment_cycles_from']:] += 1 From 4269ac5d4661128f2d9d9be823994148f773b999 Mon Sep 17 00:00:00 2001 From: rasmusvt Date: Tue, 2 Aug 2022 21:55:42 +0200 Subject: [PATCH 13/22] Fix formatting error in unit conversion --- nafuma/electrochemistry/io.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nafuma/electrochemistry/io.py b/nafuma/electrochemistry/io.py index eec472b..26867bd 100644 --- a/nafuma/electrochemistry/io.py +++ b/nafuma/electrochemistry/io.py @@ -475,7 +475,7 @@ def unit_conversion(df, options): record_number = 'Data serial number' if 'Data serial number' in df.columns else 'Record number' relative_time = 'Relative Time(h:min:s.ms)' if 'Relative Time(h:min:s.ms)' in df.columns else 'Relative Time' continuous_time = 'Continuous Time(h:min:s.ms)' if 'Continuous Time(h:min:s.ms)' in df.columns else 'Continuous Time' - real_time = 'Real Time(h:min:s:ms)' if 'Real Time(h:min:s:ms)' in df.columns else 'Real Time' + real_time = 'Real Time(h:min:s.ms)' if 'Real Time(h:min:s.ms)' in df.columns else 'Real Time' if options['summary']: From b6780e8a90b63b1c4dbb81676e2d5c22f4e32144 Mon Sep 17 00:00:00 2001 From: rasmusvt Date: Tue, 2 Aug 2022 21:55:55 +0200 Subject: [PATCH 14/22] Add option to make animation of GC plots --- nafuma/electrochemistry/plot.py | 89 ++++++++++++++++++++++++++------- 1 file changed, 70 insertions(+), 19 deletions(-) diff --git a/nafuma/electrochemistry/plot.py b/nafuma/electrochemistry/plot.py index ee8284c..5c150c8 100644 --- a/nafuma/electrochemistry/plot.py +++ b/nafuma/electrochemistry/plot.py @@ -4,6 +4,9 @@ from matplotlib.ticker import (MultipleLocator, FormatStrFormatter,AutoMinorLoca import pandas as pd import numpy as np import math +import os +import shutil +from PIL import Image import ipywidgets as widgets from IPython.display import display @@ -19,11 +22,12 @@ def plot_gc(data, options=None): # Update options - required_options = ['x_vals', 'y_vals', 'which_cycles', 'exclude_cycles', 'charge', 'discharge', 'colours', 'differentiate_charge_discharge', 'gradient', 'interactive', 'interactive_session_active', 'rc_params', 'format_params'] + required_options = ['x_vals', 'y_vals', 'which_cycles', '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 = { 'x_vals': 'capacity', 'y_vals': 'voltage', 'which_cycles': 'all', 'exclude_cycles': [], + 'show_plot': True, 'charge': True, 'discharge': True, 'colours': None, 'differentiate_charge_discharge': True, @@ -31,7 +35,11 @@ def plot_gc(data, options=None): 'interactive': False, 'interactive_session_active': False, 'rc_params': {}, - 'format_params': {}} + 'format_params': {}, + 'save_gif': False, + 'save_path': 'animation.gif', + 'fps': 1 + } options = aux.update_options(options=options, required_options=required_options, default_options=default_options) @@ -50,28 +58,69 @@ def plot_gc(data, options=None): return - # Prepare plot - fig, ax = btp.prepare_plot(options=options) - colours = generate_colours(cycles=data['cycles'], options=options) + colours = generate_colours(cycles=data['cycles'], options=options) if not options['summary']: - 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['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 options['discharge']: - cycle[1].plot(x=options['x_vals'], y=options['y_vals'], ax=ax, c=colours[i][1]) + if options['discharge']: + cycle[1].plot(x=options['x_vals'], y=options['y_vals'], ax=ax, c=colours[i][1]) - if options['interactive_session_active']: - update_labels(options, force=True) - else: - update_labels(options) + if options['interactive_session_active']: + update_labels(options, force=True) + else: + update_labels(options) + + + + if options['save_gif'] and not options['interactive_session_active']: + if not os.path.isdir('tmp'): + os.makedirs('tmp') + + for i, cycle in enumerate(data['cycles']): + if i in options['which_cycles']: + giffig, gifax = btp.prepare_plot(options=options) + + if options['charge']: + cycle[0].plot(x=options['x_vals'], y=options['y_vals'], ax=gifax, c=colours[i][0]) + if options['discharge']: + cycle[1].plot(x=options['x_vals'], y=options['y_vals'], ax=gifax, c=colours[i][1]) + + gifax.text(x=options['xlim'][1]*0.8, y=3, s=f'{i+1}') + update_labels(options) + + giffig, gifax = btp.adjust_plot(fig=giffig, ax=gifax, options=options) + + plt.savefig(os.path.join('tmp', str(i+1).zfill(4)+'.png')) + plt.close() + + + img_paths = [os.path.join('tmp', path) for path in os.listdir('tmp') if path.endswith('png')] + frames = [] + for path in img_paths: + frame = Image.open(path) + frames.append(frame) + + + frames[0].save(options['save_path'], format='GIF', append_images=frames[1:], save_all=True, duration=len(data['cycles'])/options['fps'], loop=0) + + shutil.rmtree('tmp') - elif options['summary']: + + + elif options['summary'] and options['show_plot']: + # Prepare plot + fig, ax = btp.prepare_plot(options=options) + mask = [] for i in range(data['cycles'][0].shape[0]): @@ -97,9 +146,11 @@ def plot_gc(data, options=None): - fig, ax = btp.adjust_plot(fig=fig, ax=ax, options=options) - - return data['cycles'], fig, ax + if options['show_plot']: + fig, ax = btp.adjust_plot(fig=fig, ax=ax, options=options) + return data['cycles'], fig, ax + else: + return data['cycles'], None, None def plot_gc_interactive(data, options): From 642f166d718471616fa44ec9424e93d4bc427631 Mon Sep 17 00:00:00 2001 From: rasmusvt Date: Thu, 4 Aug 2022 18:57:27 +0200 Subject: [PATCH 15/22] Constrain size for GIFs, fix fps and incomp cycles --- nafuma/electrochemistry/plot.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/nafuma/electrochemistry/plot.py b/nafuma/electrochemistry/plot.py index 5c150c8..b77d6f4 100644 --- a/nafuma/electrochemistry/plot.py +++ b/nafuma/electrochemistry/plot.py @@ -85,8 +85,15 @@ def plot_gc(data, options=None): if not os.path.isdir('tmp'): os.makedirs('tmp') + # Scale image to make GIF smaller + options['format_params']['width'] = 7.5 + options['format_params']['height'] = 3 + + options['format_params']['dpi'] = 200 + for i, cycle in enumerate(data['cycles']): if i in options['which_cycles']: + giffig, gifax = btp.prepare_plot(options=options) if options['charge']: @@ -109,8 +116,7 @@ def plot_gc(data, options=None): frame = Image.open(path) frames.append(frame) - - frames[0].save(options['save_path'], format='GIF', append_images=frames[1:], save_all=True, duration=len(data['cycles'])/options['fps'], loop=0) + frames[0].save(options['save_path'], format='GIF', append_images=frames[1:], save_all=True, duration=(1/options['fps'])*1000, loop=0) shutil.rmtree('tmp') @@ -129,6 +135,12 @@ def plot_gc(data, options=None): else: mask.append(False) + if len(mask) > len(data['cycles'][1]): + del mask[-1] + data['cycles'][0].drop(data['cycles'][0].tail(1).index, inplace=True) + + + # 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['charge']: From 745522ed82daef3294b4b116db92e28824551aa3 Mon Sep 17 00:00:00 2001 From: rasmusvt Date: Fri, 5 Aug 2022 09:51:28 +0200 Subject: [PATCH 16/22] Calculate specific energy in Neware data --- nafuma/electrochemistry/io.py | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/nafuma/electrochemistry/io.py b/nafuma/electrochemistry/io.py index 26867bd..9b7b3d6 100644 --- a/nafuma/electrochemistry/io.py +++ b/nafuma/electrochemistry/io.py @@ -342,6 +342,7 @@ def process_neware_data(df, options={}): df = add_columns(df=df, options=options) df = unit_conversion(df=df, options=options) + #df = calculate_efficiency(df=df, options=options) if options['splice_cycles']: df = splice_cycles(df=df, options=options) @@ -427,8 +428,13 @@ def process_biologic_data(df, options=None): def add_columns(df, options): if options['kind'] == 'neware': + + if options['summary']: + df[f'Energy({options["old_units"]["energy"]})'] = np.abs(df[f'Net discharge energy({options["old_units"]["energy"]})']) + if options['active_material_weight']: - df["SpecificCapacity({}/mg)".format(options['old_units']["capacity"])] = df["Capacity({})".format(options['old_units']['capacity'])] / (options['active_material_weight']) + df[f"SpecificCapacity({options['old_units']['capacity']}/mg)"] = df["Capacity({})".format(options['old_units']['capacity'])] / (options['active_material_weight']) + df[f"SpecificEnergy({options['old_units']['energy']}/mg)"] = df["Energy({})".format(options['old_units']['energy'])] / (options['active_material_weight']) if options['molecular_weight']: faradays_constant = 96485.3365 # [F] = C mol^-1 = As mol^-1 @@ -456,6 +462,11 @@ def add_columns(df, options): return df +#def calculate_efficiency(df, options): +# +# df['coulombic_efficiency'] = + + def unit_conversion(df, options): from . import unit_tables @@ -479,9 +490,7 @@ def unit_conversion(df, options): if options['summary']: - # Add the charge and discharge energy columns to get a single energy column - df[f'Energy({options["old_units"]["energy"]})'] = df[f'Chg Eng({options["old_units"]["energy"]})'] + df[f'DChg Eng({options["old_units"]["energy"]})'] - + df[f'Energy({options["old_units"]["energy"]})'] = df[f'Energy({options["old_units"]["energy"]})'] * unit_tables.energy()[options['old_units']['energy']].loc[options['units']['energy']] df[f'Starting current({options["old_units"]["current"]})'] = df[f'Starting current({options["old_units"]["current"]})'] * unit_tables.current()[options['old_units']['current']].loc[options['units']['current']] df[f'Start Volt({options["old_units"]["voltage"]})'] = df[f'Start Volt({options["old_units"]["voltage"]})'] * unit_tables.voltage()[options['old_units']['voltage']].loc[options['units']['voltage']] df[f'Capacity({options["old_units"]["capacity"]})'] = df[f'Capacity({options["old_units"]["capacity"]})'] * unit_tables.capacity()[options['old_units']['capacity']].loc[options['units']['capacity']] @@ -512,9 +521,7 @@ def unit_conversion(df, options): if drop in df.columns: df.drop(drop, axis=1, inplace=True) - columns = ['cycle', 'steps', 'status', 'voltage', 'current', 'capacity'] - - + columns = ['cycle', 'steps', 'status', 'voltage', 'current', 'capacity', 'energy'] # Add column labels for specific capacity and ions if they exist @@ -522,15 +529,19 @@ def unit_conversion(df, options): df['SpecificCapacity({}/mg)'.format(options['old_units']['capacity'])] = df['SpecificCapacity({}/mg)'.format(options['old_units']['capacity'])] * unit_tables.capacity()[options['old_units']['capacity']].loc[options['units']['capacity']] / unit_tables.mass()['mg'].loc[options['units']["mass"]] columns.append('specific_capacity') + if f'SpecificEnergy({options["old_units"]["energy"]}/mg)' in df.columns: + df[f'SpecificEnergy({options["old_units"]["energy"]}/mg)'] = df[f'SpecificEnergy({options["old_units"]["energy"]}/mg)'] * unit_tables.energy()[options['old_units']['energy']].loc[options['units']['energy']] / unit_tables.mass()['mg'].loc[options['units']["mass"]] + columns.append('specific_energy') + if 'IonsExtracted' in df.columns: columns.append('ions') # Append energy column label here as it was the last column to be generated - columns.append('energy') columns.append('cycle_time') columns.append('runtime') # Apply new column labels + print(df.columns, columns) df.columns = columns From b8ce2b64cc0373e540de3225a8f46122d4310de5 Mon Sep 17 00:00:00 2001 From: rasmusvt Date: Fri, 5 Aug 2022 10:35:35 +0200 Subject: [PATCH 17/22] Add efficiency calculations and new format of summary df --- nafuma/electrochemistry/io.py | 39 +++++++++++++++++++++++++++++------ 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/nafuma/electrochemistry/io.py b/nafuma/electrochemistry/io.py index 9b7b3d6..31626a6 100644 --- a/nafuma/electrochemistry/io.py +++ b/nafuma/electrochemistry/io.py @@ -342,18 +342,31 @@ def process_neware_data(df, options={}): df = add_columns(df=df, options=options) df = unit_conversion(df=df, options=options) - #df = calculate_efficiency(df=df, options=options) + if options['splice_cycles']: df = splice_cycles(df=df, options=options) chg_df = df.loc[df['status'] == 'CC Chg'] + chg_df.reset_index(inplace=True) dchg_df = df.loc[df['status'] == 'CC DChg'] + dchg_df.reset_index(inplace=True) - cycles = [chg_df, dchg_df] + # Construct new DataFrame + new_df = pd.DataFrame(chg_df["cycle"]) + new_df.insert(1,'charge_capacity',chg_df['capacity']) + new_df.insert(1,'charge_specific_capacity',chg_df['specific_capacity']) + new_df.insert(1,'discharge_capacity',dchg_df['capacity']) + new_df.insert(1,'discharge_specific_capacity',dchg_df['specific_capacity']) + new_df.insert(1,'charge_energy',chg_df['energy']) + new_df.insert(1,'charge_specific_energy',chg_df['specific_energy']) + new_df.insert(1,'discharge_energy',dchg_df['energy']) + new_df.insert(1,'discharge_specific_energy',dchg_df['specific_energy']) - return cycles + new_df = calculate_efficiency(df=new_df, options=options) + + return new_df @@ -462,9 +475,23 @@ def add_columns(df, options): return df -#def calculate_efficiency(df, options): -# -# df['coulombic_efficiency'] = +def calculate_efficiency(df: pd.DataFrame, options: dict) -> pd.DataFrame: + + + default_options = { + 'reference_index': 0 + } + + options = aux.update_options(options=options, required_options=default_options.keys(), default_options=default_options) + + df['charge_capacity_fade'] = (df['charge_capacity'] / df['charge_capacity'].iloc[options['reference_index']])*100 + df['discharge_capacity_fade'] = (df['discharge_capacity'] / df['discharge_capacity'].iloc[options['reference_index']])*100 + + df['coulombic_efficiency'] = (df['discharge_capacity'] / df['charge_capacity'])*100 + df['energy_efficiency'] = (df['discharge_energy'] / df['charge_energy'])*100 + + + return df def unit_conversion(df, options): From 790048098a7ef75aea69ec56f2cb50aa1037a706 Mon Sep 17 00:00:00 2001 From: rasmusvt Date: Fri, 5 Aug 2022 10:35:59 +0200 Subject: [PATCH 18/22] Make plotting match new format of summary df --- nafuma/electrochemistry/plot.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/nafuma/electrochemistry/plot.py b/nafuma/electrochemistry/plot.py index b77d6f4..5cdf869 100644 --- a/nafuma/electrochemistry/plot.py +++ b/nafuma/electrochemistry/plot.py @@ -127,28 +127,29 @@ def plot_gc(data, options=None): # Prepare plot fig, ax = btp.prepare_plot(options=options) - mask = [] - for i in range(data['cycles'][0].shape[0]): + for i in range(data['cycles'].shape[0]): if i+1 in options['which_cycles']: mask.append(True) else: mask.append(False) - if len(mask) > len(data['cycles'][1]): + + # Drop the last row if it is midway through a charge in order to avoid mismatch of length of mask and dataset. + if len(mask) > data['cycles'].shape[0]: del mask[-1] - data['cycles'][0].drop(data['cycles'][0].tail(1).index, inplace=True) + data['cycles'].drop(data['cycles'].tail(1).index, inplace=True) - - # 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['charge']: - data['cycles'][0].loc[mask].plot(x='cycle', y=options['x_vals'], ax=ax, color=colours[0][0], kind='scatter', marker="$\u25EF$", s=plt.rcParams['lines.markersize']) + 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']) # if options['discharge']: - data['cycles'][1].loc[mask].plot(x='cycle', y=options['x_vals'], ax=ax, color=colours[0][1], kind='scatter', marker="$\u25EF$", s=plt.rcParams['lines.markersize']) + 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']) if options['interactive_session_active']: From b69a4b95210265c412300b1e6143bd33f922b79a Mon Sep 17 00:00:00 2001 From: rasmusvt Date: Fri, 5 Aug 2022 11:01:09 +0200 Subject: [PATCH 19/22] Plot efficiencies without changing options --- nafuma/electrochemistry/plot.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/nafuma/electrochemistry/plot.py b/nafuma/electrochemistry/plot.py index 5cdf869..5039d0f 100644 --- a/nafuma/electrochemistry/plot.py +++ b/nafuma/electrochemistry/plot.py @@ -1,3 +1,4 @@ +from pickle import MARK import matplotlib.pyplot as plt from matplotlib.ticker import (MultipleLocator, FormatStrFormatter,AutoMinorLocator) @@ -142,14 +143,23 @@ 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['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']) - # + 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']) + 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']) - 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']) + 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']) + + + if options['limit']: + ax.axhline(y=options['limit'], ls='--', c='black') if options['interactive_session_active']: From 9a73f57a8297b031c293e3304c59cb751eb83317 Mon Sep 17 00:00:00 2001 From: rasmusvt Date: Mon, 8 Aug 2022 15:44:55 +0200 Subject: [PATCH 20/22] Allow reading CV data from BioLogic --- nafuma/electrochemistry/io.py | 92 +++++++++++++++++++++++++---------- 1 file changed, 67 insertions(+), 25 deletions(-) diff --git a/nafuma/electrochemistry/io.py b/nafuma/electrochemistry/io.py index 31626a6..7ad764d 100644 --- a/nafuma/electrochemistry/io.py +++ b/nafuma/electrochemistry/io.py @@ -6,6 +6,12 @@ import os import nafuma.auxillary as aux from sympy import re + +# FIXME This is not good practice, but a temporary fix as I don't have time to understand what causes the SettingWithCopyWarning. +# Read this: https://www.dataquest.io/blog/settingwithcopywarning/ +pd.set_option('mode.chained_assignment', None) + + def read_data(data, options={}): if data['kind'] == 'neware': @@ -382,11 +388,22 @@ def process_biologic_data(df, options=None): 'splice_cycles': None} + # Check if the DataFrame contains GC or CV data. + # FIXME This might not be a very rigorous method of checking. E.g. Rest has mode == 3, so if loading a short GC with many Rest-datapoints, the mean will be 2 and it will be treated as CV. For now manual override is sufficient + if not 'mode' in options.keys(): + options['mode'] = 'GC' if int(df['mode'].mean()) == 1 else 'CV' + aux.update_options(options=options, required_options=required_options, default_options=default_options) options['kind'] = 'biologic' # Pick out necessary columns - df = df[['Ns changes', 'Ns', 'time/s', 'Ewe/V', 'Energy charge/W.h', 'Energy discharge/W.h', '/mA', 'Capacity/mA.h', 'cycle number']].copy() + headers = [ + 'Ns changes', 'Ns', 'time/s', 'Ewe/V', 'Energy charge/W.h', 'Energy discharge/W.h', '/mA', 'Capacity/mA.h', 'cycle number' ] if options['mode'] == 'GC' else [ + 'ox/red', 'time/s', 'control/V', 'Ewe/V', '/mA', 'cycle number', '(Q-Qo)/C', 'P/W' + ] + + + 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) @@ -396,10 +413,15 @@ def process_biologic_data(df, options=None): df = unit_conversion(df=df, options=options) - # Creates masks for charge and discharge curves - chg_mask = (df['status'] == 1) & (df['status_change'] != 1) - dchg_mask = (df['status'] == 2) & (df['status_change'] != 1) + # Creates masks for charge and discharge curves + if options['mode'] == 'GC': + chg_mask = (df['status'] == 1) & (df['status_change'] != 1) + dchg_mask = (df['status'] == 2) & (df['status_change'] != 1) + + elif options['mode'] == 'CV': + chg_mask = (df['status'] == 1) # oxidation + dchg_mask = (df['status'] == 0) # reduction # Initiate cycles list cycles = [] @@ -419,7 +441,7 @@ def process_biologic_data(df, options=None): if chg_df.empty and dchg_df.empty: continue - if options['reverse_discharge']: + if options['mode'] == 'GC' and options['reverse_discharge']: max_capacity = dchg_df['capacity'].max() dchg_df['capacity'] = np.abs(dchg_df['capacity'] - max_capacity) @@ -431,10 +453,14 @@ def process_biologic_data(df, options=None): max_capacity = dchg_df['ions'].max() dchg_df['ions'] = np.abs(dchg_df['ions'] - max_capacity) + + if options['mode'] == 'CV': + chg_df = chg_df.sort_values(by='voltage').reset_index(drop=True) + dchg_df = dchg_df.sort_values(by='voltage', ascending=False).reset_index(drop=True) + cycles.append((chg_df, dchg_df)) - return cycles @@ -568,7 +594,6 @@ def unit_conversion(df, options): columns.append('runtime') # Apply new column labels - print(df.columns, columns) df.columns = columns @@ -585,6 +610,11 @@ def unit_conversion(df, options): df['SpecificCapacity({}/mg)'.format(options['old_units']['capacity'])] = df['SpecificCapacity({}/mg)'.format(options['old_units']['capacity'])] * unit_tables.capacity()[options['old_units']['capacity']].loc[options['units']['capacity']] / unit_tables.mass()['mg'].loc[options['units']["mass"]] columns.append('specific_capacity') + if f'SpecificEnergy({options["old_units"]["energy"]}/mg)' in df.columns: + df[f'SpecificEnergy({options["old_units"]["energy"]}/mg)'] = df[f'SpecificEnergy({options["old_units"]["energy"]}/mg)'] * unit_tables.energy()[options['old_units']['energy']].loc[options['units']['energy']] / unit_tables.mass()['mg'].loc[options['units']["mass"]] + columns.append('specific_energy') + + if 'IonsExtracted' in df.columns: columns.append('ions') @@ -601,21 +631,34 @@ def unit_conversion(df, options): df.columns = columns if options['kind'] == 'biologic': - df['time/{}'.format(options['old_units']['time'])] = df["time/{}".format(options['old_units']["time"])] * unit_tables.time()[options['old_units']["time"]].loc[options['units']['time']] - df["Ewe/{}".format(options['old_units']["voltage"])] = df["Ewe/{}".format(options['old_units']["voltage"])] * unit_tables.voltage()[options['old_units']["voltage"]].loc[options['units']['voltage']] - df["/{}".format(options['old_units']["current"])] = df["/{}".format(options['old_units']["current"])] * unit_tables.current()[options['old_units']["current"]].loc[options['units']['current']] + for column in df.columns: + if 'time' in column: + df['time/{}'.format(options['old_units']['time'])] = df["time/{}".format(options['old_units']["time"])] * unit_tables.time()[options['old_units']["time"]].loc[options['units']['time']] + + if 'Ewe' in column: + df["Ewe/{}".format(options['old_units']["voltage"])] = df["Ewe/{}".format(options['old_units']["voltage"])] * unit_tables.voltage()[options['old_units']["voltage"]].loc[options['units']['voltage']] + + if '' in column: + df["/{}".format(options['old_units']["current"])] = df["/{}".format(options['old_units']["current"])] * unit_tables.current()[options['old_units']["current"]].loc[options['units']['current']] - capacity = options['old_units']['capacity'].split('h')[0] + '.h' - df["Capacity/{}".format(capacity)] = df["Capacity/{}".format(capacity)] * (unit_tables.capacity()[options['old_units']["capacity"]].loc[options['units']["capacity"]]) + if 'Capacity' in column: + capacity = options['old_units']['capacity'].split('h')[0] + '.h' + df["Capacity/{}".format(capacity)] = df["Capacity/{}".format(capacity)] * (unit_tables.capacity()[options['old_units']["capacity"]].loc[options['units']["capacity"]]) - columns = ['status_change', 'status', 'time', 'voltage', 'energy_charge', 'energy_discharge', 'current', 'capacity', 'cycle'] + - if 'SpecificCapacity({}/mg)'.format(options['old_units']['capacity']) in df.columns: - df['SpecificCapacity({}/mg)'.format(options['old_units']['capacity'])] = df['SpecificCapacity({}/mg)'.format(options['old_units']['capacity'])] * unit_tables.capacity()[options['old_units']['capacity']].loc[options['units']['capacity']] / unit_tables.mass()['mg'].loc[options['units']["mass"]] - columns.append('specific_capacity') + columns = [ + 'status_change', 'status', 'time', 'voltage', 'energy_charge', 'energy_discharge', 'current', 'capacity', 'cycle'] if options['mode'] == 'GC' else [ # GC headers + 'status', 'time', 'control_voltage', 'voltage', 'current', 'cycle', 'charge', 'power' # CV headers + ] - if 'IonsExtracted' in df.columns: - columns.append('ions') + if options['mode'] == 'GC': + if 'SpecificCapacity({}/mg)'.format(options['old_units']['capacity']) in df.columns: + df['SpecificCapacity({}/mg)'.format(options['old_units']['capacity'])] = df['SpecificCapacity({}/mg)'.format(options['old_units']['capacity'])] * unit_tables.capacity()[options['old_units']['capacity']].loc[options['units']['capacity']] / unit_tables.mass()['mg'].loc[options['units']["mass"]] + columns.append('specific_capacity') + + if 'IonsExtracted' in df.columns: + columns.append('ions') df.columns = columns @@ -674,19 +717,18 @@ def get_old_units(df: pd.DataFrame, options: dict) -> dict: if options['kind'] == 'biologic': + old_units = {} for column in df.columns: if 'time' in column: - time = column.split('/')[-1] + old_units['time'] = column.split('/')[-1] elif 'Ewe' in column: - voltage = column.split('/')[-1] + old_units['voltage'] = column.split('/')[-1] elif 'Capacity' in column: - capacity = column.split('/')[-1].replace('.', '') + old_units['capacity'] = column.split('/')[-1].replace('.', '') elif 'Energy' in column: - energy = column.split('/')[-1].replace('.', '') + old_units['energy'] = column.split('/')[-1].replace('.', '') elif '' in column: - current = column.split('/')[-1] - - old_units = {'voltage': voltage, 'current': current, 'capacity': capacity, 'energy': energy, 'time': time} + old_units['current'] = column.split('/')[-1] return old_units From df5190667eed1dd0d9a7eb29ef7955ec8d2e7bd8 Mon Sep 17 00:00:00 2001 From: rasmusvt Date: Mon, 8 Aug 2022 15:45:09 +0200 Subject: [PATCH 21/22] Ad plotting of CV-data --- nafuma/electrochemistry/plot.py | 109 +++++++++++++++++++++++++++++++- 1 file changed, 106 insertions(+), 3 deletions(-) diff --git a/nafuma/electrochemistry/plot.py b/nafuma/electrochemistry/plot.py index 5039d0f..7a0246c 100644 --- a/nafuma/electrochemistry/plot.py +++ b/nafuma/electrochemistry/plot.py @@ -23,10 +23,12 @@ def plot_gc(data, options=None): # Update options - required_options = ['x_vals', 'y_vals', 'which_cycles', 'exclude_cycles', 'show_plot', 'charge', 'discharge', 'colours', 'differentiate_charge_discharge', 'gradient', 'interactive', 'interactive_session_active', 'rc_params', 'format_params', 'save_gif', 'save_path', 'fps'] + 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': 'capacity', 'y_vals': 'voltage', 'which_cycles': 'all', + 'limit': None, # Limit line to be drawn 'exclude_cycles': [], 'show_plot': True, 'charge': True, 'discharge': True, @@ -46,7 +48,7 @@ def plot_gc(data, options=None): # Read data if not already loaded - if not 'cycles' in data.keys(): + if not 'cycles' in data.keys() or options['force_reload']: data['cycles'] = ec.io.read_data(data=data, options=options) @@ -102,7 +104,7 @@ def plot_gc(data, options=None): if options['discharge']: cycle[1].plot(x=options['x_vals'], y=options['y_vals'], ax=gifax, c=colours[i][1]) - gifax.text(x=options['xlim'][1]*0.8, y=3, s=f'{i+1}') + gifax.text(x=gifax.get_xlim()[1]*0.8, y=3, s=f'{i+1}') update_labels(options) giffig, gifax = btp.adjust_plot(fig=giffig, ax=gifax, options=options) @@ -189,6 +191,107 @@ def plot_gc_interactive(data, options): display(w) + + +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', + 'which_cycles': 'all', + 'limit': None, # Limit line to be drawn + 'exclude_cycles': [], + 'show_plot': True, + 'charge': True, 'discharge': True, + 'colours': None, + 'differentiate_charge_discharge': True, + 'gradient': False, + 'interactive': False, + 'interactive_session_active': False, + 'rc_params': {}, + 'format_params': {}, + 'save_gif': False, + 'save_path': 'animation.gif', + 'fps': 1 + } + + options = aux.update_options(options=options, required_options=required_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) + + colours = generate_colours(cycles=data['cycles'], 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 options['discharge']: + cycle[1].plot(x=options['x_vals'], y=options['y_vals'], ax=ax, c=colours[i][1]) + + update_labels(options) + + + + if options['save_gif'] and not options['interactive_session_active']: + if not os.path.isdir('tmp'): + os.makedirs('tmp') + + # Scale image to make GIF smaller + options['format_params']['width'] = 7.5 + options['format_params']['height'] = 3 + + options['format_params']['dpi'] = 200 + + for i, cycle in enumerate(data['cycles']): + if i in options['which_cycles']: + + giffig, gifax = btp.prepare_plot(options=options) + + if options['charge']: + cycle[0].plot(x=options['x_vals'], y=options['y_vals'], ax=gifax, c=colours[i][0]) + if options['discharge']: + cycle[1].plot(x=options['x_vals'], y=options['y_vals'], ax=gifax, c=colours[i][1]) + + gifax.text(x=gifax.get_xlim()[1]*0.8, y=3, s=f'{i+1}') + update_labels(options) + + giffig, gifax = btp.adjust_plot(fig=giffig, ax=gifax, options=options) + + plt.savefig(os.path.join('tmp', str(i+1).zfill(4)+'.png')) + plt.close() + + + img_paths = [os.path.join('tmp', path) for path in os.listdir('tmp') if path.endswith('png')] + frames = [] + for path in img_paths: + frame = Image.open(path) + frames.append(frame) + + frames[0].save(options['save_path'], format='GIF', append_images=frames[1:], save_all=True, duration=(1/options['fps'])*1000, loop=0) + + shutil.rmtree('tmp') + + + + if options['show_plot']: + fig, ax = btp.adjust_plot(fig=fig, ax=ax, options=options) + return data['cycles'], fig, ax + else: + return data['cycles'], None, None + def update_labels(options, force=False): if 'xlabel' not in options.keys() or force: From cd0eaff25b851b8242a164d3b2bef301f58e546e Mon Sep 17 00:00:00 2001 From: rasmusvt Date: Mon, 15 Aug 2022 17:35:01 +0200 Subject: [PATCH 22/22] Make BATSMALL-read more general and import decimal = , --- nafuma/electrochemistry/io.py | 28 ++++++++++++++++++++++------ nafuma/electrochemistry/plot.py | 3 ++- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/nafuma/electrochemistry/io.py b/nafuma/electrochemistry/io.py index 7ad764d..296bd4f 100644 --- a/nafuma/electrochemistry/io.py +++ b/nafuma/electrochemistry/io.py @@ -70,7 +70,9 @@ def read_batsmall(path): Output: df: pandas DataFrame containing the data as-is, but without additional NaN-columns.''' - df = pd.read_csv(path, skiprows=2, sep='\t') + + # FIXME Now it is hardcoded that the decimal is a comma. It should do a check, as datasets can vary depending on the system settings of the machine that does the data conversion + df = pd.read_csv(path, skiprows=2, sep='\t', decimal=',') df = df.loc[:, ~df.columns.str.contains('^Unnamed')] return df @@ -133,9 +135,11 @@ def process_batsmall_data(df, options=None): df = unit_conversion(df=df, options=options) + if options['splice_cycles']: df = splice_cycles(df=df, options=options) + # Replace NaN with empty string in the Comment-column and then remove all steps where the program changes - this is due to inconsistent values for current df[["comment"]] = df[["comment"]].fillna(value={'comment': ''}) df = df[df["comment"].str.contains("program")==False] @@ -694,11 +698,23 @@ def get_old_units(df: pd.DataFrame, options: dict) -> dict: if options['kind'] == 'batsmall': - time = df.columns[0].split()[-1].strip('[]') - voltage = df.columns[1].split()[-1].strip('[]') - current = df.columns[2].split()[-1].strip('[]') - capacity, mass = df.columns[4].split()[-1].strip('[]').split('/') - old_units = {'time': time, 'current': current, 'voltage': voltage, 'capacity': capacity, 'mass': mass} + old_units = {} + + for column in df.columns: + if 'TT [' in column: + old_units['time'] = column.split()[-1].strip('[]') + elif 'U [' in column: + old_units['voltage'] = column.split()[-1].strip('[]') + elif 'I [' in column: + old_units['current'] = column.split()[-1].strip('[]') + elif 'C [' in column: + old_units['capacity'], old_units['mass'] = column.split()[-1].strip('[]').split('/') + + # time = df.columns[0].split()[-1].strip('[]') + # voltage = df.columns[1].split()[-1].strip('[]') + # current = df.columns[2].split()[-1].strip('[]') + # capacity, mass = df.columns[4].split()[-1].strip('[]').split('/') + # old_units = {'time': time, 'current': current, 'voltage': voltage, 'capacity': capacity, 'mass': mass} if options['kind']=='neware': diff --git a/nafuma/electrochemistry/plot.py b/nafuma/electrochemistry/plot.py index 7a0246c..8fbee49 100644 --- a/nafuma/electrochemistry/plot.py +++ b/nafuma/electrochemistry/plot.py @@ -23,7 +23,7 @@ def plot_gc(data, options=None): # 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'] + 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', @@ -31,6 +31,7 @@ def plot_gc(data, options=None): 'limit': None, # Limit line to be drawn 'exclude_cycles': [], 'show_plot': True, + 'summary': False, 'charge': True, 'discharge': True, 'colours': None, 'differentiate_charge_discharge': True,