Skip to content

Commit

Permalink
Merge pull request #338 from adrienmellot/feature-uncontrolled-chargi…
Browse files Browse the repository at this point in the history
…ng-share

Feature uncontrolled vs controlled charging

Co-authored by: Francesco Sanvito sanvitofrancesco@gmail.com
  • Loading branch information
brynpickering authored Apr 11, 2024
2 parents ad0d60e + 60a3e42 commit 9a4de45
Show file tree
Hide file tree
Showing 9 changed files with 218 additions and 44 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
### Added (models)
* **ADD** fully-electrified heat demand (#284).

* **ADD** fully-electrified road transportation (#270), (#271).
* **ADD** fully-electrified road transportation (#270), (#271). A parameter allows to define the share of uncontrolled (timeseries) vs controlled charging (optimised) by the solver (PR #338).

* **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
7 changes: 4 additions & 3 deletions Snakefile
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,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

ALL_CF_TECHNOLOGIES = [
"wind-onshore", "wind-offshore", "open-field-pv",
Expand Down Expand Up @@ -103,7 +104,7 @@ rule all_tests:


rule dummy_tech_locations_template: # needed to provide `techs_and_locations_template` with a locational CSV linked to each technology that has no location-specific data to define.
message: "Create empty {wildcards.resolution} location-specific data file for the {wildcards.tech_group} tech `{wildcards.tech}`."
message: "Create empty {wildcards.resolution} location-specific data file for the {wildcards.tech_group} tech `{wildcards.tech}`." # Update ruleorder at the top of the file if you instead want the techs_and_locations_template rule to be used to generate a file
input: rules.locations_template.output.csv
output: "build/data/{resolution}/{tech_group}/{tech}.csv"
conda: "envs/shell.yaml"
Expand Down Expand Up @@ -180,8 +181,8 @@ rule model_template:
),
demand_timeseries_data = (
"build/models/{resolution}/timeseries/demand/electricity.csv",
"build/models/{resolution}/timeseries/demand/electrified-road-transport.csv",
"build/models/{resolution}/timeseries/demand/road-transport-historic-electrification.csv",
"build/models/{resolution}/timeseries/demand/uncontrolled-electrified-road-transport.csv",
"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",
),
Expand Down
1 change: 1 addition & 0 deletions config/default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ parameters:
coaches-and-buses: Motor coaches, buses and trolley buses
passenger-cars: Passenger cars
motorcycles: Powered 2-wheelers
uncontrolled-ev-charging-share: 0.5
entsoe-tyndp:
scenario: National Trends
grid: Reference
Expand Down
3 changes: 3 additions & 0 deletions config/schema.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,9 @@ properties:
motorcycles:
type: string
description: JRC-IDEES name of motorcycles.
uncontrolled-ev-charging-share:
type: number
description: Share of uncontrolled charging.
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
9 changes: 7 additions & 2 deletions docs/model/customisation.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,18 @@ Here, we describe each module in terms of the technologies they contain (`callio

=== "Technologies"

**demand_road_transport_electrified**: Electrified road transport demand
**demand_road_transport_electrified_uncontrolled**: Share of electrified road transport demand which is uncontrolled.

**demand_road_transport_historic_electrified**: Removes historically electrified road transport demand to avoid double counting
**demand_road_transport_historic_electrified_uncontrolled**: Removes historically electrified road transport demand to avoid double counting. It is assumed uncontrolled.

**demand_road_transport_electrified_controlled**: Share of electrified road transport demand whose charging is optimised by the solver.

=== "Overrides"

**keep-historic-electricity-demand-from-road-transport**: Keep historically electrified road transport demand. Historically electrified road transport demand is deleted by default, as it is already considered in historic electricity demand and would thus be counted twice. Using this override together with Euro-Calliope's default electricity demand is not advised.

**(year)_transport_controlled_electrified_demand**: Total electrified road transport demand whose charging is optimised by the solver.

??? note "demand/electrified-heat.yaml"

=== "Technologies"
Expand All @@ -58,6 +62,7 @@ Here, we describe each module in terms of the technologies they contain (`callio

**demand_heat_historic_electrified**: Removes historically electrified heat demand to avoid double counting


=== "Overrides"

**keep-historic-electricity-demand-from-heat**: Keep historically electrified heat demand. Historically electrified heat demand is deleted by default, as it is already considered in historic electricity demand and would thus be counted twice. Using this override together with Euro-Calliope's default electricity demand is not advised.
Expand Down
67 changes: 43 additions & 24 deletions rules/transport.smk
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@


rule download_transport_timeseries:
# TODO have correct timeseries data once RAMP has generated the new charging profile and it's been put on Zenodo
message: "Get EV data from RAMP"
params:
url = config["data-sources"]["ev-data"]
Expand Down Expand Up @@ -35,18 +36,38 @@ rule annual_transport_demand:
jrc_road_distance = "build/data/jrc-idees/transport/processed-road-distance.csv",
params:
fill_missing_values = config["data-pre-processing"]["fill-missing-values"]["jrc-idees"],
efficiency_quantile = config["parameters"]["transport"]["future-vehicle-efficiency-percentile"]
efficiency_quantile = config["parameters"]["transport"]["future-vehicle-efficiency-percentile"],
uncontrolled_charging_share = config["parameters"]["transport"]["uncontrolled-ev-charging-share"],
conda: "../envs/default.yaml"
output:
distance = "build/data/transport/annual-road-transport-distance-demand.csv",
distance_historic_electrification = "build/data/transport/annual-road-transport-historic-electrification.csv",
road_distance_controlled = "build/data/transport/annual-road-transport-distance-demand-controlled.csv",
road_distance_uncontrolled = "build/data/transport/annual-road-transport-distance-demand-uncontrolled.csv",
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"
input:
annual_controlled_demand = "build/data/transport/annual-road-transport-distance-demand-controlled.csv",
locations = "build/data/regional/units.csv",
populations = "build/data/regional/population.csv",
params:
first_year = config["scope"]["temporal"]["first-year"],
final_year = config["scope"]["temporal"]["final-year"],
power_scaling_factor = config["scaling-factors"]["power"],
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"],
conda: "../envs/default.yaml"
output:
main = "build/data/{resolution}/demand/electrified-transport.csv",
script: "../scripts/transport/road_transport_controlled_charging.py"


rule create_road_transport_timeseries:
message: "Create timeseries for road transport demand"
rule create_uncontrolled_road_transport_timeseries:
message: "Create timeseries for road transport demand (uncontrolled charging)"
input:
annual_data = "build/data/transport/annual-road-transport-distance-demand.csv",
annual_data = "build/data/transport/annual-road-transport-distance-demand-uncontrolled.csv",
timeseries = "data/automatic/ramp-ev-consumption-profiles.csv.gz"
params:
first_year = config["scope"]["temporal"]["first-year"],
Expand All @@ -60,14 +81,14 @@ rule create_road_transport_timeseries:
wildcard_constraints:
vehicle_type = "light-duty-vehicles|heavy-duty-vehicles|coaches-and-buses|passenger-cars|motorcycles"
output:
main = "build/data/transport/timeseries/timeseries-{vehicle_type}.csv",
main = "build/data/transport/timeseries/timeseries-uncontrolled-{vehicle_type}.csv",
script: "../scripts/transport/road_transport_timeseries.py"


use rule create_road_transport_timeseries as create_road_transport_timeseries_historic_electrification with:
message: "Create timeseries for historic electrified road transport demand"
use rule create_uncontrolled_road_transport_timeseries as create_uncontrolled_road_transport_timeseries_historic_electrification with:
message: "Create timeseries for historic electrified road transport demand (uncontrolled charging)"
input:
annual_data = "build/data/transport/annual-road-transport-historic-electrification.csv",
annual_data = "build/data/transport/annual-road-transport-distance-demand-historic-electrification.csv",
timeseries = "data/automatic/ramp-ev-consumption-profiles.csv.gz",
params:
first_year = config["scope"]["temporal"]["first-year"],
Expand All @@ -78,34 +99,32 @@ use rule create_road_transport_timeseries as create_road_transport_timeseries_hi
countries = config["scope"]["spatial"]["countries"],
country_neighbour_dict = config["data-pre-processing"]["fill-missing-values"]["ramp"],
output:
"build/data/transport/timeseries/timeseries-{vehicle_type}-historic-electrification.csv"
"build/data/transport/timeseries/timeseries-uncontrolled-{vehicle_type}-historic-electrification.csv"


rule aggregate_timeseries: # TODO consider merge with other rules, as this is tiny atm
message: "Aggregates timeseries for {wildcards.resolution} electrified road transport transport"
message: "Aggregates uncontrolled charging timeseries for {wildcards.resolution} electrified road transport transport"
input:
time_series = (
"build/data/transport/timeseries/timeseries-light-duty-vehicles.csv",
"build/data/transport/timeseries/timeseries-heavy-duty-vehicles.csv",
"build/data/transport/timeseries/timeseries-coaches-and-buses.csv",
"build/data/transport/timeseries/timeseries-passenger-cars.csv",
"build/data/transport/timeseries/timeseries-motorcycles.csv"),
time_series = [
f'build/data/transport/timeseries/timeseries-uncontrolled-{vehicle_type}.csv'
for vehicle_type in config["parameters"]["transport"]["road-transport-conversion-factors"].keys()
],
locations = "build/data/regional/units.csv",
populations = "build/data/regional/population.csv"
conda: "../envs/default.yaml"
output:
"build/models/{resolution}/timeseries/demand/electrified-road-transport.csv",
"build/models/{resolution}/timeseries/demand/uncontrolled-electrified-road-transport.csv",
script: "../scripts/transport/aggregate_timeseries.py"


use rule aggregate_timeseries as aggregate_timeseries_historic_electrified with:
message: "Aggregates timeseries for {wildcards.resolution} historically electrified road transport"
message: "Aggregates uncontrolled charging timeseries for {wildcards.resolution} historically electrified road transport"
input:
time_series = (
"build/data/transport/timeseries/timeseries-light-duty-vehicles-historic-electrification.csv",
"build/data/transport/timeseries/timeseries-coaches-and-buses-historic-electrification.csv",
"build/data/transport/timeseries/timeseries-passenger-cars-historic-electrification.csv"),
"build/data/transport/timeseries/timeseries-uncontrolled-light-duty-vehicles-historic-electrification.csv",
"build/data/transport/timeseries/timeseries-uncontrolled-coaches-and-buses-historic-electrification.csv",
"build/data/transport/timeseries/timeseries-uncontrolled-passenger-cars-historic-electrification.csv"),
locations = "build/data/regional/units.csv",
populations = "build/data/regional/population.csv"
output:
"build/models/{resolution}/timeseries/demand/road-transport-historic-electrification.csv"
"build/models/{resolution}/timeseries/demand/uncontrolled-road-transport-historic-electrification.csv"
23 changes: 19 additions & 4 deletions scripts/transport/annual_transport_demand.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,8 +229,23 @@ def fill_missing_countries_and_years(
.xs("electricity")
)

# Create CSV Files for calculated data
total_road_distance.rename("value").to_csv(snakemake.output.distance)
total_historically_electrified_distance.rename("value").to_csv(
snakemake.output.distance_historic_electrification
# Separate uncontrolled and controlled charging demands and create csv files
uncontrolled_share = snakemake.params.uncontrolled_charging_share

road_distance_controlled = (
total_road_distance.rename("value")
.mul(1 - uncontrolled_share)
.to_csv(snakemake.output.road_distance_controlled)
)
road_distance_uncontrolled = (
total_road_distance.rename("value")
.mul(uncontrolled_share)
.sub(total_historically_electrified_distance.rename("value"), fill_value=0)
.to_csv(snakemake.output.road_distance_uncontrolled)
)
# ASSUME historically electrified road consumption is all uncontrolled
road_distance_historically_electrified = (
total_historically_electrified_distance.rename("value").to_csv(
snakemake.output.road_distance_historically_electrified
)
)
106 changes: 106 additions & 0 deletions scripts/transport/road_transport_controlled_charging.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import pandas as pd
import pycountry


def scale_to_regional_resolution(df, region_country_mapping, populations):
"""
Create regional electricity demand for controlled charging.
ASSUME all road transport is subnationally distributed in proportion to population.
"""
df_population_share = (
populations.loc[:, "population_sum"]
.reindex(region_country_mapping.keys())
.groupby(by=region_country_mapping)
.transform(lambda df: df / df.sum())
)

regional_df = (
pd.DataFrame(
index=df.index,
data={
id: df[country_code]
for id, country_code in region_country_mapping.items()
},
)
.mul(df_population_share)
.rename(columns=lambda col_name: col_name.replace(".", "-"))
)
pd.testing.assert_series_equal(regional_df.sum(axis=1), df.sum(axis=1))
return regional_df


def scale_to_national_resolution(df):
df.columns.name = None
return df


def scale_to_continental_resolution(df):
return df.sum(axis=1).to_frame("EUR")


def convert_annual_distance_to_electricity_demand(
path_to_controlled_annual_demand: str,
power_scaling_factor: float,
first_year: int,
final_year: int,
conversion_factors: dict[str, float],
country_codes: list[str],
):
"""
Convert annual distance driven demand to electricity demand for
controlled charging accounting for conversion factors.
"""
df_energy_demand = (
pd.read_csv(path_to_controlled_annual_demand, index_col=[1, 2])
.xs(slice(first_year, final_year), level="year", drop_level=False)
.assign(value=lambda x: x["value"] * x["vehicle_type"].map(conversion_factors))
.groupby(["country_code", "year"])
.sum()
.loc[country_codes]
.mul(power_scaling_factor)
.squeeze()
.unstack("country_code")
)

return -df_energy_demand


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

path_to_controlled_annual_demand = snakemake.input.annual_controlled_demand
power_scaling_factor = snakemake.params.power_scaling_factor
first_year = snakemake.params.first_year
final_year = snakemake.params.final_year
conversion_factors = snakemake.params.conversion_factors
path_to_output = snakemake.output[0]
country_codes = (
[pycountry.countries.lookup(c).alpha_3 for c in snakemake.params.countries],
)
region_country_mapping = (
pd.read_csv(snakemake.input.locations, index_col=0)
.loc[:, "country_code"]
.to_dict()
)
populations = pd.read_csv(snakemake.input.populations, index_col=0)

df = convert_annual_distance_to_electricity_demand(
path_to_controlled_annual_demand,
power_scaling_factor,
first_year,
final_year,
conversion_factors,
country_codes,
)

if resolution == "continental":
df = scale_to_continental_resolution(df)
elif resolution == "national":
df = scale_to_national_resolution(df)
elif resolution == "regional":
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"])
Loading

0 comments on commit 9a4de45

Please sign in to comment.