Skip to content

Commit

Permalink
Merge pull request #356 from adrienmellot/feature-ev-controlled-charg…
Browse files Browse the repository at this point in the history
…ing-constraints

Feature EV controlled charging custom constraints
  • Loading branch information
brynpickering committed Jun 6, 2024
2 parents 7f4e50a + 1a0db45 commit f929d09
Show file tree
Hide file tree
Showing 9 changed files with 328 additions and 24 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

* **ADD** fully-electrified heat demand (#284).

* **ADD** fully-electrified road transportation (#270, #271, #358). A parameter allows to define the share of uncontrolled (timeseries) vs controlled charging (optimised) by the solver (#338).
* **ADD** fully-electrified road transportation (#270, #271, #358). A parameter allows to define the share of uncontrolled (timeseries) vs controlled charging (optimised) by the solver (#338). Data for controlled charging constraints is readily available (#356), but corresponding constraints are not yet implemented (#385).

* **ADD** nuclear power plant technology with capacity limits. Capacity limits can be equal to today or be bound by a minimum and maximum capacity to represent an available range in future. In either case, capacities are allocated at a subnational resolution based on linear scaling from current capacity geolocations, using the JRC power plant database (#78).

Expand Down
6 changes: 5 additions & 1 deletion Snakefile
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ wildcard_constraints:

ruleorder: area_to_capacity_limits > hydro_capacities > biofuels > nuclear_regional_capacity > dummy_tech_locations_template
ruleorder: bio_techs_and_locations_template > techs_and_locations_template
ruleorder: create_controlled_road_transport_annual_demand > dummy_tech_locations_template
ruleorder: create_controlled_road_transport_annual_demand_and_installed_capacities > dummy_tech_locations_template

ALL_CF_TECHNOLOGIES = [
"wind-onshore", "wind-offshore", "open-field-pv",
Expand Down Expand Up @@ -178,6 +178,10 @@ rule model_template:
"build/models/{resolution}/timeseries/demand/uncontrolled-road-transport-historic-electrification.csv",
"build/models/{resolution}/timeseries/demand/electrified-heat-demand.csv",
"build/models/{resolution}/timeseries/demand/heat-demand-historic-electrification.csv",
"build/models/{resolution}/timeseries/demand/demand-shape-min-ev.csv",
"build/models/{resolution}/timeseries/demand/demand-shape-max-ev.csv",
"build/models/{resolution}/timeseries/demand/demand-shape-equals-ev.csv",
"build/models/{resolution}/timeseries/demand/plugin-profiles-ev.csv",
),
optional_input_files = lambda wildcards: expand(
f"build/models/{wildcards.resolution}/{{input_file}}",
Expand Down
15 changes: 13 additions & 2 deletions config/default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ data-sources:
swiss-end-use: https://www.bfe.admin.ch/bfe/en/home/versorgung/statistik-und-geodaten/energiestatistiken/energieverbrauch-nach-verwendungszweck.exturl.html/aHR0cHM6Ly9wdWJkYi5iZmUuYWRtaW4uY2gvZGUvcHVibGljYX/Rpb24vZG93bmxvYWQvOTg1NA==.html
swiss-energy-balance: https://www.bfe.admin.ch/bfe/en/home/versorgung/statistik-und-geodaten/energiestatistiken/gesamtenergiestatistik.exturl.html/aHR0cHM6Ly9wdWJkYi5iZmUuYWRtaW4uY2gvZGUvcHVibGljYX/Rpb24vZG93bmxvYWQvNzUxOQ==.html
swiss-industry-energy-balance: https://www.bfe.admin.ch/bfe/en/home/versorgung/statistik-und-geodaten/energiestatistiken/teilstatistiken.exturl.html/aHR0cHM6Ly9wdWJkYi5iZmUuYWRtaW4uY2gvZGUvcHVibGljYX/Rpb24vZG93bmxvYWQvODc4OA==.html
controlled-ev-profiles: https://zenodo.org/record/6579421/files/ramp-ev-consumption-profiles.csv.gz?download=1
controlled-ev-profiles: https://zenodo.org/record/6579421/files/ramp-ev-{dataset}.csv.gz?download=1
uncontrolled-ev-profiles: https://sandbox.zenodo.org/records/45530/files/uncontrolled-charging-profiles.csv.gz?download=1 # TODO: convert into Zenodo repository
gridded-temperature-data: https://zenodo.org/records/6557643/files/temperature.nc?download=1
gridded-10m-windspeed-data: https://zenodo.org/records/6557643/files/wind10m.nc?download=1
Expand Down Expand Up @@ -55,6 +55,7 @@ scaling-factors: # values are tuned for models with a few hours resolution and o
power: 0.00001 # from MW(h) to 100 GW(h)
area: 0.0001 # from km2 to 10,000 km2
monetary: 0.000000001 # from EUR to 1 billion EUR
transport: 0.01 # from Mio km to 100 Mio km
capacity-factors:
min: 0.001
max: 10 # 0.001 -> 10 leads to a numerical range of 1e5 (hourly resolution)
Expand Down Expand Up @@ -167,7 +168,17 @@ parameters:
coaches-and-buses: Motor coaches, buses and trolley buses
passenger-cars: Passenger cars
motorcycles: Powered 2-wheelers
uncontrolled-ev-charging-share: 0.5
ev-battery-sizes:
heavy-duty-vehicles: 0.2 # average from [EUCAR_2019]
light-duty-vehicles: 0.1 # own assumption based on passenger cars from [EUCAR_2019]
motorcycles: 0.01 # own assumption
coaches-and-buses: 0.2 # own assumption based on HDVs from [EUCAR_2019]
passenger-cars: 0.08 # average from [EUCAR_2019]
uncontrolled-ev-charging-share: 1
monthly-demand-bound-fraction:
min: 0.9
max: 1.1
equals: 1
entsoe-tyndp:
scenario: National Trends
grid: Reference
Expand Down
39 changes: 38 additions & 1 deletion config/schema.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ properties:
controlled-ev-profiles:
type: string
pattern: ^(https?|http?):\/\/.+
description: Web address of electric vehicle data.
description: Web address of electric vehicle data for controlled charging.
uncontrolled-ev-profiles:
type: string
pattern: ^(https?|http?):\/\/.+
Expand Down Expand Up @@ -198,6 +198,9 @@ properties:
monetary:
type: number
description: "Applied to all quantities of monetary cost (base: EUR)."
transport:
type: number
description: "Applied to all quantities of transport (base: Mio km)."
parameters:
type: object
description: Parameter values of the models.
Expand Down Expand Up @@ -368,9 +371,43 @@ properties:
motorcycles:
type: string
description: JRC-IDEES name of motorcycles.
ev-battery-sizes:
type: object
description: EV battery size per vehicle type in MWh.
additionalProperties: false
properties:
light-duty-vehicles:
type: number
description: Light duty vehicles / trucks.
heavy-duty-vehicles:
type: number
description: Heavy duty vehicles / trucks.
coaches-and-buses:
type: number
description: Coaches and buses.
passenger-cars:
type: number
description: Passenger cars.
motorcycles:
type: number
description: Motorcylces.
uncontrolled-ev-charging-share:
type: number
description: Share of uncontrolled charging.
monthly-demand-bound-fraction:
type: object
description: Charging flexibility parameter constraining the amount of monthly demand to be met.
additionalProperties: false
properties:
min:
type: number
description: Minimum monthly demand range.
max:
type: number
description: Maximum monthly demand range.
equals:
type: number
description: Equal monthly demand range.
entsoe-tyndp:
type: object
description: Parameters to define scenario choice for data accessed from the ENTSO-E ten-year network development plan 2020. For more information, see https://2020.entsos-tyndp-scenarios.eu/
Expand Down
30 changes: 27 additions & 3 deletions rules/transport.smk
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ rule download_transport_timeseries:
params:
url = config["data-sources"]["controlled-ev-profiles"]
conda: "../envs/shell.yaml"
output: protected("data/automatic/ramp-ev-consumption-profiles.csv.gz")
output:
protected("data/automatic/ramp-ev-{dataset}.csv.gz")
wildcard_constraints:
dataset = "consumption-profiles|plugin-profiles"
localrule: True
shell: "curl -sSLo {output} {params.url}"

Expand Down Expand Up @@ -38,16 +41,20 @@ rule annual_transport_demand:
road_distance_historically_electrified = "build/data/transport/annual-road-transport-distance-demand-historic-electrification.csv",
script: "../scripts/transport/annual_transport_demand.py"

rule create_controlled_road_transport_annual_demand:
message: "Create annual demand for controlled charging at {wildcards.resolution} resolution"
rule create_controlled_road_transport_annual_demand_and_installed_capacities:
message: "Create annual demand for controlled charging and corresponding charging potentials at {wildcards.resolution} resolution"
input:
annual_controlled_demand = "build/data/transport/annual-road-transport-distance-demand-controlled.csv",
ev_vehicle_number = "build/data/jrc-idees/transport/processed-road-vehicles.csv",
jrc_road_distance = "build/data/jrc-idees/transport/processed-road-distance.csv",
locations = "build/data/{resolution}/units.csv",
populations = "build/data/{resolution}/population.csv",
params:
first_year = config["scope"]["temporal"]["first-year"],
final_year = config["scope"]["temporal"]["final-year"],
power_scaling_factor = config["scaling-factors"]["power"],
transport_scaling_factor = config["scaling-factors"]["transport"],
battery_sizes = config["parameters"]["transport"]["ev-battery-sizes"],
conversion_factors = config["parameters"]["transport"]["road-transport-conversion-factors"],
countries = config["scope"]["spatial"]["countries"],
country_neighbour_dict = config["data-pre-processing"]["fill-missing-values"]["ramp"],
Expand All @@ -56,6 +63,23 @@ rule create_controlled_road_transport_annual_demand:
main = "build/data/{resolution}/demand/electrified-transport.csv",
script: "../scripts/transport/road_transport_controlled_charging.py"

rule create_controlled_ev_charging_parameters:
message: "Create timeseries parameters {wildcards.dataset_name} for controlled EV charging at {wildcards.resolution} resolution"
input:
ev_profiles = lambda wildcards: "data/automatic/ramp-ev-consumption-profiles.csv.gz" if "demand" in wildcards.dataset_name else f"data/automatic/ramp-ev-{wildcards.dataset_name}.csv.gz",
locations = "build/data/{resolution}/units.csv",
populations = "build/data/{resolution}/population.csv",
params:
demand_range = config["parameters"]["transport"]["monthly-demand-bound-fraction"],
first_year = config["scope"]["temporal"]["first-year"],
final_year = config["scope"]["temporal"]["final-year"],
country_neighbour_dict = config["data-pre-processing"]["fill-missing-values"]["ramp"],
countries = config["scope"]["spatial"]["countries"],
wildcard_constraints:
dataset_name = "demand-shape-equals|demand-shape-max|demand-shape-min|plugin-profiles"
conda: "../envs/default.yaml"
output: "build/models/{resolution}/timeseries/demand/{dataset_name}-ev.csv"
script: "../scripts/transport/road_transport_controlled_constraints.py"

rule create_uncontrolled_road_transport_timeseries:
message: "Create timeseries for road transport demand (uncontrolled charging)"
Expand Down
109 changes: 96 additions & 13 deletions scripts/transport/road_transport_controlled_charging.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def convert_annual_distance_to_electricity_demand(
final_year: int,
conversion_factors: dict[str, float],
country_codes: list[str],
):
) -> pd.DataFrame:
"""
Convert annual distance driven demand to electricity demand for
controlled charging accounting for conversion factors.
Expand All @@ -65,6 +65,60 @@ def convert_annual_distance_to_electricity_demand(
return -df_energy_demand


def extract_national_ev_charging_potentials(
path_to_ev_numbers: str,
transport_scaling_factor: float,
first_year: int,
final_year: int,
conversion_factors: dict[str, float],
battery_sizes: dict[str, float],
country_codes: list[str],
) -> pd.DataFrame:
# Extract number of EVs per vehicle type
df_ev_numbers = (
pd.read_csv(path_to_ev_numbers, index_col=[0, 1, 2, 3, 4])
.squeeze()
.droplevel(["vehicle_subtype", "section"])
)
assert not df_ev_numbers.isnull().values.any()
# Compute max. distance travelled per full battery for one EV [in Mio km / vehicle]
battery_size = pd.DataFrame.from_dict(
{
vehicle: battery_sizes[vehicle] / conversion_factors[vehicle]
for vehicle in battery_sizes
},
orient="index",
columns=["value"],
)

# Compute available chargeable distance per vehicle type [in transport scaling unit km]
df_ev_chargeable_distance = (
df_ev_numbers.align(battery_size, level="vehicle_type")[1]
.squeeze()
.mul(df_ev_numbers)
.groupby(level=["country_code", "year"])
.sum()
.loc[country_codes]
.unstack("year")
.mul(transport_scaling_factor)
)

if final_year > 2015:
# ASSUME 2015 data is used for all years after 2015
df_ev_chargeable_distance = df_ev_chargeable_distance.assign(**{
str(year): df_ev_chargeable_distance[2015]
for year in range(2016, final_year + 1)
})

df_ev_chargeable_distance.columns = df_ev_chargeable_distance.columns.astype(int)

return df_ev_chargeable_distance[range(first_year, final_year + 1)].T


def reshape_and_add_suffix(df, suffix):
return df.T.add_suffix(suffix)


if __name__ == "__main__":
resolution = snakemake.wildcards.resolution

Expand All @@ -83,8 +137,12 @@ def convert_annual_distance_to_electricity_demand(
.to_dict()
)
populations = pd.read_csv(snakemake.input.populations, index_col=0)
battery_sizes = snakemake.params.battery_sizes
transport_scaling_factor = snakemake.params.transport_scaling_factor
path_to_ev_numbers = snakemake.input.ev_vehicle_number

df = convert_annual_distance_to_electricity_demand(
# Convert annual distance driven demand to electricity demand for controlled charging
df_demand = convert_annual_distance_to_electricity_demand(
path_to_controlled_annual_demand,
power_scaling_factor,
first_year,
Expand All @@ -93,14 +151,39 @@ def convert_annual_distance_to_electricity_demand(
country_codes,
)

if resolution == "continental":
df = scale_to_continental_resolution(df)
elif resolution == "national":
df = scale_to_national_resolution(df)
elif resolution in ["regional", "ehighways"]:
df = scale_to_regional_resolution(
df, region_country_mapping=region_country_mapping, populations=populations
)
else:
raise ValueError("Input resolution is not recognised")
df.T.to_csv(path_to_output, index_label=["id"])
# Extract national EV charging potentials
df_charging_potentials = extract_national_ev_charging_potentials(
path_to_ev_numbers,
transport_scaling_factor,
first_year,
final_year,
conversion_factors,
battery_sizes,
country_codes,
)

# Add prefix for yaml template
parameters_evs = {
"_demand": df_demand,
"_charging": df_charging_potentials,
}

# Rescale to desired resolution and add suffix
dfs = []
for key, df in parameters_evs.items():
if resolution == "continental":
df = scale_to_continental_resolution(df)
elif resolution == "national":
df = scale_to_national_resolution(df)
elif resolution in ["regional", "ehighways"]:
df = scale_to_regional_resolution(
df,
region_country_mapping=region_country_mapping,
populations=populations,
)
else:
raise ValueError("Input resolution is not recognised")
dfs.append(reshape_and_add_suffix(df, key))

# Export to csv
pd.concat(dfs, axis=1).to_csv(path_to_output, index_label=["id"])
Loading

0 comments on commit f929d09

Please sign in to comment.