Skip to content

Commit cbe6748

Browse files
committed
add objective discounting across investment periods
1 parent 577b6d3 commit cbe6748

File tree

15 files changed

+377
-24
lines changed

15 files changed

+377
-24
lines changed

ispypsa_runs/development/ispypsa_inputs/ispypsa_config.yaml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ scenario: Step Change
1212
# Weighted average cost of capital for annuitisation of generation and transmission
1313
# costs, as a fraction, i.e. 0.07 is 7%.
1414
wacc: 0.07
15+
# Discount rate applied to model objective function, as a fraction, i.e. 0.07 is 7%.
16+
discount_rate: 0.05
1517
network:
1618
# Does the model consider the expansion of sub-region to sub-region transmission
1719
# capacity
@@ -44,11 +46,11 @@ temporal:
4446
path_to_parsed_traces: ENV
4547
year_type: fy
4648
start_year: 2025
47-
end_year: 2025
49+
end_year: 2028
4850
reference_year_cycle: [2018]
4951
# List of investment period start years. An investment period runs until the next the
5052
# periods begins.
51-
investment_periods: [2025]
53+
investment_periods: [2025, 2026]
5254
aggregation:
5355
# Representative weeks to use instead of full yearly temporal representation.
5456
# Options:

src/ispypsa/config/validators.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,9 +90,9 @@ def validate_end_year(cls, end_year: float, info):
9090
@field_validator("investment_periods")
9191
@classmethod
9292
def validate_investment_periods(cls, investment_periods: float, info):
93-
if min(investment_periods) > info.data.get("start_year"):
93+
if min(investment_periods) != info.data.get("start_year"):
9494
raise ValueError(
95-
"config first investment period must be less than or equal to start_year"
95+
"config first investment period must be equal to start_year"
9696
)
9797
if len(investment_periods) != len(set(investment_periods)):
9898
raise ValueError("config all years in investment_periods must be unique")
@@ -107,6 +107,7 @@ class ModelConfig(BaseModel):
107107
ispypsa_run_name: str
108108
scenario: Literal[tuple(_ISP_SCENARIOS)]
109109
wacc: float
110+
discount_rate: float
110111
network: NetworkConfig
111112
temporal: TemporalConfig
112113
iasr_workbook_version: str

src/ispypsa/model/build.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
_add_generators_to_network,
1111
)
1212
from ispypsa.model.initialise import _initialise_network
13+
from ispypsa.model.investment_period_weights import _add_investment_period_weights
1314
from ispypsa.model.lines import _add_lines_to_network
1415

1516

@@ -49,6 +50,10 @@ def build_pypsa_network(
4950
"""
5051
network = _initialise_network(pypsa_friendly_tables["snapshots"])
5152

53+
_add_investment_period_weights(
54+
network, pypsa_friendly_tables["investment_period_weights"]
55+
)
56+
5257
_add_carriers_to_network(network, pypsa_friendly_tables["generators"])
5358

5459
_add_buses_to_network(
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import pandas as pd
2+
import pypsa
3+
4+
5+
def _add_investment_period_weights(
6+
network: pypsa.Network, investment_period_weights: pd.DataFrame
7+
) -> None:
8+
"""Adds investment period weights defined in a pypsa-friendly `pd.DataFrame` to the `pypsa.Network`.
9+
10+
Args:
11+
network: The `pypsa.Network` object
12+
investment_period_weights: `pd.DataFrame` specifying the
13+
investment period weights with columns 'period', "years" and 'objective'.
14+
Where "period" is the start years of the investment periods, "years" is the
15+
length of each investment period, and "objective" is the relative weight of
16+
the objective function in each investment period.
17+
18+
Returns: None
19+
"""
20+
investment_period_weights = investment_period_weights.set_index("period")
21+
network.investment_period_weightings = investment_period_weights

src/ispypsa/translator/create_pypsa_friendly_inputs.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,16 @@
2626
from ispypsa.translator.renewable_energy_zones import (
2727
_translate_renewable_energy_zone_build_limits_to_flow_paths,
2828
)
29-
from ispypsa.translator.snapshot import (
29+
from ispypsa.translator.snapshots import (
3030
_add_investment_periods,
3131
_create_complete_snapshots_index,
32+
_create_investment_period_weightings,
3233
)
3334
from ispypsa.translator.temporal_filters import _filter_snapshots
3435

3536
_BASE_TRANSLATOR_OUPUTS = [
3637
"snapshots",
38+
"investment_period_weights",
3739
"buses",
3840
"lines",
3941
"generators",
@@ -92,6 +94,12 @@ def create_pypsa_friendly_inputs(
9294
snapshots, config.temporal.investment_periods, config.temporal.year_type
9395
)
9496

97+
pypsa_inputs["investment_period_weights"] = _create_investment_period_weightings(
98+
config.temporal.investment_periods,
99+
config.temporal.end_year,
100+
config.discount_rate,
101+
)
102+
95103
pypsa_inputs["generators"] = _translate_ecaa_generators(
96104
ispypsa_tables["ecaa_generators"], config.network.nodes.regional_granularity
97105
)

src/ispypsa/translator/snapshot.py renamed to src/ispypsa/translator/snapshots.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from datetime import datetime
22

3+
import numpy as np
34
import pandas as pd
45

56
from ispypsa.translator.helpers import _get_iteration_start_and_end_time
@@ -106,3 +107,46 @@ def _add_investment_periods(
106107
)
107108

108109
return result.loc[:, ["investment_periods", "snapshots"]]
110+
111+
112+
def _create_investment_period_weightings(
113+
investment_periods: list[int], model_end_year: int, discount_rate: float
114+
) -> pd.DataFrame:
115+
"""Create a pd.DataFrame specifying the weighting of each investment period based on
116+
the sum of discounted periods during the period.
117+
118+
Args:
119+
investment_periods: list of years in which investment periods start.
120+
model_end_year: int specifying the last year modelling
121+
discount_rate: fraction (float) specifying the discount rate i.e. 5% is 0.05.
122+
123+
Returns: pd.DataFrame with columns 'period', "years" and 'objective'. Where
124+
"period" is the start years of the investment periods, "years" is the length
125+
of each investment period, and "objective" is the relative weight of the
126+
objective function in each investment period.
127+
"""
128+
# Add model_end_year to calculate final period length
129+
all_years = investment_periods + [model_end_year]
130+
131+
# Calculate period lengths
132+
investment_period_lengths = np.diff(all_years).astype("int64")
133+
134+
# Create DataFrame with periods and their lengths
135+
investment_period_weightings = pd.DataFrame(
136+
{"period": investment_periods, "years": investment_period_lengths}
137+
)
138+
139+
model_start_year = investment_periods[0]
140+
141+
def calc_weighting(period_start_year, period_length):
142+
T0 = period_start_year - model_start_year
143+
T1 = T0 + period_length
144+
r = discount_rate
145+
discounted_weights = [(1 / (1 + r) ** t) for t in range(T0, T1)]
146+
return sum(discounted_weights)
147+
148+
investment_period_weightings["objective"] = investment_period_weightings.apply(
149+
lambda row: calc_weighting(row["period"], row["years"]), axis=1
150+
)
151+
152+
return investment_period_weightings

0 commit comments

Comments
 (0)