Skip to content

Commit

Permalink
troubleshooting pymoo stuff
Browse files Browse the repository at this point in the history
  • Loading branch information
calbaker committed Jan 28, 2025
1 parent ae1c607 commit db272d4
Show file tree
Hide file tree
Showing 2 changed files with 160 additions and 46 deletions.
192 changes: 148 additions & 44 deletions cal_and_val/thermal/cal_hev.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import pandas as pd # noqa: F401
import polars as pl # noqa: F401
from typing import List, Dict
from pymoo.core.problem import StarmapParallelization

import fastsim as fsim

Expand All @@ -40,50 +41,88 @@
# 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/2021 Hyundai Sonata Hybrid/Extended Datasets"
cyc_folder_path = Path(__file__).parent / "dyno_test_data/2021 Hyundai Sonata Hybrid/Extended Datasets"
assert cyc_folder_path.exists()

# See 2021_Hyundai_Sonata_Hybrid_TestSummary_2022-03-01_D3.xlsx for cycle-level data
cyc_files: List[str] = [
# TODO: try to find 3 hot cycles, 3 room temp cycles, and 3 cold cycles.
# The hot and cold cycles must have HVAC active!
# - wide range of initial and ambient temperatures
# - good signal quality -- somewhat subjective

# HWY x2, hot (M155), HVAC active (B155)
# TODO: connect drive cycle solar load to cabin
# TODO: check for solar load (should be around 1 kW / m^2)
"62202004 Test Data.txt",

# US06 x2, hot, HVAC active
# TODO: check for solar load (should be around 1 kW / m^2)
"62202005 Test Data.txt",

# UDDS x1, room temperature ambient
"62201013 Test Data.txt",

# HWY x2, room temperature ambient
"62201014 Test Data.txt",

# UDDSx2, 4 bag (FTP), cold start, in COLD (20°F) test cell, HVAC-AUTO-72°F, ECO drive mode
"62202013 Test Data.txt",

# UDDS, 2 bag, warm start, in COLD (20°F) test cell, HVAC-AUTO-72°F, ECO drive mode
"62202014 Test Data.txt",

# US06x2, 4 (split) bag, warm start, in COLD (20°F) test cell, HVAC-AUTO-72°F, ECO drive mode
"62202016 Test Data.txt",

# TODO: check for seat heater usage in cold cycles and account for that in model!
]
assert len(cyc_files) > 0
cyc_files = [cyc_folder_path / cyc_file for cyc_file in cyc_files]
cyc_files: List[Path] = [cyc_folder_path / cyc_file for cyc_file in cyc_files]

