Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature EV controlled charging custom constraints #356

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
42f1beb
Process data for custom constraints
adrienmellot Apr 10, 2024
9623afe
edit csv file format
adrienmellot Apr 10, 2024
e451c4a
Process data for custom constraints
adrienmellot Apr 10, 2024
ba65333
edit csv file format
adrienmellot Apr 10, 2024
ff91ce8
Merge branch 'feature-ev-controlled-charging-constraints' of https://…
adrienmellot Apr 11, 2024
6cb137c
Extract EV chargeable capacities
adrienmellot Apr 11, 2024
2cd9b9f
Have max charging potential exported to yaml
adrienmellot Apr 11, 2024
4c4621f
minor yaml update
adrienmellot Apr 11, 2024
0e1cebc
Fix ramp data download
adrienmellot Apr 12, 2024
ecd75fe
Update custom constaint from file by adding _time_varying
adrienmellot Apr 12, 2024
cffaedb
fix force_resource on heat sector
adrienmellot Apr 12, 2024
db9ed2a
Create run.py to include custom constraints
adrienmellot Apr 12, 2024
f066a5f
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Apr 12, 2024
f153a0d
delete unnecessary script which was merged in another
adrienmellot Apr 12, 2024
17225e9
Merge branch 'feature-ev-controlled-charging-constraints' of https://…
adrienmellot Apr 12, 2024
b13103b
Respect ruff formatting
adrienmellot Apr 12, 2024
aedecc8
Rename monthly demand parameters
adrienmellot May 14, 2024
f398760
Add dataset wildcards
adrienmellot May 14, 2024
d2e3dd0
Update road_transport_controlled_charging.py
adrienmellot May 14, 2024
d9fa0e2
Merge branch 'develop' into feature-ev-controlled-charging-constraints
adrienmellot May 14, 2024
014085f
Small fixes post merge
adrienmellot May 14, 2024
0832b0e
Fix compatibility with ehighways
adrienmellot May 14, 2024
5d5b4eb
style: add return types of functions
adrienmellot May 14, 2024
0b05d53
rename 'data' to 'profiles' for RAMP
adrienmellot May 14, 2024
bb6e6d9
Merge branch 'develop' into feature-ev-controlled-charging-constraints
adrienmellot May 17, 2024
92f0c7d
delete run.py following #385
adrienmellot May 30, 2024
905e331
uncontrolled charging share to 1 by default
adrienmellot May 31, 2024
1a0db45
Edits based on review
adrienmellot May 31, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -45,7 +45,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 @@ -188,6 +188,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:
brynpickering marked this conversation as resolved.
Show resolved Hide resolved
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:
brynpickering marked this conversation as resolved.
Show resolved Hide resolved
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
Loading