diff --git a/cal_and_val/thermal/cal_bev.py b/cal_and_val/thermal/cal_bev.py index 34f44a41..e933df3a 100644 --- a/cal_and_val/thermal/cal_bev.py +++ b/cal_and_val/thermal/cal_bev.py @@ -1,11 +1,8 @@ """ Calibration script for 2020 Chevrolet Bolt EV """ - -# critical import from pathlib import Path -# anticipated cricital imports import numpy as np # noqa: F401 import matplotlib.pyplot as plt # noqa: F401 import seaborn as sns @@ -13,6 +10,7 @@ import polars as pl # noqa: F401 from typing import List, Dict from pymoo.core.problem import StarmapParallelization +from copy import deepcopy import fastsim as fsim from fastsim import pymoo_api @@ -33,16 +31,16 @@ # Obtain the data from # https://nrel.sharepoint.com/:f:/r/sites/EEMSCoreModelingandDecisionSupport2022-2024/Shared%20Documents/FASTSim/DynoTestData?csf=1&web=1&e=F4FEBp # and then copy it to the local folder below -cyc_folder_path = Path(__file__) / "dyno_test_data/2020 Chevrolet Bolt EV/Extended Datasets" -assert cyc_folder_path.exists() +cyc_folder_path = Path(__file__).parent / "dyno_test_data/2020 Chevrolet Bolt EV/Extended Datasets" +assert cyc_folder_path.exists(), cyc_folder_path # See 2020_Chevrolet_Bolt_TestSummary_201005.xlsm for cycle-level data -cyc_files = [ +cyc_files: List[str] = [ # TODO: check for seat heater usage in cold cycles and account for that in model! # 20F (heater maybe on? Col R in test summary), UDDS + HWY + UDDS + US06 - "62009051 Test Data.txt" + "62009051 Test Data.txt", # 20F (heater maybe on? Col R in test summary), US06 + UDDS + HWY + UDDS - "62009053 Test Data.txt" + "62009053 Test Data.txt", # room temperature (no HVAC), UDDS + HWY + UDDS + US06 "62009019 Test Data.txt", @@ -51,61 +49,78 @@ # TODO: check for solar load (should be around 1 kW / m^2) and implement or this somewhere (`drive_cycle`???) # 95F (HVAC on), UDDS + HWY + UDDS - "62009040 Test Data.txt" + "62009040 Test Data.txt", # 95F (HVAC on), US06 - "62009041 Test Data.txt" + "62009041 Test Data.txt", ] assert len(cyc_files) > 0 cyc_files = [cyc_folder_path / cyc_file for cyc_file in cyc_files] +print("\ncyc_files:\n", '\n'.join([cf.name for cf in cyc_files]), sep='') -# TODO: use random selection to retain ~70% of cycles for calibration, and -# reserve the remaining for validation +# use random or manual selection to retain ~70% of cycles for calibration, +# and reserve the remaining for validation cyc_files_for_cal: List[str] = [ - # TODO: populate this somehow -- e.g. random split of `cyc_files` -] + "62009051 Test Data.txt", + # "62009053 Test Data.txt" + "62009019 Test Data.txt", + # "62009021 Test Data.txt", + + "62009040 Test Data.txt" + # "62009041 Test Data.txt" +] cyc_files_for_cal: List[Path] = [cyc_file for cyc_file in cyc_files if cyc_file.name in cyc_files_for_cal] -assert len(cyc_files_for_cal) > 0 +assert len(cyc_files_for_cal) > 0, cyc_files_for_cal +print("\ncyc_files_for_cal:\n", '\n'.join([cf.name for cf in cyc_files_for_cal]), sep='') def df_to_cyc(df: pd.DataFrame) -> fsim.Cycle: - # filter out "before" time - df = df[df["Time[s]_RawFacilities"] >= 0.0] - assert len(df) > 10 cyc_dict = { "time_seconds": df["Time[s]_RawFacilities"].to_list(), "speed_meters_per_second": (df["Dyno_Spd[mph]"] * mps_per_mph).to_list(), "temp_amb_air_kelvin": (df["Cell_Temp[C]"] + celsius_to_kelvin_offset).to_list(), # TODO: pipe solar load from `Cycle` into cabin thermal model # TODO: use something (e.g. regex) to determine solar load + # see column J comments in 2021_Hyundai_Sonata_Hybrid_TestSummary_2022-03-01_D3.xlsx # "pwr_solar_load_watts": df[], } return fsim.Cycle.from_pydict(cyc_dict, skip_init=False) def veh_init(cyc_file_stem: str, dfs: Dict[str, pd.DataFrame]) -> fsim.Vehicle: + vd = deepcopy(veh_dict) # initialize SOC - # TODO: figure out if `HVBatt_SOC_CAN4__per` is the correct column within the dyno data - veh_dict['pt_type']['BatteryElectricVehicle']['res']['state']['soc'] = \ - dfs[cyc_file_stem]["HVBatt_SOC_CAN4__per"][0] / 100 + vd['pt_type']['BatteryElectricVehicle']['res']['state']['soc'] = \ + dfs[cyc_file_stem]["HVBatt_SOC_CAN4__per"].iloc[1] / 100 + assert 0 < vd['pt_type']['BatteryElectricVehicle']['res']['state']['soc'] < 1, "\ninit soc: {}\nhead: {}".format( + vd['pt_type']['BatteryElectricVehicle']['res']['state']['soc'], dfs[cyc_file_stem]["HVBatt_SOC_CAN4__per"].head()) # initialize cabin temp - veh_dict['cabin']['LumpedCabin']['state']['temperature_kelvin'] = \ - dfs[cyc_file_stem]["Cabin_Temp[C]"][0] + celsius_to_kelvin_offset + vd['cabin']['LumpedCabin']['state']['temperature_kelvin'] = \ + dfs[cyc_file_stem]["Cabin_Lower_Vent_Temp__C"][0] + celsius_to_kelvin_offset # initialize battery temperature to match cabin temperature because battery # temperature is not available in test data - veh_dict['pt_type']['BatteryElectricVehicle']['res']['thrml']['RESLumpedThermal']['state']['temperature_kelvin'] = \ - dfs[cyc_file_stem]["Cabin_Temp[C]"][0] + celsius_to_kelvin_offset - # initialize engine temperature - veh_dict['pt_type']['BatteryElectricVehicle']['fc']['thrml']['FuelConverterThermal']['state']['temperature_kelvin'] = \ - dfs[cyc_file_stem]["engine_coolant_temp_PCAN__C"][0] + celsius_to_kelvin_offset - return fsim.Vehicle.from_pydict(veh_dict) + # Also, battery temperature has no effect in the HEV because efficiency data + # does not go below 23*C and there is no active thermal management + vd['pt_type']['BatteryElectricVehicle']['res']['thrml']['RESLumpedThermal']['state']['temperature_kelvin'] = \ + dfs[cyc_file_stem]["Cabin_Lower_Vent_Temp__C"][0] + celsius_to_kelvin_offset + return fsim.Vehicle.from_pydict(vd, skip_init=False) + dfs_for_cal: Dict[str, pd.DataFrame] = { # `delimiter="\t"` should work for tab separated variables cyc_file.stem: pd.read_csv(cyc_file, delimiter="\t") for cyc_file in cyc_files_for_cal } -for cyc_file in cyc_files_for_cal: - cyc_file: Path - # `delimiter="\t"` should work for tab separated variables - dfs_for_cal[cyc_file.stem] = pd.read_csv(cyc_file, delimiter="\t") +for key, df_for_cal in dfs_for_cal.items(): + # filter out "before" time + df_for_cal = df_for_cal[df_for_cal["Time[s]_RawFacilities"] >= 0.0] + # TODO: figure out if we should use an integrator for resampling rate vars + # df_for_cal = df_for_cal.set_index("Time[s]_RawFacilities") + # df_for_cal = df_for_cal.resample("1s", origin="start").bfill() + df_for_cal = df_for_cal[::10] + # cut out junk data from specific files + if key == "62009040 Test Data": + df_for_cal = df_for_cal[df_for_cal["Time[s]_RawFacilities"] < 2153] + df_for_cal.reset_index(inplace=True) + dfs_for_cal[key] = df_for_cal + cycs_for_cal: Dict[str, fsim.Cycle] = {} # populate `cycs_for_cal` for (cyc_file_stem, df) in dfs_for_cal.items(): @@ -123,15 +138,28 @@ def veh_init(cyc_file_stem: str, dfs: Dict[str, pd.DataFrame]) -> fsim.Vehicle: cyc: fsim.Cycle # NOTE: maybe change `save_interval` to 5 veh = veh_init(cyc_file_stem, dfs_for_cal) - sds_for_cal[cyc_file_stem] = fsim.SimDrive(veh, cyc).to_pydict() + sds_for_cal[cyc_file_stem] = fsim.SimDrive(veh, cyc, sim_params).to_pydict() cyc_files_for_val: List[Path] = list(set(cyc_files) - set(cyc_files_for_cal)) assert len(cyc_files_for_val) > 0 +print("\ncyc_files_for_val:\n", '\n'.join([cf.name for cf in cyc_files_for_val]), sep='') dfs_for_val: Dict[str, pd.DataFrame] = { # `delimiter="\t"` should work for tab separated variables cyc_file.stem: pd.read_csv(cyc_file, delimiter="\t") for cyc_file in cyc_files_for_val } +for key, df_for_val in dfs_for_val.items(): + # filter out "before" time + df_for_val = df_for_val[df_for_val["Time[s]_RawFacilities"] >= 0.0] + # TODO: figure out if we should use an integrator for resampling rate vars + # df_for_val = df_for_val.set_index("Time[s]_RawFacilities") + # df_for_val = df_for_val.resample("1s", origin="start").bfill() + df_for_val = df_for_val[::10] + # cut out junk data from specific files + if key == "62009040 Test Data": + df_for_cal = df_for_cal[df_for_cal["Time[s]_RawFacilities"] < 2153] + df_for_val.reset_index(inplace=True) + dfs_for_val[key] = df_for_val cycs_for_val: Dict[str, fsim.Cycle] = {} # populate `cycs_for_val` @@ -146,95 +174,146 @@ def veh_init(cyc_file_stem: str, dfs: Dict[str, pd.DataFrame]) -> fsim.Vehicle: cyc_file_stem: str cyc: fsim.Cycle veh = veh_init(cyc_file_stem, dfs_for_val) - sds_for_val[cyc_file_stem] = fsim.SimDrive(veh, cyc).to_pydict() + sds_for_val[cyc_file_stem] = fsim.SimDrive(veh, cyc, sim_params).to_pydict() # Setup model objectives ## Parameter Functions -def new_em_eff_max(sd_dict, new_eff_max): +def new_em_eff_max(sd_dict, new_eff_max) -> Dict: """ Set `new_eff_max` in `ElectricMachine` """ em = fsim.ElectricMachine.from_pydict(sd_dict['veh']['pt_type']['BatteryElectricVehicle']['em']) em.__eff_fwd_max = new_eff_max sd_dict['veh']['pt_type']['BatteryElectricVehicle']['em'] = em.to_pydict() - + return sd_dict -def new_em_eff_range(sd_dict, new_eff_range): +def new_em_eff_range(sd_dict, new_eff_range) -> Dict: """ Set `new_eff_range` in `ElectricMachine` """ em = fsim.ElectricMachine.from_pydict(sd_dict['veh']['pt_type']['BatteryElectricVehicle']['em']) em.__eff_fwd_range = new_eff_range sd_dict['veh']['pt_type']['BatteryElectricVehicle']['em'] = em.to_pydict() - + return sd_dict + +def get_mod_soc(sd_dict): + return np.array(sd_dict['veh']['pt_type']['BatteryElectricVehicle']['res']['history']['soc']) + +def get_exp_soc(df): + return df['HVBatt_SOC_CAN4__per'] / 100 + +save_path = Path(__file__).parent / "pymoo_res" / Path(__file__).stem +save_path.mkdir(exist_ok=True, parents=True) ## Model Objectives -cal_mod_obj = fsim.pymoo_api.ModelObjectives( +cal_mod_obj = pymoo_api.ModelObjectives( models = sds_for_cal, dfs = dfs_for_cal, obj_fns=( ( - lambda sd_df: np.array(sd_df['veh.pt_type.BatteryElectricVehicle.res.history.soc']), - lambda df: df['HVBatt_SOC_CAN4__per'] + get_mod_soc, + get_exp_soc ), # TODO: add objectives for: - # - battery temperature - ( - lambda sd_df: np.array(sd_df['veh.pt_type.BatteryElectricVehicle.res.thermal.RESLumpedThermal.history.temperature_kelvin']), - # HVBatt_cell_temp_1_CAN3__C (or average of temps?) or HVBatt_pack_average_temp_HPCM2__C? - lambda df: df['HVBatt_pack_average_temp_HPCM2__C'] + celsius_to_kelvin_offset - ), + # - achieved and cycle speed + # - engine fuel usage + # - battery temperature -- BEV only, if available + # - engine temperature # - cabin temperature - # - HVAC power, if available + # - HVAC power for cabin, if available ), param_fns=( new_em_eff_max, new_em_eff_range, # TODO: make sure this has functions for modifying - # - HVAC PID controls for cabin (not for battery because Sonata has - # passive thermal management, but make sure to do battery thermal - # controls for BEV) - # - battery thermal + # - cabin thermal + # - thermal mass + # - length + # - htc to amb when stopped + # - set width from vehicle specs -- no need to calibrate + # - battery thermal -- not necessary for HEV because battery temperature has no real effect # - thermal mass # - convection to ambient # - convection to cabin + ), + # must match order and length of `params_fns` + bounds=( + (0.80, 0.99), + (0.1, 0.6), + ), + verbose=False, +) + +val_mod_obj = pymoo_api.ModelObjectives( + models = sds_for_val, + dfs = dfs_for_val, + obj_fns=( + ( + get_mod_soc, + get_exp_soc + ), + # TODO: add objectives for: + # - achieved and cycle speed + # - battery temperature -- BEV only, if available + # - cabin temperature + # - HVAC power for cabin, if available + ), + param_fns=( + new_em_eff_max, + new_em_eff_range, + # TODO: make sure this has functions for modifying # - cabin thermal # - thermal mass # - length # - htc to amb when stopped - # - set width from vehicle specs -- no need to calibrate + # - set width from vehicle specs -- no need to valibrate + # - battery thermal -- not necessary for HEV because battery temperature has no real effect + # - thermal mass + # - convection to ambient + # - convection to cabin ), # must match order and length of `params_fns` bounds=( (0.80, 0.99), (0.1, 0.6), ), - + verbose=False, ) - -# verify that model responds to input parameter changes by individually perturbing parameters -baseline_errors = cal_mod_obj.get_errors( - cal_mod_obj.update_params([ - fsim.ElectricMachine.from_pydict(veh_dict['pt_type']['BatteryElectricVehicle']['em']).eff_fwd_max, - fsim.ElectricMachine.from_pydict(veh_dict['pt_type']['BatteryElectricVehicle']['em']).eff_fwd_range, - ]) -) -param0_perturb = cal_mod_obj.get_errors( - cal_mod_obj.update_params([0.90 + 0.5, 0.3]) -) -assert list(param0_perturb.values()) != list(baseline_errors.values()) -param1_perturb = cal_mod_obj.get_errors( - cal_mod_obj.update_params([0.90, 0.3 + 0.1]) -) -assert list(param1_perturb.values()) != list(baseline_errors.values()) +em_eff_fwd_max = fsim.ElectricMachine.from_pydict(veh_dict['pt_type']['BatteryElectricVehicle']['em'], skip_init=False).eff_fwd_max +em_eff_fwd_range = fsim.ElectricMachine.from_pydict(veh_dict['pt_type']['BatteryElectricVehicle']['em'], skip_init=False).eff_fwd_range +# print("Verifying that model responds to input parameter changes by individually perturbing parameters") +# baseline_errors = cal_mod_obj.get_errors( +# cal_mod_obj.update_params([ +# em_eff_fwd_max, +# em_eff_fwd_range, +# ]) +# ) +# param0_perturb = cal_mod_obj.get_errors( +# cal_mod_obj.update_params([ +# em_eff_fwd_max + 0.05, +# em_eff_fwd_range, +# ]) +# ) +# assert list(param0_perturb.values()) != list(baseline_errors.values()) +# param1_perturb = cal_mod_obj.get_errors( +# cal_mod_obj.update_params([ +# em_eff_fwd_max, +# em_eff_fwd_range + 0.1, +# ]) +# ) +# assert list(param1_perturb.values()) != list(baseline_errors.values()) +# param2_perturb = cal_mod_obj.get_errors( +# cal_mod_obj.update_params([ +# em_eff_fwd_max, +# em_eff_fwd_range, +# ]) +# ) +# assert list(param2_perturb.values()) != list(baseline_errors.values()) +# print("Success!") if __name__ == "__main__": - parser = fsim.cal.get_parser( - # Defaults are set low to allow for fast run time during testing. For a good - # optimization, set this much higher. - def_save_path=None, - ) + parser = pymoo_api.get_parser() args = parser.parse_args() n_processes = args.processes @@ -242,21 +321,16 @@ def new_em_eff_range(sd_dict, new_eff_range): # should be at least as big as n_processes pop_size = args.pop_size run_minimize = not (args.skip_minimize) - if args.save_path is not None: - save_path = Path(args.save_path) - save_path.mkdir(exist_ok=True) - else: - save_path = None print("Starting calibration.") - algorithm = fsim.calibration.NSGA2( + algorithm = pymoo_api.NSGA2( # size of each population pop_size=pop_size, # LatinHyperCube sampling seems to be more effective than the default # random sampling - sampling=fsim.calibration.LHS(), + sampling=pymoo_api.LHS(), ) - termination = fsim.calibration.DMOT( + termination = pymoo_api.DMOT( # max number of generations, default of 10 is very small n_max_gen=n_max_gen, # evaluate tolerance over this interval of generations every @@ -288,7 +362,7 @@ def new_em_eff_range(sd_dict, new_eff_range): import multiprocessing with multiprocessing.Pool(n_processes) as pool: - problem = fsim.calibration.CalibrationProblem( + problem = pymoo_api.CalibrationProblem( mod_obj=cal_mod_obj, elementwise_runner=StarmapParallelization(pool.starmap), ) @@ -299,4 +373,3 @@ def new_em_eff_range(sd_dict, new_eff_range): save_path=save_path, ) -