# TODO: use random or manual selection to retain ~70% of cycles for calibration,
# use random or manual selection to retain ~70% of cycles for calibration,
# and reserve the remaining for validation
cyc_files_for_cal: List[Path] = [
# TOOD: populate this somehow -- e.g. random split of `cyc_files`
cyc_files_for_cal: List[str] = [
"62202004 Test Data.txt",
# "62202005 Test Data.txt",
"62201013 Test Data.txt",
"62201014 Test Data.txt",
"62202013 Test Data.txt",
# "62202014 Test Data.txt",
"62202016 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

def df_to_cyc(df: pd.DataFrame) -> fsim.Cycle:
cyc_dict_raw = df.to_dict()
# filter out "before" time
cyc_dict_raw = cyc_dict_raw[cyc_dict_raw["Time[s]_RawFacilities"] >= 0.0]
df = df[df["Time[s]_RawFacilities"] >= 0.0]
cyc_dict = {
"time_seconds": df["Time[s]_RawFacilities"],
"speed_meters_per_second": df["Dyno_Spd[mph]"] * mps_per_mph,
"temp_amb_air_kelvin": df["Cell_Temp[C]"] + celsius_to_kelvin_offset,
"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)


def veh_init(veh: fsim.Vehicle, cyc_file_stem: str) -> fsim.Vehicle:
veh_dict = veh.to_pydict()
# initialize SOC
veh_dict['pt_type']['HybridElectricVehicle']['res']['state']['soc'] = \
dfs_for_cal[cyc_file_stem]["HVBatt_SOC_high_precision_PCAN__per"][0]
# initialize cabin temp
veh_dict['cabin']['LumpedCabin']['state']['temperature_kelvin'] = \
dfs_for_cal[cyc_file_stem]["Cabin_Temp[C]"][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
veh_dict['pt_type']['HybridElectricVehicle']['res']['thrml']['RESLumpedThermal']['state']['temperature_kelvin'] = \
dfs_for_cal[cyc_file_stem]["Cabin_Temp[C]"][0] + celsius_to_kelvin_offset
# initialize engine temperature
veh_dict['veh']['pt_type']['HybridElectricVehicle']['fc']['thrml']['FuelConverterThermal']['state']['temperature_kelvin'] = \
dfs_for_cal[cyc_file_stem]["engine_coolant_temp_PCAN__C"][0] + celsius_to_kelvin_offset
return fsim.Vehicle.from_pydict(veh_dict)


dfs_for_cal: Dict[str, pd.DataFrame] = {
# `delimiter="\t"` should work for tab separated variables
Expand All @@ -105,19 +144,12 @@ def df_to_cyc(df: pd.DataFrame) -> fsim.Cycle:
for (cyc_file_stem, cyc) in cycs_for_cal.items():
cyc_file_stem: str
cyc: fsim.Cycle
veh_dict = veh.to_pydict()
# initialize SOC
veh_dict['veh']['pt_type']['HybridElectricVehicle']['res']['state']['soc'] = dfs_for_cal[cyc_file_stem]["HVBatt_SOC_high_precision_PCAN__per"][0]
# TODO: pick up work here!
# TODO: clone veh and set up initial conditions for:
# - SOC
# - cabin temp
# - battery temp if available, else use cabin temp
# - engine temp for HEV
# NOTE: maybe change `save_interval` to 5
veh = veh_init(veh, cyc_file_stem)
sds_for_cal[cyc_file_stem] = fsim.SimDrive(veh, cyc).to_pydict()

cyc_files_for_val: List[Path] = list(set(cyc_files) - set(cyc_files_for_cal))
assert len(cyc_files_for_val) > 0

dfs_for_val: Dict[str, pd.DataFrame] = {
# `delimiter="\t"` should work for tab separated variables
Expand All @@ -136,12 +168,7 @@ def df_to_cyc(df: pd.DataFrame) -> fsim.Cycle:
for (cyc_file_stem, cyc) in cycs_for_val.items():
cyc_file_stem: str
cyc: fsim.Cycle
# TODO: clone veh and set up initial conditions for:
# - SOC
# - cabin temp
# - battery temp if available, else use cabin temp
# - engine temp for HEV
# NOTE: maybe change `save_interval` to 5
veh = veh_init(veh, cyc_file_stem)
sds_for_val[cyc_file_stem] = fsim.SimDrive(veh, cyc).to_pydict()

# Setup model objectives
Expand All @@ -153,8 +180,6 @@ def new_em_eff_max(sd_dict, new_eff_peak):
em = fsim.ElectricMachine.from_pydict(sd_dict['veh']['pt_type']['HybridElectricVehicle']['em'])
em.set_eff_peak(new_eff_peak)
sd_dict['veh']['pt_type']['HybridElectricVehicle']['em'] = em.to_pydict()
# TODO: check that `sd_dict` is mutably modified outside the scope of this function, e.g. with a debugger
# maybe do this by individually perturbing all the objectives and checking for response

def new_em_eff_range(sd_dict, new_eff_range):
"""
Expand All @@ -163,7 +188,6 @@ def new_em_eff_range(sd_dict, new_eff_range):
em = fsim.ElectricMachine.from_pydict(sd_dict['veh']['pt_type']['HybridElectricVehicle']['em'])
em.set_eff_range(new_eff_range)
sd_dict['veh']['pt_type']['HybridElectricVehicle']['em'] = em.to_pydict()
# TODO: check that `sd_dict` is mutably modified outside the scope of this function, e.g. with a debugger

def new_fc_eff_max(sd_dict, new_eff_peak):
"""
Expand All @@ -172,7 +196,6 @@ def new_fc_eff_max(sd_dict, new_eff_peak):
fc = fsim.FuelConverter.from_pydict(sd_dict['veh']['pt_type']['HybridElectricVehicle']['fc'])
fc.set_eff_peak(new_eff_peak)
sd_dict['veh']['pt_type']['HybridElectricVehicle']['fc'] = fc.to_pydict()
# TODO: check that `sd_dict` is mutably modified outside the scope of this function, e.g. with a debugger

def new_fc_eff_range(sd_dict, new_eff_range):
"""
Expand All @@ -181,8 +204,6 @@ def new_fc_eff_range(sd_dict, new_eff_range):
fc = fsim.FuelConverter.from_pydict(sd_dict['veh']['pt_type']['HybridElectricVehicle']['fc'])
fc.set_eff_range(new_eff_range)
sd_dict['veh']['pt_type']['HybridElectricVehicle']['fc'] = fc.to_pydict()
# TODO: check that `sd_dict` is mutably modified outside the scope of this function, e.g. with a debugger


## Model Objectives
cal_mod_obj = fsim.pymoo_api.ModelObjectives(
Expand All @@ -191,7 +212,7 @@ def new_fc_eff_range(sd_dict, new_eff_range):
obj_fns=(
(
lambda sd_dict: np.array(sd_dict['veh']['pt_type']['HybridElectricVehicle']['res']['history']['soc']),
lambda df: df['TODO: find signal for test data soc']
lambda df: df['HVBatt_SOC_high_precision_PCAN__per']
),
# TODO: add objectives for:
# - engine fuel usage
Expand All @@ -209,10 +230,6 @@ def new_fc_eff_range(sd_dict, new_eff_range):
# - 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
# - thermal mass
# - convection to ambient
# - convection to cabin
# - cabin thermal
# - thermal mass
# - length
Expand All @@ -222,6 +239,10 @@ def new_fc_eff_range(sd_dict, new_eff_range):
# - thermal mass
# - convection to ambient when stopped
# - diameter
# - 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=(
Expand All @@ -233,7 +254,90 @@ def new_fc_eff_range(sd_dict, new_eff_range):

)

# Setup calibration problem
cal_prob = fsim.pymoo_api.CalibrationProblem(
mod_obj=cal_mod_obj,
# verify that model responds to input parameter changes by individually perturbing parameters
baseline_errors = cal_mod_obj.get_errors(
cal_mod_obj.update_params([0.90, 0.3, 0.4])
)
param0_perturb = cal_mod_obj.get_errors(
cal_mod_obj.update_params([0.90 + 0.5, 0.3, 0.4])
)
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, 0.4])
)
assert list(param1_perturb.values()) != list(baseline_errors.values())
param2_perturb = cal_mod_obj.get_errors(
cal_mod_obj.update_params([0.90, 0.3, 0.4 + 0.01])
)
assert list(param2_perturb.values()) != list(baseline_errors.values())

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,
)
args = parser.parse_args()

n_processes = args.processes
n_max_gen = args.n_max_gen
# 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(
# size of each population
pop_size=pop_size,
# LatinHyperCube sampling seems to be more effective than the default
# random sampling
sampling=fsim.calibration.LHS(),
)
termination = fsim.calibration.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
period=5,
# parameter variation tolerance
xtol=args.xtol,
# objective variation tolerance
ftol=args.ftol
)

if n_processes == 1:
print("Running serial evaluation.")
# series evaluation
# Setup calibration problem
cal_prob = fsim.pymoo_api.CalibrationProblem(
mod_obj=cal_mod_obj,
)

res, res_df = fsim.pymoo_api.run_minimize(
problem=cal_prob,
algorithm=algorithm,
termination=termination,
save_path=save_path,
)
else:
print(f"Running parallel evaluation with n_processes: {n_processes}.")
assert n_processes > 1
# parallel evaluation
import multiprocessing

with multiprocessing.Pool(n_processes) as pool:
problem = fsim.calibration.CalibrationProblem(
mod_obj=cal_mod_obj,
elementwise_runner=StarmapParallelization(pool.starmap),
)
res, res_df = fsim.pymoo_api.run_minimize(
problem=problem,
algorithm=algorithm,
termination=termination,
save_path=save_path,
)

14 changes: 12 additions & 2 deletions fastsim-core/src/drive_cycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,18 @@ pub struct Cycle {
pub speed: Vec<si::Velocity>,
// TODO: consider trapezoidal integration scheme
/// calculated prescribed distance based on RHS integral of time and speed
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub dist: Vec<si::Length>,
/// road grade (expressed as a decimal, not percent)
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub grade: Vec<si::Ratio>,
// TODO: consider trapezoidal integration scheme
// TODO: @mokeefe, please check out how elevation is handled
/// calculated prescribed elevation based on RHS integral distance and grade
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub elev: Vec<si::Length>,
/// road charging/discharing capacity
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub pwr_max_chrg: Vec<si::Power>,
/// ambient air temperature w.r.t. to time (rather than spatial position)
#[serde(default, skip_serializing_if = "Vec::is_empty")]
Expand All @@ -52,8 +56,10 @@ pub struct Cycle {
pub pwr_solar_load: Vec<si::Power>,
// TODO: add provision for optional time-varying aux load
/// grade interpolator
#[serde(default, skip_serializing_if = "Option::is_none")]
pub grade_interp: Option<Interpolator>,
/// elevation interpolator
#[serde(default, skip_serializing_if = "Option::is_none")]
pub elev_interp: Option<Interpolator>,
}

Expand Down Expand Up @@ -87,6 +93,10 @@ impl Init for Cycle {
.collect()
};

// populate grade if not provided
if self.grade.is_empty() {
self.grade = vec![si::Ratio::ZERO; self.len().with_context(|| format_dbg!())?]
};
// calculate elevation from RHS integral of grade and distance
self.init_elev = self.init_elev.or_else(|| Some(*ELEV_DEFAULT));
self.elev = self
Expand Down Expand Up @@ -223,9 +233,9 @@ impl SerdeAPI for Cycle {
let mut cyc = Self::default();
let mut rdr = csv::Reader::from_reader(rdr);
for result in rdr.deserialize() {
cyc.push(result?)?;
cyc.push(result.with_context(|| format_dbg!())?)
.with_context(|| format_dbg!())?;
}

cyc
}
#[cfg(feature = "json")]
Expand Down

0 comments on commit db272d4

Please sign in to comment.