-
Notifications
You must be signed in to change notification settings - Fork 54
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #236 from PyPSA/transport
separate rules for transport demand and nodal energy totals
- Loading branch information
Showing
5 changed files
with
348 additions
and
213 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
"""Build population-weighted energy totals.""" | ||
|
||
import pandas as pd | ||
|
||
if __name__ == '__main__': | ||
if 'snakemake' not in globals(): | ||
from helper import mock_snakemake | ||
snakemake = mock_snakemake( | ||
'build_population_weighted_energy_totals', | ||
simpl='', | ||
clusters=48, | ||
) | ||
|
||
pop_layout = pd.read_csv(snakemake.input.clustered_pop_layout, index_col=0) | ||
|
||
energy_totals = pd.read_csv(snakemake.input.energy_totals, index_col=0) | ||
|
||
nodal_energy_totals = energy_totals.loc[pop_layout.ct].fillna(0.) | ||
nodal_energy_totals.index = pop_layout.index | ||
nodal_energy_totals = nodal_energy_totals.multiply(pop_layout.fraction, axis=0) | ||
|
||
nodal_energy_totals.to_csv(snakemake.output[0]) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,201 @@ | ||
"""Build transport demand.""" | ||
|
||
import pandas as pd | ||
import numpy as np | ||
import xarray as xr | ||
from helper import generate_periodic_profiles | ||
|
||
|
||
def build_nodal_transport_data(fn, pop_layout): | ||
|
||
transport_data = pd.read_csv(fn, index_col=0) | ||
|
||
nodal_transport_data = transport_data.loc[pop_layout.ct].fillna(0.0) | ||
nodal_transport_data.index = pop_layout.index | ||
nodal_transport_data["number cars"] = ( | ||
pop_layout["fraction"] * nodal_transport_data["number cars"] | ||
) | ||
nodal_transport_data.loc[ | ||
nodal_transport_data["average fuel efficiency"] == 0.0, | ||
"average fuel efficiency", | ||
] = transport_data["average fuel efficiency"].mean() | ||
|
||
return nodal_transport_data | ||
|
||
|
||
def build_transport_demand(traffic_fn, airtemp_fn, nodes, nodal_transport_data): | ||
|
||
## Get overall demand curve for all vehicles | ||
|
||
traffic = pd.read_csv( | ||
traffic_fn, skiprows=2, usecols=["count"], squeeze=True | ||
) | ||
|
||
transport_shape = generate_periodic_profiles( | ||
dt_index=snapshots, | ||
nodes=nodes, | ||
weekly_profile=traffic.values, | ||
) | ||
transport_shape = transport_shape / transport_shape.sum() | ||
|
||
# electric motors are more efficient, so alter transport demand | ||
|
||
plug_to_wheels_eta = options["bev_plug_to_wheel_efficiency"] | ||
battery_to_wheels_eta = plug_to_wheels_eta * options["bev_charge_efficiency"] | ||
|
||
efficiency_gain = ( | ||
nodal_transport_data["average fuel efficiency"] / battery_to_wheels_eta | ||
) | ||
|
||
# get heating demand for correction to demand time series | ||
temperature = xr.open_dataarray(airtemp_fn).to_pandas() | ||
|
||
# correction factors for vehicle heating | ||
dd_ICE = transport_degree_factor( | ||
temperature, | ||
options["transport_heating_deadband_lower"], | ||
options["transport_heating_deadband_upper"], | ||
options["ICE_lower_degree_factor"], | ||
options["ICE_upper_degree_factor"], | ||
) | ||
|
||
dd_EV = transport_degree_factor( | ||
temperature, | ||
options["transport_heating_deadband_lower"], | ||
options["transport_heating_deadband_upper"], | ||
options["EV_lower_degree_factor"], | ||
options["EV_upper_degree_factor"], | ||
) | ||
|
||
# divide out the heating/cooling demand from ICE totals | ||
# and multiply back in the heating/cooling demand for EVs | ||
ice_correction = (transport_shape * (1 + dd_ICE)).sum() / transport_shape.sum() | ||
|
||
energy_totals_transport = ( | ||
pop_weighted_energy_totals["total road"] | ||
+ pop_weighted_energy_totals["total rail"] | ||
- pop_weighted_energy_totals["electricity rail"] | ||
) | ||
|
||
transport = ( | ||
(transport_shape.multiply(energy_totals_transport) * 1e6 * Nyears) | ||
.divide(efficiency_gain * ice_correction) | ||
.multiply(1 + dd_EV) | ||
) | ||
|
||
return transport | ||
|
||
|
||
def transport_degree_factor( | ||
temperature, | ||
deadband_lower=15, | ||
deadband_upper=20, | ||
lower_degree_factor=0.5, | ||
upper_degree_factor=1.6, | ||
): | ||
""" | ||
Work out how much energy demand in vehicles increases due to heating and cooling. | ||
There is a deadband where there is no increase. | ||
Degree factors are % increase in demand compared to no heating/cooling fuel consumption. | ||
Returns per unit increase in demand for each place and time | ||
""" | ||
|
||
dd = temperature.copy() | ||
|
||
dd[(temperature > deadband_lower) & (temperature < deadband_upper)] = 0.0 | ||
|
||
dT_lower = deadband_lower - temperature[temperature < deadband_lower] | ||
dd[temperature < deadband_lower] = lower_degree_factor / 100 * dT_lower | ||
|
||
dT_upper = temperature[temperature > deadband_upper] - deadband_upper | ||
dd[temperature > deadband_upper] = upper_degree_factor / 100 * dT_upper | ||
|
||
return dd | ||
|
||
|
||
def bev_availability_profile(fn, snapshots, nodes, options): | ||
""" | ||
Derive plugged-in availability for passenger electric vehicles. | ||
""" | ||
|
||
traffic = pd.read_csv(fn, skiprows=2, usecols=["count"], squeeze=True) | ||
|
||
avail_max = options["bev_avail_max"] | ||
avail_mean = options["bev_avail_mean"] | ||
|
||
avail = avail_max - (avail_max - avail_mean) * (traffic - traffic.min()) / ( | ||
traffic.mean() - traffic.min() | ||
) | ||
|
||
avail_profile = generate_periodic_profiles( | ||
dt_index=snapshots, | ||
nodes=nodes, | ||
weekly_profile=avail.values, | ||
) | ||
|
||
return avail_profile | ||
|
||
|
||
def bev_dsm_profile(snapshots, nodes, options): | ||
|
||
dsm_week = np.zeros((24 * 7,)) | ||
|
||
dsm_week[(np.arange(0, 7, 1) * 24 + options["bev_dsm_restriction_time"])] = options[ | ||
"bev_dsm_restriction_value" | ||
] | ||
|
||
dsm_profile = generate_periodic_profiles( | ||
dt_index=snapshots, | ||
nodes=nodes, | ||
weekly_profile=dsm_week, | ||
) | ||
|
||
return dsm_profile | ||
|
||
|
||
if __name__ == "__main__": | ||
if "snakemake" not in globals(): | ||
from helper import mock_snakemake | ||
|
||
snakemake = mock_snakemake( | ||
"build_transport_demand", | ||
simpl="", | ||
clusters=48, | ||
) | ||
|
||
pop_layout = pd.read_csv(snakemake.input.clustered_pop_layout, index_col=0) | ||
|
||
nodes = pop_layout.index | ||
|
||
pop_weighted_energy_totals = pd.read_csv( | ||
snakemake.input.pop_weighted_energy_totals, index_col=0 | ||
) | ||
|
||
options = snakemake.config["sector"] | ||
|
||
snapshots = pd.date_range(freq='h', **snakemake.config["snapshots"], tz="UTC") | ||
|
||
Nyears = 1 | ||
|
||
nodal_transport_data = build_nodal_transport_data( | ||
snakemake.input.transport_data, | ||
pop_layout | ||
) | ||
|
||
transport_demand = build_transport_demand( | ||
snakemake.input.traffic_data_KFZ, | ||
snakemake.input.temp_air_total, | ||
nodes, nodal_transport_data | ||
) | ||
|
||
avail_profile = bev_availability_profile( | ||
snakemake.input.traffic_data_Pkw, | ||
snapshots, nodes, options | ||
) | ||
|
||
dsm_profile = bev_dsm_profile(snapshots, nodes, options) | ||
|
||
nodal_transport_data.to_csv(snakemake.output.transport_data) | ||
transport_demand.to_csv(snakemake.output.transport_demand) | ||
avail_profile.to_csv(snakemake.output.avail_profile) | ||
dsm_profile.to_csv(snakemake.output.dsm_profile) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.