Skip to content

Commit

Permalink
Merge pull request #104 from patrickherring-TRI/MAT-1837_create_drivi…
Browse files Browse the repository at this point in the history
…ng_protocols

Mat 1837 create driving protocols
  • Loading branch information
patrickherring-TRI authored Sep 23, 2020
2 parents 9a1fa3c + 10bd849 commit b6388ec
Show file tree
Hide file tree
Showing 9 changed files with 5,580 additions and 73 deletions.
16 changes: 8 additions & 8 deletions beep/conversion_schemas/maccor_waveform_conversion.yaml
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
control_mode: 'P'
value_scale: 1
charge_limit_mode: ''
charge_limit_value: ''
discharge_limit_mode: ''
discharge_limit_value: ''
charge_limit_mode: 'V'
charge_limit_value: 4.2
discharge_limit_mode: 'V'
discharge_limit_value: 2.7
charge_end_mode: 'V'
charge_end_operation: '>='
charge_end_mode_value: 4.2
charge_end_mode_value: 4.25
discharge_end_mode: 'V'
discharge_end_operation: '<='
discharge_end_mode_value: 3
discharge_end_mode_value: 2.5
report_mode: 'T'
report_value: 1
range: 'A'
report_value: 3.0000
range: 'A'
84 changes: 27 additions & 57 deletions beep/generate_protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,74 +42,20 @@
import json
import datetime
import csv
import numpy as np
import pandas as pd
from docopt import docopt
from monty.serialization import loadfn

from beep import logger, __version__
from beep.protocol import PROCEDURE_TEMPLATE_DIR, BIOLOGIC_TEMPLATE_DIR
from beep.protocol.maccor import Procedure
from beep.protocol.maccor import Procedure, insert_driving_parametersv1
from beep.protocol.biologic import Settings

from beep.utils import KinesisEvents, WorkflowOutputs

s = {"service": "ProtocolGenerator"}


def convert_velocity_to_power_waveform(waveform_file, velocity_units):
"""
Helper function to perform model based conversion of velocity waveform into power waveform.
For model description and parameters ref JECS, 161 (14) A2099-A2108 (2014)
"Model-Based SEI Layer Growth and Capacity Fade Analysis for EV and PHEV Batteries and Drive Cycles"
Args:
waveform_file (str): file containing tab or comma delimited values of time and velocity
velocity_units (str): units of velocity. Accept 'mph' or 'kmph' or 'mps'
returns
pd.DataFrame with two columns: time (sec) and power (W). Negative = Discharge
"""
df = pd.read_csv(waveform_file, sep="\t", header=0)
df.columns = ["t", "v"]

if velocity_units == "mph":
scale = 1600.0 / 3600.0
elif velocity_units == "kmph":
scale = 1000.0 / 3600.0
elif velocity_units == "mps":
scale = 1.0
else:
raise NotImplementedError

df.v = df.v * scale

# Define model constants
m = 1500 # kg
rolling_resistance_coef = 0.01 # rolling resistance coeff
g = 9.8 # m/s^2
theta = 0 # gradient in radians
rho = 1.225 # kg/m^3
drag_coef = 0.34 # Coeff of drag
frontal_area = 1.75 # m^2
v_wind = 0 # wind velocity in m/s

# Power = Force * vel
# Force = Rate of change of momentum + Rolling frictional force + Aerodynamic drag force

# Method treats the time-series as is and does not interpolate on a uniform grid before computing gradient.
power = (
m * np.gradient(df.v, df.t)
+ rolling_resistance_coef * m * g * np.cos(theta * np.pi / 180)
+ 0.5 * rho * drag_coef * frontal_area * (df.v - v_wind) ** 2
)

power = -power * df.v # positive power = charge

return pd.DataFrame({"time": df.t, "power": power})


def generate_protocol_files_from_csv(csv_filename, output_directory=None):

