From 525a8e11dffe654d578d5b4428180176db91b49d Mon Sep 17 00:00:00 2001 From: Chad Baker Date: Tue, 4 Feb 2025 10:06:10 -0700 Subject: [PATCH 1/5] bev is nearly caught up with hev but untested --- cal_and_val/thermal/cal_bev.py | 308 +++++++++------- cal_and_val/thermal/cal_hev.py | 76 ++-- .../f3-vehicles/2020 Chevrolet Bolt EV.yaml | 2 +- cal_and_val/thermal/val_bev.py | 331 ++++++++++++++++++ python/fastsim/demos/demo_bev_thrml.py | 2 +- 5 files changed, 546 insertions(+), 173 deletions(-) diff --git a/cal_and_val/thermal/cal_bev.py b/cal_and_val/thermal/cal_bev.py index d5ee5281..0bb48969 100644 --- a/cal_and_val/thermal/cal_bev.py +++ b/cal_and_val/thermal/cal_bev.py @@ -1,8 +1,8 @@ """ -Calibration script for 2020 Chevrolet Bolt EV +Calibration script for 2021_Hyundai_Sonata_Hybrid_Blue """ +import pprint from pathlib import Path - import numpy as np # noqa: F401 import matplotlib.pyplot as plt # noqa: F401 import seaborn as sns @@ -18,17 +18,21 @@ # Unit conversion constants mps_per_mph = 0.447 celsius_to_kelvin_offset = 273.15 +lhv_btu_per_lbm = 18_575 +lhv_joules_per_gram = 43_205.450 # Initialize seaborn plot configuration sns.set_style() veh = fsim.Vehicle.from_file(Path(__file__).parent / "f3-vehicles/2020 Chevrolet Bolt EV.yaml") veh_dict = veh.to_pydict() +veh_dict_flat = veh.to_pydict(flatten=True) sim_params_dict = fsim.SimParams.default().to_pydict() sim_params_dict["trace_miss_opts"] = "AllowChecked" sim_params = fsim.SimParams.from_pydict(sim_params_dict, skip_init=False) + # 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 @@ -41,28 +45,29 @@ cabin_temp_column = "Cabin_Temp[C]" eng_clnt_temp_column = "engine_coolant_temp_PCAN__C" cell_temp_column = "Cell_Temp[C]" +soc_column = "HVBatt_SOC_CAN4__per" # See 2020_Chevrolet_Bolt_TestSummary_201005.xlsm for cycle-level data -cyc_files: List[str] = [ +cyc_files_dict: Dict[str, Dict] = { # 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": {cell_temp_column: -6.7, "solar load [W/m^2]": None, "set temp [*C]": 22}, # 20F (heater maybe on? Col R in test summary), US06 + UDDS + HWY + UDDS - "62009053 Test Data.txt", + "62009053 Test Data.txt": {cell_temp_column: -6.7, "solar load [W/m^2]": None, "set temp [*C]": 22}, # room temperature (no HVAC), UDDS + HWY + UDDS + US06 - "62009019 Test Data.txt", + "62009019 Test Data.txt": {cell_temp_column: 22, "solar load [W/m^2]": None, "set temp [*C]": None}, # room temperature (no HVAC), US06 + UDDS + HWY + UDDS - "62009021 Test Data.txt", + "62009021 Test Data.txt": {cell_temp_column: 22, "solar load [W/m^2]": None, "set temp [*C]": None}, # 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": {cell_temp_column: 35, "solar load [W/m^2]": None, "set temp [*C]": 22}, # 95F (HVAC on), US06 - "62009041 Test Data.txt", -] -assert len(cyc_files) > 0 -cyc_files = [cyc_folder_path / cyc_file for cyc_file in cyc_files] + "62009041 Test Data.txt": {cell_temp_column: 35, "solar load [W/m^2]": None, "set temp [*C]": 22}, +} +assert len(cyc_files_dict) > 0 +cyc_files: List[Path] = [cyc_folder_path / cyc_file for cyc_file in cyc_files_dict.keys()] print("\ncyc_files:\n", '\n'.join([cf.name for cf in cyc_files]), sep='') # use random or manual selection to retain ~70% of cycles for calibration, @@ -77,60 +82,70 @@ "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] +cyc_files_for_cal: List[Path] = [cyc_file for cyc_file in cyc_files_dict if cyc_file.name in cyc_files_for_cal] 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: cyc_dict = { - "time_seconds": df["Time[s]_RawFacilities"].to_list(), - "speed_meters_per_second": (df["Dyno_Spd[mph]"] * mps_per_mph).to_list(), + "time_seconds": df[time_column].to_list(), + "speed_meters_per_second": (df[speed_column] * mps_per_mph).to_list(), "temp_amb_air_kelvin": (df[cell_temp_column] + 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) +pt_type_var = "BatteryElectricVehicle" + def veh_init(cyc_file_stem: str, dfs: Dict[str, pd.DataFrame]) -> fsim.Vehicle: + vd = deepcopy(veh_dict) + # initialize SOC - 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()) + # + vd['pt_type'][pt_type_var]['res']['state']['soc'] = \ + dfs[cyc_file_stem][soc_column].iloc[1] / 100 + assert 0 < vd['pt_type'][pt_type_var]['res']['state']['soc'] < 1, "\ninit soc: {}\nhead: {}".format( + vd['pt_type'][pt_type_var]['res']['state']['soc'], dfs[cyc_file_stem]["HVBatt_SOC_CAN4__per"].head()) # initialize cabin temp - vd['cabin']['LumpedCabin']['state']['temperature_kelvin'] = \ - dfs[cyc_file_stem]["Cabin_Lower_Vent_Temp__C"][0] + celsius_to_kelvin_offset + # not using `"Cabin_Lower_Vent_Temp__C"` because vent tempertature is way different from cabin temp! + vd['cabin'][cabin_type_var]['state']['temperature_kelvin'] = \ + dfs[cyc_file_stem][cabin_temp_column][0] + celsius_to_kelvin_offset + # initialize battery temperature to match cabin temperature because battery + # # temperature is not available in test data # 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 + vd['pt_type'][pt_type_var]['res']['thrml']['RESLumpedThermal']['state']['temperature_kelvin'] = \ + dfs[cyc_file_stem][cabin_temp_column][0] + celsius_to_kelvin_offset + + # set HVAC set point temperature + te_set = next(iter([v["set temp [*C]"] for k, v in cyc_files_dict.items() if k.replace(".txt", "") == cyc_file_stem])) + vd['hvac'][cabin_type_var]['te_set_kelvin'] = te_set + celsius_to_kelvin_offset if te_set is not None else None + return fsim.Vehicle.from_pydict(vd, skip_init=False) +def resample_df(df: pd.DataFrame) -> pd.DataFrame: + # filter out "before" time + df = df[df[time_column] >= 0.0] + dt = np.diff(df[time_column], prepend=1) + df['cumu. dist [mph*s]'] = (dt * df[speed_column]).cumsum() + init_speed = df[speed_column].iloc[0] + df = df[::10] # convert to ~1 Hz + df.reset_index(inplace=True) + dt_new = np.diff(df[time_column]) + df[speed_column] = np.concat(([init_speed], np.diff(df['cumu. dist [mph*s]']) / dt_new)) + + return df 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 + cyc_file.stem: resample_df(pd.read_csv(cyc_file, delimiter="\t")) for cyc_file in cyc_files_for_cal } -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` + +cycs_for_cal: Dict[str, fsim.Cycle] = {} # populate `cycs_for_cal` for (cyc_file_stem, df) in dfs_for_cal.items(): cyc_file_stem: str df: pd.DataFrame @@ -154,20 +169,8 @@ def veh_init(cyc_file_stem: str, dfs: Dict[str, pd.DataFrame]) -> fsim.Vehicle: 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 + cyc_file.stem: resample_df(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` @@ -186,30 +189,108 @@ def veh_init(cyc_file_stem: str, dfs: Dict[str, pd.DataFrame]) -> fsim.Vehicle: # Setup model objectives ## Parameter Functions + +cabin_type_var = 'LumpedCabin' +hvac_type_var = 'LumpedCabinAndRES' + 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 = fsim.ElectricMachine.from_pydict(sd_dict['veh']['pt_type'][pt_type_var]['em']) em.__eff_fwd_max = new_eff_max - sd_dict['veh']['pt_type']['BatteryElectricVehicle']['em'] = em.to_pydict() + sd_dict['veh']['pt_type'][pt_type_var]['em'] = em.to_pydict() return sd_dict 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 = fsim.ElectricMachine.from_pydict(sd_dict['veh']['pt_type'][pt_type_var]['em']) em.__eff_fwd_range = new_eff_range - sd_dict['veh']['pt_type']['BatteryElectricVehicle']['em'] = em.to_pydict() + sd_dict['veh']['pt_type'][pt_type_var]['em'] = em.to_pydict() + return sd_dict + +def new_cab_shell_htc_w_per_m2_k(sd_dict, new_val) -> Dict: + sd_dict['veh']['cabin'][cabin_type_var]['cab_shell_htc_to_amb_watts_per_square_meter_kelvin'] = new_val + return sd_dict + +def new_cab_htc_to_amb_stop_w_per_m2_k(sd_dict, new_val) -> Dict: + sd_dict['veh']['cabin'][cabin_type_var]['cab_htc_to_amb_stop_watts_per_square_meter_kelvin'] = new_val + return sd_dict + +def new_cab_tm_j_per_k(sd_dict, new_val) -> Dict: + sd_dict['veh']['cabin'][cabin_type_var]['heat_capacitance_joules_per_kelvin'] = new_val + return sd_dict + +def new_cab_length_m(sd_dict, new_val) -> Dict: + sd_dict['veh']['cabin'][cabin_type_var]['length_meters'] = new_val + return sd_dict + +def new_res_cndctnc_to_amb(sd_dict, new_val) -> Dict: + """ + Sets conductance from res to amb to `new_val` + """ + sd_dict["veh"]["pt_type"][pt_type_var]["res"]["thrml"]["RESLumpedThermal"]["conductance_to_amb_watts_per_kelvin"] = new_val + return sd_dict + +def new_res_cndctnc_to_cab(sd_dict, new_val) -> Dict: + """ + Sets conductance from res to cabin to `new_val` + """ + sd_dict["veh"]["pt_type"][pt_type_var]["res"]["thrml"]["RESLumpedThermal"]["conductance_to_cab_watts_per_kelvin"] = new_val + return sd_dict + +def new_res_tm_j_per_k(sd_dict, new_val) -> Dict: + sd_dict["veh"]["pt_type"][pt_type_var]["res"]["thrml"]["RESLumpedThermal"]["heat_capacitance_joules_per_kelvin"] = new_val + return sd_dict + +def new_hvac_p_res_w_per_k(sd_dict, new_val) -> Dict: + sd_dict['veh']['hvac'][hvac_type_var]['p_res_watts_per_kelvin'] = new_val return sd_dict +def new_hvac_i_res(sd_dict, new_val) -> Dict: + """Set `new_val` for HVAC integral control gain""" + sd_dict['veh']['hvac'][hvac_type_var]['i_res'] = new_val + return sd_dict + +def new_hvac_p_cabin_w_per_k(sd_dict, new_val) -> Dict: + sd_dict['veh']['hvac'][hvac_type_var]['p_cabin_watts_per_kelvin'] = new_val + return sd_dict + +def new_hvac_i_cabin(sd_dict, new_val) -> Dict: + """Set `new_val` for HVAC integral control gain""" + sd_dict['veh']['hvac'][hvac_type_var]['i_cabin'] = new_val + return sd_dict + +def new_hvac_frac_of_ideal_cop(sd_dict, new_val) -> Dict: + sd_dict['veh']['hvac'][hvac_type_var]['frac_of_ideal_cop'] = new_val + return sd_dict + +# Objective Functions -- `obj_fns` def get_mod_soc(sd_dict): - return np.array(sd_dict['veh']['pt_type']['BatteryElectricVehicle']['res']['history']['soc']) + return np.array(sd_dict['veh']['pt_type'][pt_type_var]['res']['history']['soc']) def get_exp_soc(df): return df['HVBatt_SOC_CAN4__per'] / 100 +def get_mod_cab_temp_celsius(sd_dict): + return np.array(sd_dict['veh']['cabin'][cabin_type_var]['history']['temperature_kelvin']) - celsius_to_kelvin_offset + +def get_exp_cab_temp_celsius(df): + return df[cabin_temp_column] + +def get_mod_pwr_hvac_kw(sd_dict): + return np.array(sd_dict['veh']['hvac'][cabin_type_var]['history']['pwr_aux_for_hvac_watts']) / 1e3 + +def get_exp_pwr_hvac_kw(df): + if df[cell_temp_column].mean() < 15: + pwr_hvac = [0] * len(df) + else: + pwr_hvac = df["HVAC_Power_Hioki_P3[W]"] / 1e3 + return pwr_hvac + + save_path = Path(__file__).parent / "pymoo_res" / Path(__file__).stem save_path.mkdir(exist_ok=True, parents=True) @@ -222,59 +303,32 @@ def get_exp_soc(df): get_mod_soc, get_exp_soc ), - # TODO: add objectives for: - # - achieved and cycle speed - # - engine fuel usage - # - battery temperature -- BEV only, if available - # - engine temperature - # - 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 - # - 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 + get_mod_cab_temp_celsius, + get_exp_cab_temp_celsius ), # TODO: add objectives for: # - achieved and cycle speed # - battery temperature -- BEV only, if available - # - cabin temperature # - HVAC power for cabin, if available + # - HVAC power for res, if available ), param_fns=( new_em_eff_max, new_em_eff_range, + new_cab_shell_htc_w_per_m2_k, + new_cab_htc_to_amb_stop_w_per_m2_k, + new_cab_tm_j_per_k, + new_cab_length_m, + new_res_cndctnc_to_amb, + new_res_cndctnc_to_cab, + new_res_tm_j_per_k, + new_hvac_p_res_w_per_k, + new_hvac_i_res, + new_hvac_p_cabin_w_per_k, + new_hvac_i_cabin, + new_hvac_frac_of_ideal_cop, # 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 valibrate # - battery thermal -- not necessary for HEV because battery temperature has no real effect # - thermal mass # - convection to ambient @@ -282,43 +336,29 @@ def get_exp_soc(df): ), # must match order and length of `params_fns` bounds=( - (0.80, 0.99), - (0.1, 0.6), + (0.80, 0.99), # new_em_eff_max, + (0.1, 0.6), # new_em_eff_range, + (10, 250), # new_cab_shell_htc_w_per_m2_k, + (10, 250), # new_cab_htc_to_amb_stop_w_per_m2_k, + (100e3, 350e3), # new_cab_tm_j_per_k, + (1.5, 7), # new_cab_length_m, + (1, 50), # new_res_cndctnc_to_amb, + (1, 50), # new_res_cndctnc_to_cab, + (30e3, 200e3), #new_res_tm_j_per_k, + (5, 1_000), # new_hvac_p_res_w_per_k, + (1, 100), # new_hvac_i_res, + (5, 1_000), # new_hvac_p_cabin_w_per_k, + (1, 100), #new_hvac_i_cabin, + (0.15, 0.35), # new_hvac_frac_of_ideal_cop, ), verbose=False, ) -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!") +val_mod_obj = deepcopy(cal_mod_obj) +val_mod_obj.dfs = dfs_for_val +val_mod_obj.models = sds_for_val + +# TODO: put in parameter perturbation here if __name__ == "__main__": parser = pymoo_api.get_parser() diff --git a/cal_and_val/thermal/cal_hev.py b/cal_and_val/thermal/cal_hev.py index 26b7c336..78ff707f 100644 --- a/cal_and_val/thermal/cal_hev.py +++ b/cal_and_val/thermal/cal_hev.py @@ -22,7 +22,7 @@ lhv_joules_per_gram = 43_205.450 # Initialize seaborn plot configuration -sns.set() +sns.set_style() veh = fsim.Vehicle.from_file(Path(__file__).parent / "f3-vehicles/2021_Hyundai_Sonata_Hybrid_Blue.yaml") veh_dict = veh.to_pydict() @@ -108,13 +108,15 @@ def df_to_cyc(df: pd.DataFrame) -> fsim.Cycle: } return fsim.Cycle.from_pydict(cyc_dict, skip_init=False) +pt_type_var = "HybridElectricVehicle" + def veh_init(cyc_file_stem: str, dfs: Dict[str, pd.DataFrame]) -> fsim.Vehicle: vd = deepcopy(veh_dict) # initialize SOC - vd['pt_type']['HybridElectricVehicle']['res']['state']['soc'] = \ + vd['pt_type'][pt_type_var]['res']['state']['soc'] = \ dfs[cyc_file_stem]["HVBatt_SOC_high_precision_PCAN__per"].iloc[1] / 100 - assert 0 < vd['pt_type']['HybridElectricVehicle']['res']['state']['soc'] < 1, "\ninit soc: {}\nhead: {}".format( - vd['pt_type']['HybridElectricVehicle']['res']['state']['soc'], dfs[cyc_file_stem]["HVBatt_SOC_high_precision_PCAN__per"].head()) + assert 0 < vd['pt_type'][pt_type_var]['res']['state']['soc'] < 1, "\ninit soc: {}\nhead: {}".format( + vd['pt_type'][pt_type_var]['res']['state']['soc'], dfs[cyc_file_stem]["HVBatt_SOC_high_precision_PCAN__per"].head()) # initialize cabin temp vd['cabin']['LumpedCabin']['state']['temperature_kelvin'] = \ dfs[cyc_file_stem][cabin_temp_column][0] + celsius_to_kelvin_offset @@ -122,10 +124,10 @@ def veh_init(cyc_file_stem: str, dfs: Dict[str, pd.DataFrame]) -> fsim.Vehicle: # temperature is not available in test data # 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']['HybridElectricVehicle']['res']['thrml']['RESLumpedThermal']['state']['temperature_kelvin'] = \ + vd['pt_type'][pt_type_var]['res']['thrml']['RESLumpedThermal']['state']['temperature_kelvin'] = \ dfs[cyc_file_stem]["Cabin_Temp[C]"][0] + celsius_to_kelvin_offset # initialize engine temperature - vd['pt_type']['HybridElectricVehicle']['fc']['thrml']['FuelConverterThermal']['state']['temperature_kelvin'] = \ + vd['pt_type'][pt_type_var]['fc']['thrml']['FuelConverterThermal']['state']['temperature_kelvin'] = \ dfs[cyc_file_stem][eng_clnt_temp_column][0] + celsius_to_kelvin_offset # set HVAC set point temperature te_set = next(iter([v["set temp [*C]"] for k, v in cyc_files_dict.items() if k.replace(".txt", "") == cyc_file_stem])) @@ -204,38 +206,38 @@ 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']['HybridElectricVehicle']['em']) + em = fsim.ElectricMachine.from_pydict(sd_dict['veh']['pt_type'][pt_type_var]['em']) em.__eff_fwd_max = new_eff_max - sd_dict['veh']['pt_type']['HybridElectricVehicle']['em'] = em.to_pydict() + sd_dict['veh']['pt_type'][pt_type_var]['em'] = em.to_pydict() return sd_dict 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']['HybridElectricVehicle']['em']) + em = fsim.ElectricMachine.from_pydict(sd_dict['veh']['pt_type'][pt_type_var]['em']) em.__eff_fwd_range = new_eff_range - sd_dict['veh']['pt_type']['HybridElectricVehicle']['em'] = em.to_pydict() + sd_dict['veh']['pt_type'][pt_type_var]['em'] = em.to_pydict() return sd_dict def new_fc_eff_max(sd_dict, new_eff_max) -> Dict: """ Set `new_eff_max` in `FuelConverter` """ - fc = fsim.FuelConverter.from_pydict(sd_dict['veh']['pt_type']['HybridElectricVehicle']['fc']) + fc = fsim.FuelConverter.from_pydict(sd_dict['veh']['pt_type'][pt_type_var]['fc']) fc.__eff_max = new_eff_max - sd_dict["veh"]["pt_type"]["HybridElectricVehicle"]["fc"] = fc.to_pydict() + sd_dict["veh"]["pt_type"][pt_type_var]["fc"] = fc.to_pydict() return sd_dict def new_fc_eff_range(sd_dict, new_eff_range) -> Dict: """ Set `new_eff_range` in `FuelConverter` """ - fc = fsim.FuelConverter.from_pydict(sd_dict['veh']['pt_type']['HybridElectricVehicle']['fc']) + fc = fsim.FuelConverter.from_pydict(sd_dict['veh']['pt_type'][pt_type_var]['fc']) fc_eff_max = fc.eff_max # TODO: this is a quick and dirty apprach, change to using constraints in PyMOO fc.__eff_range = min(new_eff_range, fc_eff_max * 0.95) - sd_dict['veh']['pt_type']['HybridElectricVehicle']['fc'] = fc.to_pydict() + sd_dict['veh']['pt_type'][pt_type_var]['fc'] = fc.to_pydict() return sd_dict def new_cab_shell_htc_w_per_m2_k(sd_dict, new_val) -> Dict: @@ -255,31 +257,31 @@ def new_cab_length_m(sd_dict, new_val) -> Dict: return sd_dict def new_speed_soc_disch_buffer_m_per_s(sd_dict, new_val) -> Dict: - sd_dict["veh"]["pt_type"]["HybridElectricVehicle"]["pt_cntrl"]["RGWDB"]["speed_soc_disch_buffer_meters_per_second"] = new_val + sd_dict["veh"]["pt_type"][pt_type_var]["pt_cntrl"]["RGWDB"]["speed_soc_disch_buffer_meters_per_second"] = new_val return sd_dict def new_speed_soc_disch_buffer_coeff(sd_dict, new_val) -> Dict: - sd_dict["veh"]["pt_type"]["HybridElectricVehicle"]["pt_cntrl"]["RGWDB"]["speed_soc_disch_buffer_coeff"] = new_val + sd_dict["veh"]["pt_type"][pt_type_var]["pt_cntrl"]["RGWDB"]["speed_soc_disch_buffer_coeff"] = new_val return sd_dict def new_speed_soc_fc_on_buffer_m_per_s(sd_dict, new_val) -> Dict: - sd_dict["veh"]["pt_type"]["HybridElectricVehicle"]["pt_cntrl"]["RGWDB"]["speed_soc_fc_on_buffer_meters_per_second"] = new_val + sd_dict["veh"]["pt_type"][pt_type_var]["pt_cntrl"]["RGWDB"]["speed_soc_fc_on_buffer_meters_per_second"] = new_val return sd_dict def new_speed_soc_fc_on_buffer_coeff(sd_dict, new_val) -> Dict: - sd_dict["veh"]["pt_type"]["HybridElectricVehicle"]["pt_cntrl"]["RGWDB"]["speed_soc_fc_on_buffer_coeff"] = new_val + sd_dict["veh"]["pt_type"][pt_type_var]["pt_cntrl"]["RGWDB"]["speed_soc_fc_on_buffer_coeff"] = new_val return sd_dict def new_fc_min_time_on_s(sd_dict, new_val) -> Dict: - sd_dict["veh"]["pt_type"]["HybridElectricVehicle"]["pt_cntrl"]["RGWDB"]["fc_min_time_on_seconds"] = new_val + sd_dict["veh"]["pt_type"][pt_type_var]["pt_cntrl"]["RGWDB"]["fc_min_time_on_seconds"] = new_val return sd_dict def new_frac_pwr_demand_fc_forced_on(sd_dict, new_val) -> Dict: - sd_dict["veh"]["pt_type"]["HybridElectricVehicle"]["pt_cntrl"]["RGWDB"]["frac_pwr_demand_fc_forced_on"] = new_val + sd_dict["veh"]["pt_type"][pt_type_var]["pt_cntrl"]["RGWDB"]["frac_pwr_demand_fc_forced_on"] = new_val return sd_dict def new_frac_of_most_eff_pwr_to_run_fc(sd_dict, new_val) -> Dict: - sd_dict["veh"]["pt_type"]["HybridElectricVehicle"]["pt_cntrl"]["RGWDB"]["frac_of_most_eff_pwr_to_run_fc"] = new_val + sd_dict["veh"]["pt_type"][pt_type_var]["pt_cntrl"]["RGWDB"]["frac_of_most_eff_pwr_to_run_fc"] = new_val return sd_dict def new_hvac_p_w_per_k(sd_dict, new_val) -> Dict: @@ -312,39 +314,39 @@ def new_hvac_frac_of_ideal_cop(sd_dict, new_val) -> Dict: # return sd_dict def new_fc_thrml_heat_capacitance_j_per_k(sd_dict, new_val) -> Dict: - sd_dict["veh"]["pt_type"]["HybridElectricVehicle"]["fc"]["thrml"]["FuelConverterThermal"]["heat_capacitance_joules_per_kelvin"] = new_val + sd_dict["veh"]["pt_type"][pt_type_var]["fc"]["thrml"]["FuelConverterThermal"]["heat_capacitance_joules_per_kelvin"] = new_val return sd_dict def new_fc_thrml_length_for_convection_m(sd_dict, new_val) -> Dict: - sd_dict["veh"]["pt_type"]["HybridElectricVehicle"]["fc"]["thrml"]["FuelConverterThermal"]["length_for_convection_meters"] = new_val + sd_dict["veh"]["pt_type"][pt_type_var]["fc"]["thrml"]["FuelConverterThermal"]["length_for_convection_meters"] = new_val return sd_dict def new_fc_thrml_htc_to_amb_stop_w_per_m2_k(sd_dict, new_val) -> Dict: - sd_dict["veh"]["pt_type"]["HybridElectricVehicle"]["fc"]["thrml"]["FuelConverterThermal"]["htc_to_amb_stop_watts_per_square_meter_kelvin"] = new_val + sd_dict["veh"]["pt_type"][pt_type_var]["fc"]["thrml"]["FuelConverterThermal"]["htc_to_amb_stop_watts_per_square_meter_kelvin"] = new_val return sd_dict def new_fc_thrml_conductance_from_comb_w_per_k(sd_dict, new_val) -> Dict: - sd_dict["veh"]["pt_type"]["HybridElectricVehicle"]["fc"]["thrml"]["FuelConverterThermal"]["conductance_from_comb_watts_per_kelvin"] = new_val + sd_dict["veh"]["pt_type"][pt_type_var]["fc"]["thrml"]["FuelConverterThermal"]["conductance_from_comb_watts_per_kelvin"] = new_val return sd_dict def new_fc_thrml_max_frac_from_comb(sd_dict, new_val) -> Dict: - sd_dict["veh"]["pt_type"]["HybridElectricVehicle"]["fc"]["thrml"]["FuelConverterThermal"]["max_frac_from_comb"] = new_val + sd_dict["veh"]["pt_type"][pt_type_var]["fc"]["thrml"]["FuelConverterThermal"]["max_frac_from_comb"] = new_val return sd_dict def new_fc_thrml_radiator_effectiveness(sd_dict, new_val) -> Dict: - sd_dict["veh"]["pt_type"]["HybridElectricVehicle"]["fc"]["thrml"]["FuelConverterThermal"]["radiator_effectiveness"] = new_val + sd_dict["veh"]["pt_type"][pt_type_var]["fc"]["thrml"]["FuelConverterThermal"]["radiator_effectiveness"] = new_val return sd_dict def new_fc_thrml_fc_eff_model_Exponential_offset(sd_dict, new_val) -> Dict: - sd_dict["veh"]["pt_type"]["HybridElectricVehicle"]["fc"]["thrml"]["FuelConverterThermal"]["fc_eff_model"]["Exponential"]["offset"] = new_val + sd_dict["veh"]["pt_type"][pt_type_var]["fc"]["thrml"]["FuelConverterThermal"]["fc_eff_model"]["Exponential"]["offset"] = new_val return sd_dict def new_fc_thrml_fc_eff_model_Exponential_lag(sd_dict, new_val) -> Dict: - sd_dict["veh"]["pt_type"]["HybridElectricVehicle"]["fc"]["thrml"]["FuelConverterThermal"]["fc_eff_model"]["Exponential"]["lag"] = new_val + sd_dict["veh"]["pt_type"][pt_type_var]["fc"]["thrml"]["FuelConverterThermal"]["fc_eff_model"]["Exponential"]["lag"] = new_val return sd_dict def new_fc_thrml_fc_eff_model_Exponential_minimum(sd_dict, new_val) -> Dict: - sd_dict["veh"]["pt_type"]["HybridElectricVehicle"]["fc"]["thrml"]["FuelConverterThermal"]["fc_eff_model"]["Exponential"]["minimum"] = new_val + sd_dict["veh"]["pt_type"][pt_type_var]["fc"]["thrml"]["FuelConverterThermal"]["fc_eff_model"]["Exponential"]["minimum"] = new_val return sd_dict # veh.pt_type.HybridElectricVehicle.pt_cntrl.RGWDB.speed_soc_regen_buffer_meters_per_second @@ -355,13 +357,13 @@ def new_fc_thrml_fc_eff_model_Exponential_minimum(sd_dict, new_val) -> Dict: # Objective Functions -- `obj_fns` def get_mod_soc(sd_dict): - return np.array(sd_dict['veh']['pt_type']['HybridElectricVehicle']['res']['history']['soc']) + return np.array(sd_dict['veh']['pt_type'][pt_type_var]['res']['history']['soc']) def get_exp_soc(df): return df['HVBatt_SOC_high_precision_PCAN__per'] / 100 def get_mod_fc_temp_celsius(sd_dict): - return np.array(sd_dict['veh']['pt_type']['HybridElectricVehicle']['fc']['thrml']['FuelConverterThermal']['history']['temperature_kelvin']) - celsius_to_kelvin_offset + return np.array(sd_dict['veh']['pt_type'][pt_type_var]['fc']['thrml']['FuelConverterThermal']['history']['temperature_kelvin']) - celsius_to_kelvin_offset def get_exp_fc_temp_celsius(df): return df[eng_clnt_temp_column] @@ -379,13 +381,13 @@ def get_exp_speed_m_per_s(df): return df[speed_column] * mps_per_mph def get_mod_pwr_fuel_kw(sd_dict): - return np.array(sd_dict['veh']['pt_type']['HybridElectricVehicle']['fc']['history']['pwr_fuel_watts']) / 1e3 + return np.array(sd_dict['veh']['pt_type'][pt_type_var]['fc']['history']['pwr_fuel_watts']) / 1e3 def get_exp_pwr_fuel_kw(df): return df[fuel_column] * lhv_joules_per_gram / 1e3 def get_mod_energy_fuel_megajoules(sd_dict): - return np.array(sd_dict['veh']['pt_type']['HybridElectricVehicle']['fc']['history']['energy_fuel_joules']) / 1e6 + return np.array(sd_dict['veh']['pt_type'][pt_type_var]['fc']['history']['energy_fuel_joules']) / 1e6 def get_exp_energy_fuel_megajoules(df): pwr_fuel_watts = df[fuel_column] * lhv_joules_per_gram @@ -405,7 +407,7 @@ def get_exp_pwr_hvac_kw(df): ## Constraint functions def get_fc_temp_too_hot(sd_dict): - te_fc_deg_c = sd_dict['veh']['pt_type']['HybridElectricVehicle']['fc']['thrml']['FuelConverterThermal']['state']['temperature_kelvin'] - celsius_to_kelvin_offset + te_fc_deg_c = sd_dict['veh']['pt_type'][pt_type_var]['fc']['thrml']['FuelConverterThermal']['state']['temperature_kelvin'] - celsius_to_kelvin_offset if np.any(te_fc_deg_c > 115): return 1 else: @@ -529,8 +531,8 @@ def perturb_params(pos_perturb_dec: float = 0.05, neg_perturb_dec: float = 0.1): # - `pos_perturb_doc`: perturbation percentage added to all params. Can be overridden invididually # - `neg_perturb_doc`: perturbation percentage subtracted from all params. Can be overridden invididually """ - em = fsim.ElectricMachine.from_pydict(veh_dict['pt_type']['HybridElectricVehicle']['em'], skip_init=False) - fc = fsim.FuelConverter.from_pydict(veh_dict['pt_type']['HybridElectricVehicle']['fc'], skip_init=False) + em = fsim.ElectricMachine.from_pydict(veh_dict['pt_type'][pt_type_var]['em'], skip_init=False) + fc = fsim.FuelConverter.from_pydict(veh_dict['pt_type'][pt_type_var]['fc'], skip_init=False) baseline_params_and_bounds = [ (em.eff_fwd_max, None), (em.eff_fwd_range, None), diff --git a/cal_and_val/thermal/f3-vehicles/2020 Chevrolet Bolt EV.yaml b/cal_and_val/thermal/f3-vehicles/2020 Chevrolet Bolt EV.yaml index 62584a90..131a6447 100644 --- a/cal_and_val/thermal/f3-vehicles/2020 Chevrolet Bolt EV.yaml +++ b/cal_and_val/thermal/f3-vehicles/2020 Chevrolet Bolt EV.yaml @@ -17,7 +17,7 @@ cabin: hvac: LumpedCabinAndRES: te_set_kelvin: 295.15 - te_deadband_kelvin: 1.5 + te_deadband_kelvin: 0.5 p_cabin_watts_per_kelvin: 0.0 i_cabin: 0.0 pwr_i_max_cabin_watts: 5000.0 diff --git a/cal_and_val/thermal/val_bev.py b/cal_and_val/thermal/val_bev.py index e69de29b..f1cc0505 100644 --- a/cal_and_val/thermal/val_bev.py +++ b/cal_and_val/thermal/val_bev.py @@ -0,0 +1,331 @@ +# %% +import pandas as pd +import matplotlib.pyplot as plt +import numpy as np +from copy import deepcopy + +import fastsim as fsim +from cal_bev import cal_mod_obj, val_mod_obj, save_path, cyc_files_dict +from cal_bev import time_column, speed_column, cell_temp_column +from cal_bev import mps_per_mph + +res_df = pd.read_csv(save_path / "pymoo_res_df.csv") +res_df_fuel_energy = res_df.filter(regex="get_mod_energy_fuel") +res_df_fuel_energy_summed = res_df.filter( + regex="get_mod_energy_fuel").sum(1) +best_row_fuel_energy = res_df_fuel_energy_summed.argmin() +param_vals_fuel_energy = res_df.iloc[ + best_row_fuel_energy, + :len(cal_mod_obj.param_fns)].to_numpy() + +best_row = res_df["euclidean"].argmin() +best_df = res_df.iloc[best_row, :] +param_vals_euclidean = res_df.iloc[ + best_row, + :len(cal_mod_obj.param_fns)].to_numpy() + +param_vals_best = param_vals_fuel_energy + +# getting the solved models +(errors_cal, cvs_cal, sds_cal_solved, sds_cal) = cal_mod_obj.get_errors( + sim_drives=cal_mod_obj.update_params(param_vals_best), + return_mods=True, + ) +(errors_val, cvs_val, sds_val_solved, sds_val) = val_mod_obj.get_errors( + sim_drives=val_mod_obj.update_params(param_vals_best), + return_mods=True, + ) + + # %% + + # plotting +plot_save_path = save_path / "plots" +plot_save_path.mkdir(exist_ok=True) + +for ((key, df_cal), (sd_key, sd_cal)) in zip(cal_mod_obj.dfs.items(), sds_cal_solved.items()): + if not isinstance(sd_cal, dict): + print(f"skipping {key}") + continue + assert key == sd_key + df_cal = df_cal[:len(sd_cal['veh']['history']['time_seconds'])] + + for obj_fn in cal_mod_obj.obj_fns: + fig, ax = plt.subplots(2, 1, sharex=True) + cell_temp = next(iter( + [v[cell_temp_column] + for k, v in cyc_files_dict.items() if k.replace(".txt", "") == key] + )) + fig.suptitle(f"{key}\ncell temp [*C]: {cell_temp}, calibration") + ax[0].plot( + sd_cal['veh']['history']['time_seconds'], + obj_fn[0](sd_cal), + label='mod', + ) + ax[0].plot( + df_cal[time_column], + obj_fn[1](df_cal), + label='exp', + ) + ax[0].legend() + ax[0].set_ylabel(obj_fn[0].__name__.replace('get_mod_', '')) + + ax[1].plot( + sd_cal['veh']['history']['time_seconds'], + sd_cal['veh']['history']['speed_ach_meters_per_second'], + label='mod', + ) + ax[1].plot( + df_cal[time_column], + df_cal[speed_column] * mps_per_mph, + label="exp", + ) + ax[1].legend() + ax[1].set_ylabel("Speed [m/s]") + plt.savefig(plot_save_path / f"{key}_{obj_fn[0].__name__.replace('get_mod_', '')}_cal.svg") + +for ((key, df_val), (sd_key, sd_val)) in zip(val_mod_obj.dfs.items(), sds_val_solved.items()): + if not isinstance(sd_val, dict): + print(f"skipping {key}") + continue + assert key == sd_key + + df_val = df_val[:len(sd_val['veh']['history']['time_seconds'])] + + for obj_fn in val_mod_obj.obj_fns: + fig, ax = plt.subplots(2, 1, sharex=True) + cell_temp = next(iter( + [v[cell_temp_column] + for k, v in cyc_files_dict.items() if k.replace(".txt", "") == key] + )) + fig.suptitle(f"{key}\ncell temp [*C]: {cell_temp}, validation") + ax[0].plot( + sd_val['veh']['history']['time_seconds'], + obj_fn[0](sd_val), + label='mod', + ) + ax[0].plot( + df_val[time_column], + obj_fn[1](df_val), + label='exp', + ) + ax[0].legend() + ax[0].set_ylabel(obj_fn[0].__name__.replace('get_mod_', '')) + + ax[1].plot( + sd_val['veh']['history']['time_seconds'], + sd_val['veh']['history']['speed_ach_meters_per_second'], + label='mod', + ) + ax[1].plot( + df_val[time_column], + df_val[speed_column] * mps_per_mph, + label='exp', + ) + ax[1].legend() + ax[1].set_ylabel("Speed [m/s]") + plt.savefig(plot_save_path / f"{key}_{obj_fn[0].__name__.replace('get_mod_', '')}_val.svg") +# %% +# function for plot formatting +def draw_error_zones(ax): + """Draw 0%, ±5%, ±10% error regions on MPL Axes object""" + xl, xu = ax.get_xlim() + yl, yu = ax.get_ylim() + l = min(xl, yl) + u = max(xu, yu) + lims = np.array([0, u * 1.01]) + + # Plot 0% error diagonalx + ax.plot(lims, lims, linestyle="dotted", color="g", label="0% error") + + # Plot ±5%, ±10% error regions with transparencies + counter = 0 + error_1 = 0 + error_2 = 0 + error_3 = 0 + for err, alpha in zip((0.05, 0.10, 0.15), (0.35, 0.2, 0.15)): + error = ax.fill_between( + lims, + lims * (1 - err), + lims * (1 + err), + alpha=alpha, + color="g", + label=f"±{err*100:.0f}% error", + ) + + ax.set_xlim(left=l, right=u) + ax.set_ylim(bottom=l, top=u) + # ax.legend(loc="lower right", framealpha=0.5, fontsize=8, borderpad=0.25) + + return error + +# %% +# Scatter plots with temperature effects + +fuel_energy_exp_cal = [] +fuel_energy_mod_cal = [] +for ((key, df_cal), (sd_key, sd_cal)) in zip(cal_mod_obj.dfs.items(), sds_cal_solved.items()): + if not isinstance(sd_cal, dict): + print(f"skipping {key}") + continue + assert key == sd_key + + df_cal = df_cal[:len(sd_cal['veh']['history']['time_seconds'])] + + mod_energy_fuel = get_mod_energy_fuel_megajoules(sd_cal) + exp_energy_fuel = get_exp_energy_fuel_megajoules(df_cal) + assert len(mod_energy_fuel) == len(exp_energy_fuel) + + fuel_energy_mod_cal.append( + mod_energy_fuel[-1] + ) + fuel_energy_exp_cal.append( + exp_energy_fuel.iloc[-1] + ) + +fuel_energy_exp_val = [] +fuel_energy_mod_val = [] + +for ((key, df_val), (sd_key, sd_val)) in zip(val_mod_obj.dfs.items(), sds_val_solved.items()): + if not isinstance(sd_val, dict): + print(f"skipping {key}") + continue + assert key == sd_key + + df_val = df_val[:len(sd_val['veh']['history']['time_seconds'])] + + mod_energy_fuel = get_mod_energy_fuel_megajoules(sd_val) + exp_energy_fuel = get_exp_energy_fuel_megajoules(df_val) + assert len(mod_energy_fuel) == len(exp_energy_fuel) + + fuel_energy_mod_val.append( + mod_energy_fuel[-1] + ) + fuel_energy_exp_val.append( + exp_energy_fuel.iloc[-1] + ) + + +fig, ax = plt.subplots() +fig.suptitle("Model v. Test Data With Thermal Effects") +ax.scatter( + fuel_energy_exp_cal, + fuel_energy_mod_cal, + label='cal', +) +ax.scatter( + fuel_energy_exp_val, + fuel_energy_mod_val, + label='val', +) +draw_error_zones(ax) +ax.set_xlabel("Test Data Fuel Used [MJ]") +ax.set_ylabel("FASTSim Fuel Used [MJ]") +ax.legend() +plt.savefig(plot_save_path / "scatter with thrml effects.svg") + +# %% + +# Scatter plots without temperature effects + +fuel_energy_mod_cal_no_thrml = [] +fuel_energy_exp_cal_no_thrml = [] +for ((key, df_cal), (sd_key, sd_cal)) in zip(cal_mod_obj.dfs.items(), sds_cal.items()): + if not isinstance(sd_cal, dict): + print(f"skipping {key}") + continue + assert key == sd_key + + sd_cal_no_thrml = deepcopy(sd_cal) + + sd_cal_no_thrml['veh']['hvac'] = 'None' + sd_cal_no_thrml['veh']['cabin'] = 'None' + sd_cal_no_thrml['veh']['pt_type']['HybridElectricVehicle']['fc']['thrml'] = 'None' + sd_cal_no_thrml['veh']['pt_type']['HybridElectricVehicle']['res']['thrml'] = 'None' + res = fsim.ReversibleEnergyStorage.from_pydict( + sd_cal_no_thrml['veh']['pt_type']['HybridElectricVehicle']['res'], skip_init=False) + res.set_default_pwr_interp() + sd_cal_no_thrml['veh']['pt_type']['HybridElectricVehicle']['res'] = res.to_pydict() + sd_cal_no_thrml['veh']['pt_type']['HybridElectricVehicle']['pt_cntrl']['RGWDB']['temp_fc_allowed_off_kelvin'] = None + sd_cal_no_thrml['veh']['pt_type']['HybridElectricVehicle']['pt_cntrl']['RGWDB']['temp_fc_forced_on_kelvin'] = None + + sd_cal_no_thrml = fsim.SimDrive.from_pydict( + sd_cal_no_thrml, skip_init=False) + try: + sd_cal_no_thrml.walk_once() + except Exception: + pass + sd_cal_no_thrml = sd_cal_no_thrml.to_pydict() + + df_cal = df_cal[:len(sd_cal_no_thrml['veh']['history']['time_seconds'])] + + mod_energy_fuel = get_mod_energy_fuel_megajoules(sd_cal_no_thrml) + exp_energy_fuel = get_exp_energy_fuel_megajoules(df_cal) + assert len(mod_energy_fuel) == len(exp_energy_fuel) + + fuel_energy_mod_cal_no_thrml.append( + mod_energy_fuel[-1] + ) + fuel_energy_exp_cal_no_thrml.append( + exp_energy_fuel.iloc[-1] + ) + +fuel_energy_exp_val_no_thrml = [] +fuel_energy_mod_val_no_thrml = [] +for ((key, df_val), (sd_key, sd_val)) in zip(val_mod_obj.dfs.items(), sds_val.items()): + if not isinstance(sd_val, dict): + print(f"skipping {key}") + continue + assert key == sd_key + + sd_val_no_thrml = deepcopy(sd_val) + + sd_val_no_thrml['veh']['hvac'] = 'None' + sd_val_no_thrml['veh']['cabin'] = 'None' + sd_val_no_thrml['veh']['pt_type']['HybridElectricVehicle']['fc']['thrml'] = 'None' + sd_val_no_thrml['veh']['pt_type']['HybridElectricVehicle']['res']['thrml'] = 'None' + res = fsim.ReversibleEnergyStorage.from_pydict( + sd_val_no_thrml['veh']['pt_type']['HybridElectricVehicle']['res'], skip_init=False) + res.set_default_pwr_interp() + sd_val_no_thrml['veh']['pt_type']['HybridElectricVehicle']['res'] = res.to_pydict() + sd_val_no_thrml['veh']['pt_type']['HybridElectricVehicle']['pt_cntrl']['RGWDB']['temp_fc_allowed_off_kelvin'] = None + sd_val_no_thrml['veh']['pt_type']['HybridElectricVehicle']['pt_cntrl']['RGWDB']['temp_fc_forced_on_kelvin'] = None + sd_val_no_thrml = fsim.SimDrive.from_pydict( + sd_val_no_thrml, skip_init=False) + try: + sd_val_no_thrml.walk_once() + except Exception: + pass + sd_val_no_thrml = sd_val_no_thrml.to_pydict() + + df_val = df_val[:len(sd_val_no_thrml['veh']['history']['time_seconds'])] + + mod_energy_fuel = get_mod_energy_fuel_megajoules(sd_val_no_thrml) + exp_energy_fuel = get_exp_energy_fuel_megajoules(df_val) + assert len(mod_energy_fuel) == len(exp_energy_fuel) + + fuel_energy_mod_val_no_thrml.append( + mod_energy_fuel[-1] + ) + fuel_energy_exp_val_no_thrml.append( + exp_energy_fuel.iloc[-1] + ) + +fig, ax = plt.subplots() +fig.suptitle("Model v. Test Data Without Thermal Effects") +ax.scatter( + fuel_energy_exp_cal, + fuel_energy_mod_cal_no_thrml, + label='cal', +) +ax.scatter( + fuel_energy_exp_val, + fuel_energy_mod_val_no_thrml, + label='val', +) +draw_error_zones(ax) +ax.set_xlabel("Test Data Fuel Used [MJ]") +ax.set_ylabel("FASTSim Fuel Used [MJ]") +ax.legend() +plt.savefig(plot_save_path / "scatter without thrml effects.svg") + +# %% diff --git a/python/fastsim/demos/demo_bev_thrml.py b/python/fastsim/demos/demo_bev_thrml.py index d3689a3e..70a38a25 100644 --- a/python/fastsim/demos/demo_bev_thrml.py +++ b/python/fastsim/demos/demo_bev_thrml.py @@ -19,7 +19,7 @@ # if environment var `SHOW_PLOTS=false` is set, no plots are shown SHOW_PLOTS = os.environ.get("SHOW_PLOTS", "true").lower() == "true" # if environment var `SAVE_FIGS=true` is set, save plots -SAVE_FIGS = os.environ.get("SAVE_FIGS", "false").lower() == "true" +kSAVE_FIGS = os.environ.get("SAVE_FIGS", "false").lower() == "true" # `fastsim3` -- load vehicle and cycle, build simulation, and run # %% From 93cdecd8253ae3d12b2b505c92ac34fef5955984 Mon Sep 17 00:00:00 2001 From: Chad Baker Date: Tue, 4 Feb 2025 10:40:19 -0700 Subject: [PATCH 2/5] cleaned up several errors --- cal_and_val/thermal/cal_bev.py | 16 ++-- .../hvac/hvac_sys_for_lumped_cabin_and_res.rs | 83 ++++++++++++------- python/fastsim/__init__.py | 10 +-- 3 files changed, 59 insertions(+), 50 deletions(-) diff --git a/cal_and_val/thermal/cal_bev.py b/cal_and_val/thermal/cal_bev.py index 0bb48969..5cf7adeb 100644 --- a/cal_and_val/thermal/cal_bev.py +++ b/cal_and_val/thermal/cal_bev.py @@ -36,13 +36,13 @@ # 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__).parent / "dyno_test_data/2020 Chevrolet Bolt EV/Extended Datasets" +cyc_folder_path = Path(__file__).parent / "dyno_test_data/2020 Chevrolet Bolt/Extended Datasets" assert cyc_folder_path.exists(), cyc_folder_path # Test data columns time_column = "Time[s]_RawFacilities" speed_column = "Dyno_Spd[mph]" -cabin_temp_column = "Cabin_Temp[C]" +cabin_temp_column = "Cabin_Driver_Headrest_Temp__C" eng_clnt_temp_column = "engine_coolant_temp_PCAN__C" cell_temp_column = "Cell_Temp[C]" soc_column = "HVBatt_SOC_CAN4__per" @@ -82,7 +82,7 @@ "62009040 Test Data.txt" # "62009041 Test Data.txt" ] -cyc_files_for_cal: List[Path] = [cyc_file for cyc_file in cyc_files_dict if cyc_file.name in cyc_files_for_cal] +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, cyc_files_for_cal print("\ncyc_files_for_cal:\n", '\n'.join([cf.name for cf in cyc_files_for_cal]), sep='') @@ -97,19 +97,18 @@ def df_to_cyc(df: pd.DataFrame) -> fsim.Cycle: return fsim.Cycle.from_pydict(cyc_dict, skip_init=False) pt_type_var = "BatteryElectricVehicle" +cabin_type_var = 'LumpedCabin' +hvac_type_var = 'LumpedCabinAndRES' def veh_init(cyc_file_stem: str, dfs: Dict[str, pd.DataFrame]) -> fsim.Vehicle: - vd = deepcopy(veh_dict) # initialize SOC - # vd['pt_type'][pt_type_var]['res']['state']['soc'] = \ dfs[cyc_file_stem][soc_column].iloc[1] / 100 assert 0 < vd['pt_type'][pt_type_var]['res']['state']['soc'] < 1, "\ninit soc: {}\nhead: {}".format( vd['pt_type'][pt_type_var]['res']['state']['soc'], dfs[cyc_file_stem]["HVBatt_SOC_CAN4__per"].head()) # initialize cabin temp - # not using `"Cabin_Lower_Vent_Temp__C"` because vent tempertature is way different from cabin temp! vd['cabin'][cabin_type_var]['state']['temperature_kelvin'] = \ dfs[cyc_file_stem][cabin_temp_column][0] + celsius_to_kelvin_offset @@ -123,7 +122,7 @@ def veh_init(cyc_file_stem: str, dfs: Dict[str, pd.DataFrame]) -> fsim.Vehicle: # set HVAC set point temperature te_set = next(iter([v["set temp [*C]"] for k, v in cyc_files_dict.items() if k.replace(".txt", "") == cyc_file_stem])) - vd['hvac'][cabin_type_var]['te_set_kelvin'] = te_set + celsius_to_kelvin_offset if te_set is not None else None + vd['hvac'][hvac_type_var]['te_set_kelvin'] = te_set + celsius_to_kelvin_offset if te_set is not None else None return fsim.Vehicle.from_pydict(vd, skip_init=False) @@ -190,9 +189,6 @@ def resample_df(df: pd.DataFrame) -> pd.DataFrame: # Setup model objectives ## Parameter Functions -cabin_type_var = 'LumpedCabin' -hvac_type_var = 'LumpedCabinAndRES' - def new_em_eff_max(sd_dict, new_eff_max) -> Dict: """ Set `new_eff_max` in `ElectricMachine` diff --git a/fastsim-core/src/vehicle/hvac/hvac_sys_for_lumped_cabin_and_res.rs b/fastsim-core/src/vehicle/hvac/hvac_sys_for_lumped_cabin_and_res.rs index dd514b48..b3e843d1 100644 --- a/fastsim-core/src/vehicle/hvac/hvac_sys_for_lumped_cabin_and_res.rs +++ b/fastsim-core/src/vehicle/hvac/hvac_sys_for_lumped_cabin_and_res.rs @@ -12,7 +12,7 @@ use super::*; /// HVAC system for [LumpedCabin] and [ReversibleEnergyStorage::thrml] pub struct HVACSystemForLumpedCabinAndRES { /// set point temperature - pub te_set: si::Temperature, + pub te_set: Option, /// Deadband range. Any cabin temperature within this range of `te_set` /// results in no HVAC power draw pub te_deadband: si::TemperatureInterval, @@ -62,7 +62,7 @@ pub struct HVACSystemForLumpedCabinAndRES { impl Default for HVACSystemForLumpedCabinAndRES { fn default() -> Self { Self { - te_set: *TE_STD_AIR, + te_set: Some(*TE_STD_AIR), te_deadband: 1.5 * uc::KELVIN_INT, p_cabin: Default::default(), i_cabin: Default::default(), @@ -300,44 +300,49 @@ impl HVACSystemForLumpedCabinAndRES { cab_heat_cap: si::HeatCapacity, dt: si::Time, ) -> anyhow::Result { - let pwr_thrml_hvac_to_cabin = if cab_state.temperature <= (self.te_set + self.te_deadband) - && cab_state.temperature >= (self.te_set - self.te_deadband) + let te_set = match self.te_set { + Some(te_set) => te_set, + None => return Ok(si::Power::ZERO), + }; + let pwr_thrml_hvac_to_cabin = if cab_state.temperature <= (te_set + self.te_deadband) + && cab_state.temperature >= (te_set - self.te_deadband) { // inside deadband; no hvac power is needed - self.state.pwr_i = si::Power::ZERO; // reset to 0.0 - self.state.pwr_p = si::Power::ZERO; - self.state.pwr_d = si::Power::ZERO; + self.state.pwr_i_cab = si::Power::ZERO; // reset to 0.0 + self.state.pwr_p_cab = si::Power::ZERO; + self.state.pwr_d_cab = si::Power::ZERO; si::Power::ZERO } else { // outside deadband let te_delta_vs_set = (cab_state.temperature.get::() - - self.te_set.get::()) + - te_set.get::()) * uc::KELVIN_INT; - self.state.pwr_p = -self.p_cabin * te_delta_vs_set; - self.state.pwr_i -= self.i_cabin * uc::W / uc::KELVIN / uc::S * te_delta_vs_set * dt; - self.state.pwr_i = self + self.state.pwr_p_cab = -self.p_cabin * te_delta_vs_set; + self.state.pwr_i_cab -= + self.i_cabin * uc::W / uc::KELVIN / uc::S * te_delta_vs_set * dt; + self.state.pwr_i_cab = self .state - .pwr_i + .pwr_i_cab .max(-self.pwr_i_max_cabin) .min(self.pwr_i_max_cabin); - self.state.pwr_d = -self.d_cabin * uc::J / uc::KELVIN + self.state.pwr_d_cab = -self.d_cabin * uc::J / uc::KELVIN * ((cab_state.temperature.get::() - cab_state.temp_prev.get::()) * uc::KELVIN_INT / dt); - let pwr_thrml_hvac_to_cabin: si::Power = - if cab_state.temperature > self.te_set + self.te_deadband { + let pwr_thrml_hvac_to_cab: si::Power = + if cab_state.temperature > te_set + self.te_deadband { // COOLING MODE; cabin is hotter than set point - if self.state.pwr_i > si::Power::ZERO { + if self.state.pwr_i_cab > si::Power::ZERO { // If `pwr_i` is greater than zero, reset to switch from heating to cooling - self.state.pwr_i = si::Power::ZERO; + self.state.pwr_i_cab = si::Power::ZERO; } let mut pwr_thrml_hvac_to_cab = - (self.state.pwr_p + self.state.pwr_i + self.state.pwr_d) + (self.state.pwr_p_cab + self.state.pwr_i_cab + self.state.pwr_d_cab) .max(-self.pwr_thrml_max); if (-pwr_thrml_hvac_to_cab / self.state.cop) > self.pwr_aux_for_hvac_max { @@ -347,16 +352,21 @@ impl HVACSystemForLumpedCabinAndRES { } else { self.state.pwr_aux_for_hvac = pwr_thrml_hvac_to_cab / self.state.cop; } + ensure!( + pwr_thrml_hvac_to_cab < si::Power::ZERO, + "{}\nHVAC should be cooling cabin", + format_dbg!(pwr_thrml_hvac_to_cab) + ); pwr_thrml_hvac_to_cab } else { // HEATING MODE; cabin is colder than set point - if self.state.pwr_i < si::Power::ZERO { + if self.state.pwr_i_cab < si::Power::ZERO { // If `pwr_i` is less than zero reset to switch from cooling to heating - self.state.pwr_i = si::Power::ZERO; + self.state.pwr_i_cab = si::Power::ZERO; } let mut pwr_thrml_hvac_to_cabin: si::Power = - (self.state.pwr_p + self.state.pwr_i + self.state.pwr_d) + (self.state.pwr_p_cab + self.state.pwr_i_cab + self.state.pwr_d_cab) .min(self.pwr_thrml_max); // Assumes blower has negligible impact on aux load, may want to revise later @@ -370,7 +380,12 @@ impl HVACSystemForLumpedCabinAndRES { .with_context(|| format_dbg!())?; pwr_thrml_hvac_to_cabin }; - pwr_thrml_hvac_to_cabin + ensure!( + pwr_thrml_hvac_to_cab >= si::Power::ZERO, + "{}\nHVAC should be heating cabin", + format_dbg!(pwr_thrml_hvac_to_cab) + ); + pwr_thrml_hvac_to_cab }; Ok(pwr_thrml_hvac_to_cabin) } @@ -383,8 +398,12 @@ impl HVACSystemForLumpedCabinAndRES { res_temp_prev: si::Temperature, dt: si::Time, ) -> anyhow::Result { - let pwr_thrml_hvac_to_res = if res_temp <= self.te_set + self.te_deadband - && res_temp >= self.te_set - self.te_deadband + let te_set = match self.te_set { + Some(te_set) => te_set, + None => return Ok(si::Power::ZERO), + }; + let pwr_thrml_hvac_to_res = if res_temp <= te_set + self.te_deadband + && res_temp >= te_set - self.te_deadband { // inside deadband; no hvac power is needed @@ -395,7 +414,7 @@ impl HVACSystemForLumpedCabinAndRES { } else { // outside deadband let te_delta_vs_set = (res_temp.get::() - - self.te_set.get::()) + - te_set.get::()) * uc::KELVIN_INT; self.state.pwr_p_res = -self.p_res * te_delta_vs_set; self.state.pwr_i_res -= self.i_res * uc::W / uc::KELVIN / uc::S * te_delta_vs_set * dt; @@ -410,7 +429,7 @@ impl HVACSystemForLumpedCabinAndRES { * uc::KELVIN_INT / dt); - let pwr_thrml_hvac_to_res: si::Power = if res_temp > self.te_set + self.te_deadband { + let pwr_thrml_hvac_to_res: si::Power = if res_temp > te_set + self.te_deadband { // COOLING MODE; Reversible Energy Storage is hotter than set point if self.state.pwr_i_res > si::Power::ZERO { @@ -493,17 +512,17 @@ pub struct HVACSystemForLumpedCabinAndRESState { /// time step counter pub i: u32, /// portion of total HVAC cooling/heating (negative/positive) power due to proportional gain - pub pwr_p: si::Power, + pub pwr_p_cab: si::Power, /// portion of total HVAC cooling/heating (negative/positive) cumulative energy due to proportional gain - pub energy_p: si::Energy, + pub energy_p_cab: si::Energy, /// portion of total HVAC cooling/heating (negative/positive) power due to integral gain - pub pwr_i: si::Power, + pub pwr_i_cab: si::Power, /// portion of total HVAC cooling/heating (negative/positive) cumulative energy due to integral gain - pub energy_i: si::Energy, + pub energy_i_cab: si::Energy, /// portion of total HVAC cooling/heating (negative/positive) power due to derivative gain - pub pwr_d: si::Power, + pub pwr_d_cab: si::Power, /// portion of total HVAC cooling/heating (negative/positive) cumulative energy due to derivative gain - pub energy_d: si::Energy, + pub energy_d_cab: si::Energy, /// portion of total HVAC cooling/heating (negative/positive) power to [ReversibleEnergyStorage::thrml] due to proportional gain pub pwr_p_res: si::Power, /// portion of total HVAC cooling/heating (negative/positive) cumulative energy to [ReversibleEnergyStorage::thrml] due to proportional gain diff --git a/python/fastsim/__init__.py b/python/fastsim/__init__.py index 75bd8f35..1fc17b70 100644 --- a/python/fastsim/__init__.py +++ b/python/fastsim/__init__.py @@ -181,14 +181,8 @@ def from_pydict(cls, pydict: Dict, data_fmt: str = "msg_pack", skip_init: bool = obj = cls.from_yaml(yaml.dump(pydict), skip_init=skip_init) case "msg_pack": import msgpack - try: - obj = cls.from_msg_pack( - msgpack.packb(pydict), skip_init=skip_init) - except Exception as err: - print( - f"{err}\nFalling back to YAML.") - obj = cls.from_pydict( - pydict, data_fmt="yaml", skip_init=skip_init) + obj = cls.from_msg_pack( + msgpack.packb(pydict), skip_init=skip_init) case "json": from json import dumps obj = cls.from_json(dumps(pydict), skip_init=skip_init) From 81f706bf3d1d8170292e409c0aa10acefb2dfe49 Mon Sep 17 00:00:00 2001 From: Chad Baker Date: Tue, 4 Feb 2025 10:47:05 -0700 Subject: [PATCH 3/5] need to add param perturbation --- cal_and_val/thermal/cal_bev.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cal_and_val/thermal/cal_bev.py b/cal_and_val/thermal/cal_bev.py index 5cf7adeb..2aead0c7 100644 --- a/cal_and_val/thermal/cal_bev.py +++ b/cal_and_val/thermal/cal_bev.py @@ -347,6 +347,7 @@ def get_exp_pwr_hvac_kw(df): (1, 100), #new_hvac_i_cabin, (0.15, 0.35), # new_hvac_frac_of_ideal_cop, ), + constr_fns=(), verbose=False, ) From 64833a17104b57b216614d85f7cdbee89dd40cca Mon Sep 17 00:00:00 2001 From: Chad Baker Date: Tue, 4 Feb 2025 11:15:12 -0700 Subject: [PATCH 4/5] BEV cal runs but euclid min is constant and some cycles completely fail to run --- cal_and_val/thermal/cal_bev.py | 77 +++++++++++++++++++++++++++++++++- cal_and_val/thermal/cal_hev.py | 2 +- cal_and_val/thermal/val_bev.py | 1 + 3 files changed, 77 insertions(+), 3 deletions(-) diff --git a/cal_and_val/thermal/cal_bev.py b/cal_and_val/thermal/cal_bev.py index 2aead0c7..15588cd6 100644 --- a/cal_and_val/thermal/cal_bev.py +++ b/cal_and_val/thermal/cal_bev.py @@ -22,7 +22,7 @@ lhv_joules_per_gram = 43_205.450 # Initialize seaborn plot configuration -sns.set_style() +sns.set_style("darkgrid") veh = fsim.Vehicle.from_file(Path(__file__).parent / "f3-vehicles/2020 Chevrolet Bolt EV.yaml") veh_dict = veh.to_pydict() @@ -188,7 +188,7 @@ def resample_df(df: pd.DataFrame) -> pd.DataFrame: # Setup model objectives ## Parameter Functions - +# `param_fns` def new_em_eff_max(sd_dict, new_eff_max) -> Dict: """ Set `new_eff_max` in `ElectricMachine` @@ -356,8 +356,81 @@ def get_exp_pwr_hvac_kw(df): val_mod_obj.models = sds_for_val # TODO: put in parameter perturbation here +def perturb_params(pos_perturb_dec: float = 0.05, neg_perturb_dec: float = 0.1): + """ + # Arguments: + # - `pos_perturb_doc`: perturbation percentage added to all params. Can be overridden invididually + # - `neg_perturb_doc`: perturbation percentage subtracted from all params. Can be overridden invididually + """ + em = fsim.ElectricMachine.from_pydict(veh_dict['pt_type'][pt_type_var]['em'], skip_init=False) + baseline_params_and_bounds = [ + (em.eff_fwd_max, None), + (em.eff_fwd_range, None), + (veh_dict['cabin'][cabin_type_var]['cab_shell_htc_to_amb_watts_per_square_meter_kelvin'], None), + (veh_dict['cabin'][cabin_type_var]['cab_htc_to_amb_stop_watts_per_square_meter_kelvin'], None), + (veh_dict['cabin'][cabin_type_var]['heat_capacitance_joules_per_kelvin'], None), + (veh_dict['cabin'][cabin_type_var]['length_meters'], None), + (veh_dict["pt_type"][pt_type_var]["res"]["thrml"]["RESLumpedThermal"]["conductance_to_amb_watts_per_kelvin"], None), + (veh_dict["pt_type"][pt_type_var]["res"]["thrml"]["RESLumpedThermal"]["conductance_to_cab_watts_per_kelvin"], None), + (veh_dict["pt_type"][pt_type_var]["res"]["thrml"]["RESLumpedThermal"]["heat_capacitance_joules_per_kelvin"], None), + (veh_dict['hvac'][hvac_type_var]['p_res_watts_per_kelvin'], None), + (veh_dict['hvac'][hvac_type_var]['i_res'], None), + (veh_dict['hvac'][hvac_type_var]['p_cabin_watts_per_kelvin'], None), + (veh_dict['hvac'][hvac_type_var]['i_cabin'], None), + (veh_dict['hvac'][hvac_type_var]['frac_of_ideal_cop'] , None), + ] + + baseline_params = [bpb[0] for bpb in baseline_params_and_bounds] + + 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([param for param in baseline_params]) + )[0] + + for i, param_and_bounds in enumerate(baseline_params_and_bounds): + param = param_and_bounds[0] + bounds = param_and_bounds[1] + # +5% + if bounds is not None: + param_pos_perturb_dec = bounds[0] + param_neg_perturb_dec = bounds[1] + else: + param_pos_perturb_dec = pos_perturb_dec + param_neg_perturb_dec = neg_perturb_dec + + assert param_pos_perturb_dec >= 0 + assert param_neg_perturb_dec >= 0 + + perturbed_params = baseline_params.copy() + perturbed_params[i] = param * (1 + param_pos_perturb_dec) + perturbed_errors = cal_mod_obj.get_errors(cal_mod_obj.update_params(perturbed_params)) + if np.all(perturbed_errors == baseline_errors): + print("\nperturbed_errros:") + pprint.pp(perturbed_errors) + print("baseline_errors") + pprint.pp(baseline_errors) + print("") + raise Exception(f"+{100 * param_pos_perturb_dec}% perturbation failed for param {cal_mod_obj.param_fns[i].__name__}") + + # -5% + perturbed_params = baseline_params.copy() + perturbed_params[i] = param * (1 - param_neg_perturb_dec) + perturbed_errors = cal_mod_obj.get_errors(cal_mod_obj.update_params(perturbed_params)) + if np.all(perturbed_errors == baseline_errors): + print("\nperturbed_errros:") + pprint.pp(perturbed_errors) + print("baseline_errors") + pprint.pp(baseline_errors) + print("") + raise Exception(f"-{100 * param_neg_perturb_dec}% perturbation failed for param {cal_mod_obj.param_fns[i].__name__}") + + print("Success!") if __name__ == "__main__": + print("Params and bounds:") + pprint.pp(cal_mod_obj.params_and_bounds()) + print("") + perturb_params() parser = pymoo_api.get_parser() args = parser.parse_args() diff --git a/cal_and_val/thermal/cal_hev.py b/cal_and_val/thermal/cal_hev.py index 78ff707f..df7b1123 100644 --- a/cal_and_val/thermal/cal_hev.py +++ b/cal_and_val/thermal/cal_hev.py @@ -22,7 +22,7 @@ lhv_joules_per_gram = 43_205.450 # Initialize seaborn plot configuration -sns.set_style() +sns.set_style("darkgrid") veh = fsim.Vehicle.from_file(Path(__file__).parent / "f3-vehicles/2021_Hyundai_Sonata_Hybrid_Blue.yaml") veh_dict = veh.to_pydict() diff --git a/cal_and_val/thermal/val_bev.py b/cal_and_val/thermal/val_bev.py index f1cc0505..b2e3e59c 100644 --- a/cal_and_val/thermal/val_bev.py +++ b/cal_and_val/thermal/val_bev.py @@ -4,6 +4,7 @@ import numpy as np from copy import deepcopy +# local import fastsim as fsim from cal_bev import cal_mod_obj, val_mod_obj, save_path, cyc_files_dict from cal_bev import time_column, speed_column, cell_temp_column From ef61061d688a66bf331cc82ae264ddd644af45c2 Mon Sep 17 00:00:00 2001 From: Chad Baker Date: Tue, 4 Feb 2025 13:11:20 -0700 Subject: [PATCH 5/5] still need to fix BEV hvac cop calculation --- .../f3-vehicles/2020 Chevrolet Bolt EV.yaml | 20 +++++----- .../hvac/hvac_sys_for_lumped_cabin_and_res.rs | 40 +++++++++++++------ python/fastsim/pymoo_api.py | 3 +- 3 files changed, 39 insertions(+), 24 deletions(-) diff --git a/cal_and_val/thermal/f3-vehicles/2020 Chevrolet Bolt EV.yaml b/cal_and_val/thermal/f3-vehicles/2020 Chevrolet Bolt EV.yaml index 131a6447..2296926a 100644 --- a/cal_and_val/thermal/f3-vehicles/2020 Chevrolet Bolt EV.yaml +++ b/cal_and_val/thermal/f3-vehicles/2020 Chevrolet Bolt EV.yaml @@ -18,20 +18,20 @@ hvac: LumpedCabinAndRES: te_set_kelvin: 295.15 te_deadband_kelvin: 0.5 - p_cabin_watts_per_kelvin: 0.0 - i_cabin: 0.0 - pwr_i_max_cabin_watts: 5000.0 - d_cabin: 0.0 - p_res_watts_per_kelvin: 0.0 - i_res: 0.0 - pwr_i_max_res_watts: 5000.0 - d_res: 0.0 - pwr_thrml_max_watts: 10000.0 + p_cabin_watts_per_kelvin: 500.0 + i_cabin: 40.0 + pwr_i_max_cabin_watts: 15000.0 + d_cabin: 5.0 + p_res_watts_per_kelvin: 500.0 + i_res: 40.0 + pwr_i_max_res_watts: 15000.0 + d_res: 5.0 + pwr_thrml_max_watts: 15000.0 frac_of_ideal_cop: 0.15 cabin_heat_source: ResistanceHeater res_heat_source: ResistanceHeater res_cooling_source: HVAC - pwr_aux_for_hvac_max_watts: 5000.0 + pwr_aux_for_hvac_max_watts: 10000.0 pt_type: BatteryElectricVehicle: res: diff --git a/fastsim-core/src/vehicle/hvac/hvac_sys_for_lumped_cabin_and_res.rs b/fastsim-core/src/vehicle/hvac/hvac_sys_for_lumped_cabin_and_res.rs index b3e843d1..a3ba2258 100644 --- a/fastsim-core/src/vehicle/hvac/hvac_sys_for_lumped_cabin_and_res.rs +++ b/fastsim-core/src/vehicle/hvac/hvac_sys_for_lumped_cabin_and_res.rs @@ -117,13 +117,17 @@ impl HVACSystemForLumpedCabinAndRES { let (res_temp, res_temp_prev) = res_temps; ensure!(!res_temp.is_nan(), format_dbg!(res_temp)); ensure!(!res_temp_prev.is_nan(), format_dbg!(res_temp_prev)); + // TODO: split `solve_for_cabin` and `solve_for_res` into + // `solve_for_cabin_requsted` and `solve_for_res_requsted` for the initial + // calculation and then `solve_for_cabin_ach` and `solve_for_res_ach` after solving + // for cop let mut pwr_thrml_hvac_to_cabin = self .solve_for_cabin(te_fc, cab_state, cab_heat_cap, dt) .with_context(|| format_dbg!())?; let mut pwr_thrml_hvac_to_res: si::Power = self .solve_for_res(res_temp, res_temp_prev, dt) .with_context(|| format_dbg!())?; - let (cop_ideal, te_ref) = if pwr_thrml_hvac_to_res + pwr_thrml_hvac_to_cabin + let (cop_ideal, _te_ref) = if pwr_thrml_hvac_to_res + pwr_thrml_hvac_to_cabin > si::Power::ZERO { // heating mode @@ -170,9 +174,13 @@ impl HVACSystemForLumpedCabinAndRES { if te_delta_vs_amb.abs() < 5.0 * uc::KELVIN_INT { // cabin is cooler than ambient + threshold // TODO: make this `5.0` not hardcoded - (te_ref / (5.0 * uc::KELVIN), te_ref) + let cop_ideal = te_ref / (5.0 * uc::KELVIN); + ensure!(cop_ideal > si::Ratio::ZERO, format_dbg!(cop_ideal)); + (cop_ideal, te_ref) } else { - (te_ref / te_delta_vs_amb.abs(), te_ref) + let cop_ideal = te_ref / te_delta_vs_amb.abs(); + ensure!(cop_ideal > si::Ratio::ZERO, format_dbg!(cop_ideal)); + (cop_ideal, te_ref) } } else if pwr_thrml_hvac_to_res + pwr_thrml_hvac_to_cabin < si::Power::ZERO { // cooling mode @@ -218,20 +226,18 @@ impl HVACSystemForLumpedCabinAndRES { if te_delta_vs_amb.abs() < 5.0 * uc::KELVIN_INT { // cooling-dominating component is cooler than ambient + threshold // TODO: make this `5.0` not hardcoded - (te_ref / (5.0 * uc::KELVIN), te_ref) + let cop_ideal = te_ref / (5.0 * uc::KELVIN); + ensure!(cop_ideal > si::Ratio::ZERO, format_dbg!(cop_ideal)); + (cop_ideal, te_ref) } else { - (te_ref / te_delta_vs_amb.abs(), te_ref) + let cop_ideal = te_ref / te_delta_vs_amb.abs(); + ensure!(cop_ideal > si::Ratio::ZERO, format_dbg!(cop_ideal)); + (cop_ideal, te_ref) } } else { (si::Ratio::ZERO, f64::NAN * uc::KELVIN) }; self.state.cop = cop_ideal * self.frac_of_ideal_cop; - ensure!( - self.state.cop >= 0.0 * uc::R, - "{}\n{}", - format_dbg!(cop_ideal), - format_dbg!(te_ref) - ); let mut pwr_thrml_fc_to_cabin = si::Power::ZERO; self.state.pwr_aux_for_hvac = if pwr_thrml_hvac_to_cabin > si::Power::ZERO { @@ -345,6 +351,12 @@ impl HVACSystemForLumpedCabinAndRES { (self.state.pwr_p_cab + self.state.pwr_i_cab + self.state.pwr_d_cab) .max(-self.pwr_thrml_max); + ensure!( + pwr_thrml_hvac_to_cab < si::Power::ZERO, + "{}\nHVAC should be cooling cabin", + format_dbg!(pwr_thrml_hvac_to_cab) + ); + if (-pwr_thrml_hvac_to_cab / self.state.cop) > self.pwr_aux_for_hvac_max { self.state.pwr_aux_for_hvac = self.pwr_aux_for_hvac_max; // correct if limit is exceeded @@ -354,8 +366,10 @@ impl HVACSystemForLumpedCabinAndRES { } ensure!( pwr_thrml_hvac_to_cab < si::Power::ZERO, - "{}\nHVAC should be cooling cabin", - format_dbg!(pwr_thrml_hvac_to_cab) + "HVAC should be cooling cabin\n{}\n{}\n{}", + format_dbg!(pwr_thrml_hvac_to_cab), + format_dbg!(self.state.pwr_aux_for_hvac), + format_dbg!(self.state.cop) ); pwr_thrml_hvac_to_cab } else { diff --git a/python/fastsim/pymoo_api.py b/python/fastsim/pymoo_api.py index 05941ec6..897f883a 100644 --- a/python/fastsim/pymoo_api.py +++ b/python/fastsim/pymoo_api.py @@ -207,10 +207,11 @@ def get_errors( t1 = time.perf_counter() sd_dict = sd.to_pydict() walk_success = True - except RuntimeError as _err: + except RuntimeError as err: t1 = time.perf_counter() sd_dict = sd.to_pydict() walk_success = True + print(err) if len(sd_dict['veh']['history']['time_seconds']) < np.floor(len(df_exp) / 2): walk_success = False