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 uncontrolled vs controlled charging #338

Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
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 @@ -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 @@ -98,7 +99,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 @@ -175,8 +176,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
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't looked, but how close is this to a copy-paste of the uncontrolled python file? If it is very similar, we should consider using the two files with a controlled/uncontrolled flag that decides on the functions to use etc.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's fairly different, because the uncontrolled scripts generate timeseries per vehicle type which are then aggregated and rescaled per resolution. This last step is similar but still different because contains different data structures. The controlled script is actually closer to the rescale.py in the heat sector.

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"])
39 changes: 30 additions & 9 deletions templates/models/techs/demand/electrified-transport.yaml
Original file line number Diff line number Diff line change
@@ -1,29 +1,50 @@
techs:
demand_road_transport_electrified:
demand_road_transport_electrified_uncontrolled:
essentials:
name: 'Electrified road transport demand'
name: 'Uncontrolled electrified road transport demand -- follows a timeseries'
parent: demand
carrier: electricity
constraints:
resource: file=demand/electrified-road-transport.csv
resource: file=demand/uncontrolled-electrified-road-transport.csv

demand_road_transport_historic_electrified:
demand_road_transport_historic_electrified_uncontrolled:
essentials:
name: 'Removes historic electrified road transport demand'
name: 'Removes historic electrified road transport demand -- assumed uncontrolled'
parent: supply
carrier: electricity
constraints:
resource: file=demand/road-transport-historic-electrification.csv
resource: file=demand/uncontrolled-road-transport-historic-electrification.csv
resource_min_use: 1
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be "force_resource"?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated it, indeed I think it makes more sense


demand_road_transport_electrified_controlled:
essentials:
name: 'Controlled electrified road transport demand'
parent: demand
carrier: electricity
constraints:
force_resource: false
resource: -.inf

overrides:
keep-historic-electricity-demand-from-road-transport:
{% for id, location in locations.iterrows() %}
{{ id }}.techs.demand_road_transport_historic_electrified.exists: False
{{ id }}.techs.demand_road_transport_historic_electrified_uncontrolled.exists: False
{% endfor %}

{% for year in locations.columns %}
{{ year }}_transport_controlled_electrified_demand:
group_constraints:
{% for location in locations.index %}
{{ location }}_annual_controlled_electricity_demand:
locs: [{{ location }}]
techs: [demand_road_transport_electrified_controlled]
carrier_con_equals:
electricity: {{ locations.loc[location, year] }} # {{ (1 / scaling_factors.power) | unit("MWh") }}
{% endfor %}
{% endfor %}

locations:
{% for id, location in locations.iterrows() %}
{{ id }}.techs.demand_road_transport_electrified:
{{ id }}.techs.demand_road_transport_historic_electrified:
{{ id }}.techs.demand_road_transport_electrified_uncontrolled:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs controlled tech here too. I'd maybe update to:

{{ id }}.techs:
  demand_road_transport_electrified_uncontrolled:
  demand_road_transport_historic_electrified_uncontrolled:
  demand_road_transport_electrified_controlled:
  

{{ id }}.techs.demand_road_transport_historic_electrified_uncontrolled:
{% endfor %}
Loading