"""
Expand Down Expand Up @@ -172,8 +118,31 @@ def generate_protocol_files_from_csv(csv_filename, output_directory=None):
diag_params_df["diagnostic_parameter_set"]
== protocol_params["diagnostic_parameter_set"]
].squeeze()

protocol = Procedure.generate_procedure_regcyclev3(index, protocol_params)
template_fullpath = os.path.join(PROCEDURE_TEMPLATE_DIR, template)
protocol = Procedure.generate_procedure_regcyclev3(index,
protocol_params,
template=template_fullpath)
protocol.generate_procedure_diagcyclev3(
protocol_params["capacity_nominal"], diagnostic_params
)
filename = "{}.000".format(filename_prefix)
filename = os.path.join(output_directory, "procedures", filename)
elif template == "drivingV1.000":
diag_params_df = pd.read_csv(
os.path.join(PROCEDURE_TEMPLATE_DIR, "PreDiag_parameters - DP.csv")
)
diagnostic_params = diag_params_df[
diag_params_df["diagnostic_parameter_set"]
== protocol_params["diagnostic_parameter_set"]
].squeeze()
mwf_dir = os.path.join(output_directory, "mwf_files")
waveform_name = insert_driving_parametersv1(protocol_params,
waveform_directory=mwf_dir)
template_fullpath = os.path.join(PROCEDURE_TEMPLATE_DIR, template)
protocol = Procedure.generate_procedure_drivingv1(index,
protocol_params,
waveform_name,
template=template_fullpath)
protocol.generate_procedure_diagcyclev3(
protocol_params["capacity_nominal"], diagnostic_params
)
Expand All @@ -184,6 +153,7 @@ def generate_protocol_files_from_csv(csv_filename, output_directory=None):
protocol = protocol.formation_protocol_bcs(protocol, protocol_params)
filename = "{}.mps".format(filename_prefix)
filename = os.path.join(output_directory, "settings", filename)

else:
warnings.warn("Unsupported file template {}, skipping.".format(template))
result = "error"
Expand Down
132 changes: 131 additions & 1 deletion beep/protocol/maccor.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@
from copy import deepcopy

import numpy as np
import pandas as pd
import xmltodict
from beep.protocol import PROCEDURE_TEMPLATE_DIR
from beep.protocol import PROCEDURE_TEMPLATE_DIR, PROTOCOL_SCHEMA_DIR
from beep.conversion_schemas import MACCOR_WAVEFORM_CONFIG
from beep.utils import DashOrderedDict
from beep.utils.waveform import convert_velocity_to_power_waveform

s = {"service": "ProtocolGenerator"}

Expand Down Expand Up @@ -598,6 +600,95 @@ def generate_procedure_regcyclev3(cls, protocol_index, reg_param, template=None)

return obj

@classmethod
def generate_procedure_drivingv1(cls, protocol_index, reg_param, waveform_name, template=None):
"""
Generates a procedure according to the diagnosticV3 template.
Args:
protocol_index (int): number of the protocol file being
generated from this file.
reg_param (pandas.DataFrame): containing the following quantities
charge_constant_current_1 (float): C
charge_percent_limit_1 (float): % of nominal capacity
charge_constant_current_2 (float): C
charge_cutoff_voltage (float): V
charge_constant_voltage_time (integer): mins
charge_rest_time (integer): mins
discharge_profile (str): {'US06', 'LA4', '9Lap'}
profile_charge_limit (float): upper limit voltage for the profile
max_profile_power (float): maximum power setpoint during the profile
n_repeats (int): number of repetitions for the profile
discharge_cutoff_voltage (float): V
power_scaling (float): Power relative to the other profiles
discharge_rest_time (integer): mins
cell_temperature_nominal (float): ˚C
capacity_nominal (float): Ah
diagnostic_start_cycle (integer): cycles
diagnostic_interval (integer): cycles
waveform_name (str): Name of the waveform file (not path) without extension
template (str): template for invocation, defaults to
the diagnosticV3.000 template
Returns:
(Procedure): dictionary invoked using template/parameters
"""
assert (
reg_param["charge_cutoff_voltage"] > reg_param["discharge_cutoff_voltage"]
)
assert (
reg_param["charge_constant_current_1"]
<= reg_param["charge_constant_current_2"]
)

rest_idx = 0

template = template or os.path.join(PROCEDURE_TEMPLATE_DIR, "drivingV1.000")
obj = cls.from_file(template)
obj.insert_initialrest_regcyclev3(rest_idx, protocol_index)

dc_idx = 1
obj.insert_resistance_regcyclev2(dc_idx, reg_param)

# Start of initial set of regular cycles
reg_charge_idx = 27 + 1
obj.insert_charge_regcyclev3(reg_charge_idx, reg_param)
reg_discharge_idx = 27 + 5
obj.insert_maccor_waveform_discharge(reg_discharge_idx, waveform_name)

# Start of main loop of regular cycles
reg_charge_idx = 59 + 1
obj.insert_charge_regcyclev3(reg_charge_idx, reg_param)
reg_discharge_idx = 59 + 5
obj.insert_maccor_waveform_discharge(reg_discharge_idx, waveform_name)

# Storage cycle
reg_storage_idx = 93
obj.insert_storage_regcyclev3(reg_storage_idx, reg_param)

# Check that the upper charge voltage is set the same for both charge current steps
reg_charge_idx = 27 + 1
assert (
obj["MaccorTestProcedure"]["ProcSteps"]["TestStep"][reg_charge_idx]["Ends"][
"EndEntry"
][1]["Value"]
== obj["MaccorTestProcedure"]["ProcSteps"]["TestStep"][reg_charge_idx + 1][
"Ends"
]["EndEntry"][0]["Value"]
)

reg_charge_idx = 59 + 1
assert (
obj["MaccorTestProcedure"]["ProcSteps"]["TestStep"][reg_charge_idx]["Ends"][
"EndEntry"
][1]["Value"]
== obj["MaccorTestProcedure"]["ProcSteps"]["TestStep"][reg_charge_idx + 1][
"Ends"
]["EndEntry"][0]["Value"]
)

return obj

def insert_initialrest_regcyclev3(self, rest_idx, index):
"""
Inserts initial rest into procedure dictionary at given id.
Expand Down Expand Up @@ -1127,6 +1218,45 @@ def set_skip_to_end_diagnostic(self, max_v, min_v, step_key='070'):
return self


