Source code for prereise.gather.demanddata.transportation_electrification.immediate
import numpy as np
import pandas as pd
from prereise.gather.demanddata.transportation_electrification import const, data_helper
allowed_locations_by_strategy = {
1: {1}, # home only
2: set(range(1, 13)), # home and go to work related (1-12, inclusive)
# We don't need to set the strategy for 3 (everywhere), it's handled elsewhere
4: {1, 21}, # home and school only
5: {1, 11, 12, 21}, # home and work and school
}
[docs]def calculate_charging(
trips, charging_power, battery_capacity, kwhmi, charging_efficiency
):
"""Parse travel patterns to estimate charging and state-of-charge after each trip.
:param pandas.DataFrame trips: trip data.
:param int/float charging_power: charging power (kW).
:param int/float battery_capacity: battery capacity (kWh).
:param int/float kwhmi: vehicle electricity consumption (kWh/ mile).
:param int/float charging_efficiency: from grid to battery efficiency.
"""
# Add trip_number entries
trips["trip_number"] = trips.groupby("vehicle_number").cumcount() + 1
grouped_trips = trips.groupby("trip_number")
for trip_num, group in grouped_trips:
if trip_num == 1:
# For the first trip, we assume that the vehicle starts with a full battery
trips.loc[group.index, "trip start battery charge"] = battery_capacity
else:
# For subsequent trips, the starting SoC depends on the previous trip
relevant_vehicles = group["vehicle_number"]
previous_group = grouped_trips.get_group(trip_num - 1)
start_soc = (
previous_group["trip end battery charge"]
+ previous_group["charging consumption"]
)
trips.loc[group.index, "trip start battery charge"] = start_soc.values[
previous_group["vehicle_number"].isin(relevant_vehicles)
]
# For all trips, update ending state of charge
trips.loc[group.index, "trip end battery charge"] = (
trips.loc[group.index, "trip start battery charge"]
- group["trip_miles"] * kwhmi * const.ER
)
# Calculate charging duration/energy for the trips that can charge
trips.loc[group.index, "full_charge_time"] = (
battery_capacity - trips["trip end battery charge"]
) / (charging_power * charging_efficiency)
trips.loc[group.index, "charging time"] = trips["charging_allowed"] * (
trips[["full_charge_time", "dwell_time"]].apply(min, axis=1)
)
trips.loc[group.index, "charging consumption"] = (
trips["charging time"] * charging_power * charging_efficiency
)
[docs]def resample_daily_charging(trips, charging_power):
"""Translate start and end times and power to a 72-hour output array.
:param pandas.DataFrame trips: trip data with trip-end and charge-time columns.
:param int/float charging_power: charging power (kW).
:return: (*numpy.array*) -- hourly total charging power for the 72-hour span.
"""
fine_resolution = 7200
coarse_resolution = 72
ratio = int(fine_resolution / coarse_resolution)
# determine timing of charging
augmented_trips = trips.assign(
start_point=(ratio * trips["trip_end"]).map(round),
elapsed=(ratio * trips["charging time"]).map(round),
end_point=lambda x: x["start_point"] + x["elapsed"],
)
# Translate times to fine-resolution arrays
indiv_charging_profiles = np.zeros((len(trips), fine_resolution), dtype=bool)
for i, (trip_id, trip) in enumerate(augmented_trips.iterrows()):
indiv_charging_profiles[i, trip["start_point"] : trip["end_point"]] = True
# Sum fine-resolution arrays for each trip into one aggregate array
total_profile = indiv_charging_profiles.sum(axis=0) * charging_power
# Resample fine-resolution arrays into a coarse-resolution array
output_array = np.zeros(coarse_resolution)
for k in range(coarse_resolution):
if k == 0:
# First hour, normal sum
output_array[k] = sum(total_profile[:ratio]) / ratio
elif k == coarse_resolution - 1:
# Last hour, normal sum
output_array[k] = sum(total_profile[(-1 * ratio) :]) / ratio
else:
# Every other hour: sum from the half hour before to the half hour after
output_array[k] = (
sum(total_profile[int((k - 0.5) * ratio) : int((k + 0.5) * ratio)])
/ 100
)
return output_array
[docs]def immediate_charging(
census_region,
model_year,
veh_range,
power,
location_strategy,
veh_type,
filepath,
trip_strategy=1,
input_day=None,
):
"""Immediate charging function
:param int census_region: any of the 9 census regions defined by US census bureau.
:param int model_year: year that is being modelled/projected to, 2017, 2030, 2040, 2050.
:param int veh_range: 100, 200, or 300, represents how far vehicle can travel on single charge.
:param int power: charger power, EVSE kW.
:param int location_strategy: where the vehicle can charge-1, 2, 3, 4, or 5;
1-home only, 2-home and work related, 3-anywhere if possibile,
4-home and school only, 5-home and work and school.
:param str veh_type: determine which category (LDV or LDT) to produce charging profiles for
:param str filepath: the path to the nhts mat file.
:param int trip_strategy: determine to charge after any trip (1) or only after the last trip (2)
:return: (*numpy.ndarray*) -- charging profiles.
"""
if veh_type.lower() == "ldv":
trips = data_helper.remove_ldt(data_helper.load_data(census_region, filepath))
elif veh_type.lower() == "ldt":
trips = data_helper.remove_ldv(data_helper.load_data(census_region, filepath))
elif veh_type.lower() == "mdv":
trips = data_helper.load_hdv_data("mhdv", filepath)
elif veh_type.lower() == "hdv":
trips = data_helper.load_hdv_data("hhdv", filepath)
# filter for cyclical trips
filtered_census_data = pd.DataFrame(columns=const.nhts_census_column_names)
i = 0
while i < len(trips):
total_trips = int(trips.iloc[i, trips.columns.get_loc("total_trips")])
# copy one vehicle information to the block
individual = trips.iloc[i : i + total_trips].copy()
if individual["why_from"].iloc[0] == individual["dwell_location"].iloc[-1]:
filtered_census_data = pd.concat(
[filtered_census_data, individual], ignore_index=True
)
i += total_trips
trips = filtered_census_data
#####
# Constants
kwhmi = data_helper.get_kwhmi(model_year, veh_type, veh_range)
battery_capacity = kwhmi * veh_range
input_day = data_helper.get_input_day(data_helper.get_model_year_dti(model_year))
# updates the weekend and weekday values in the nhts data
trips = data_helper.update_if_weekend(trips)
if power > 19.2:
charging_efficiency = 0.95
else:
charging_efficiency = 0.9
# add new columns to newdata to store data that is not in NHTS data
new_columns = [
"trip start battery charge",
"trip end battery charge",
"charging power",
"charging time",
"charging consumption",
"BEV could be used",
"trip_number",
]
trips = trips.reindex(list(trips.columns) + new_columns, axis=1, fill_value=0)
# Add flag for whether the total mileage is within the vehicle's range
trips["BEV could be used"] = (
trips["total vehicle miles traveled"] < veh_range * const.ER
)
# Add booleans for whether the location allows charging
if location_strategy == 3:
trips["location_allowed"] = True
else:
allowed = allowed_locations_by_strategy[location_strategy]
trips["location_allowed"] = trips["dwell_location"].isin(allowed)
# Add booleans for whether the trip_number (compared to total trips) allows charging
if trip_strategy == 1:
trips["trip_allowed"] = True
elif trip_strategy == 2:
trips["trip_allowed"] = trips["trip_number"] == trips["total_trips"]
# Add booleans for whether the dell time is long enough to allow charging
trips["dwell_allowed"] = trips["dwell_time"] > 0.2
# Add boolean for whether this trip allows charging
allowed_cols = [
"location_allowed",
"trip_allowed",
"dwell_allowed",
]
trips["charging_allowed"] = trips[allowed_cols].apply(all, axis=1)
trips["dwell_charging"] = (
trips["charging_allowed"] * trips["dwell_time"] * power * charging_efficiency
)
grouped_trips = trips.groupby("vehicle_number")
for vehicle_num, group in grouped_trips:
trips.loc[group.index, "max_charging"] = trips.loc[
group.index, "dwell_charging"
].sum()
trips.loc[group.index, "required_charging"] = (
trips.loc[group.index, "trip_miles"].sum() * kwhmi
)
# Filter for whenever available charging is insufficient to meet required charging
trips = trips.loc[(trips["required_charging"] <= trips["max_charging"])]
# Filter by vehicle range
trips = trips.loc[trips["total vehicle miles traveled"] < veh_range * const.ER]
# Evaluate weekend vs. weekday for each trip
data_day = data_helper.get_data_day(trips)
weekday_trips = trips.loc[data_day == 2].copy()
weekend_trips = trips.loc[data_day == 1].copy()
# Calculate the charge times and SOC for each trip, then resample resolution
calculate_charging(
weekday_trips, power, battery_capacity, kwhmi, charging_efficiency
)
calculate_charging(
weekend_trips, power, battery_capacity, kwhmi, charging_efficiency
)
daily_resampled_profiles = {
"weekday": resample_daily_charging(weekday_trips, power),
"weekend": resample_daily_charging(weekend_trips, power),
}
model_year_profile = np.zeros(24 * len(input_day))
flag_translation = {1: "weekend", 2: "weekday"}
for i, weekday_flag in enumerate(input_day):
daily_profile = daily_resampled_profiles[flag_translation[weekday_flag]]
# print(f"day: {i}")
# print(f"daily sum: {np.sum(daily_profile)}")
# create wrap-around indexing function
trip_window_indices = np.arange(i * 24, i * 24 + 72) % len(model_year_profile)
# MW
model_year_profile[trip_window_indices] += daily_profile
# Normalize the output so that it sums to 1
summed_profile = model_year_profile / model_year_profile.sum()
output_load_sum_list = [
np.sum(daily_resampled_profiles["weekend"]),
np.sum(daily_resampled_profiles["weekday"]),
]
return summed_profile, output_load_sum_list, trips
[docs]def adjust_bev(
hourly_profile,
adjustment_values,
model_year,
veh_type,
veh_range,
bev_vmt,
charging_efficiency,
):
"""Adjusts the charging profiles by applying weighting factors based on
seasonal/monthly values
:param numpy.ndarray hourly_profile: normalized charging profiles
:param pandas.DataFrame adjustment_values: weighting factors for each
day of the year loaded from month_info_nhts.mat.
:param int model_year: year that is being modelled/projected to, 2017, 2030, 2040, 2050.
:param str veh_type: determine which category (MDV or HDV) to produce charging profiles for
:param int veh_range: 100, 200, or 300, represents how far vehicle can travel on single charge.
:param int/float bev_vmt: BEV VMT value / scaling factor loaded from Regional_scaling_factors.csv
:param float charging_efficiency: from grid to battery efficiency.
:return: (*numpy.ndarray*) -- final adjusted charging profiles.
"""
kwhmi = data_helper.get_kwhmi(model_year, veh_type, veh_range)
# weekday/weekend, monthly urban and rural moves scaling
adjusted_load = apply_daily_adjustments(
hourly_profile,
adjustment_values,
)
# simulation year urban and rural scaling specific to region
simulation_hourly_profile = apply_annual_scaling(
adjusted_load,
bev_vmt,
charging_efficiency,
kwhmi,
)
return simulation_hourly_profile
[docs]def apply_daily_adjustments(
hourly_profile,
adjustment_values,
num_days_per_year=365,
num_segments_per_day=24,
):
"""Adjusts the charging profiles by applying weighting factors based on
annual vehicle miles traveled (VMT) for battery electric vehicles in a specific geographic region
:param numpy.ndarray hourly_profile: normalized charging profiles
:param pandas.DataFrame adjustment_values: weighting factors for each
day of the year loaded from month_info_nhts.mat.
:param int num_days_per_year: optional year parameter to facilite easier testing
:param int num_segments_per_day: optional specification of hours per day
:return: (*numpy.ndarray*) -- adjusted charging profile
"""
# weekday/weekend, monthly urban and rural moves scaling
adj_vals = adjustment_values.transpose().to_numpy()
profiles = hourly_profile.reshape(
(num_segments_per_day, num_days_per_year), order="F"
)
pr = profiles / np.sum(profiles, axis=0)
adjusted = np.multiply(pr, adj_vals)
adjusted_load = adjusted.T.flatten()
return adjusted_load
[docs]def apply_annual_scaling(
hourly_profile,
bev_vmt,
charging_efficiency,
kwhmi,
):
"""Adjusts the charging profiles by applying weighting factors based on
seasonal/monthly values
:param numpy.ndarray hourly_profile: hourly charging profile
:param int/float bev_vmt: BEV VMT value / scaling factor loaded from Regional_scaling_factors.csv
:param float charging_efficiency: from grid to battery efficiency.
:param int kwhmi: fuel efficiency, should vary based on vehicle type and model_year.
:return: (*numpy.ndarray*) -- adjusted charging profile
"""
bev_annual_load = bev_vmt * kwhmi / charging_efficiency
simulation_hourly_profile = bev_annual_load * hourly_profile
return simulation_hourly_profile