def insert_driving_parametersv1(reg_params, waveform_directory=None):
"""
Args:
reg_params (pandas.DataFrame): containing the following quantities
discharge_profile (str): {'US06', 'LA4', '9Lap'}
profile_charge_limit (float): upper limit voltage for the profile
max_profile_power (float): maximum power setpoint during the profile
n_repeats (int): number of repetitions for the profile
discharge_cutoff_voltage (float): V
power_scaling (float): Power relative to the other profiles
waveform_directory (str): path to save waveform files
"""
mwf_config = MACCOR_WAVEFORM_CONFIG
velocity_name = reg_params["discharge_profile"] + "_velocity_waveform.txt"
velocity_file = os.path.join(PROTOCOL_SCHEMA_DIR, velocity_name)
df = convert_velocity_to_power_waveform(velocity_file, velocity_units="mph")

if not os.path.exists(waveform_directory):
os.makedirs(waveform_directory)

mwf_config["value_scale"] = reg_params["max_profile_power"] * reg_params["power_scaling"]
mwf_config["charge_limit_value"] = reg_params["profile_charge_limit"]
mwf_config["charge_end_mode_value"] = reg_params["profile_charge_limit"] + 0.05
mwf_config["discharge_end_mode_value"] = reg_params["discharge_cutoff_voltage"] - 0.1

df = df[["time", "power"]]
time_axis = list(df["time"]).copy()
for i in range(reg_params['n_repeats'] - 1):
time_axis = time_axis + [time_axis[-1] + el for el in df['time']]

df = pd.DataFrame({'time': time_axis,
'power': list(df['power']) * reg_params['n_repeats']})
filename = '{}_x{}_{}W'.format(reg_params['discharge_profile'], reg_params['n_repeats'],
int(reg_params["max_profile_power"] * reg_params['power_scaling']))
file_path = generate_maccor_waveform_file(df, filename, waveform_directory, mwf_config=mwf_config)

return file_path


def generate_maccor_waveform_file(df, file_prefix, file_directory, mwf_config=None):
"""
Helper function that takes in a variable power waveform and outputs a maccor waveform file (.MWF), which is read by
Expand Down
Loading

0 comments on commit b6388ec

Please sign in to comment.