Skip to content

Commit a4b8731

Browse files
authored
Merge pull request #43 from Open-ISP/multi-investment-periods
Multi period investment
2 parents c02e69e + f7bb573 commit a4b8731

28 files changed

+889
-147
lines changed

dodo.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ def create_pypsa_inputs_from_config_and_ispypsa_inputs(
127127
regional_granularity=config.network.nodes.regional_granularity,
128128
reference_year_mapping=reference_year_mapping,
129129
year_type=config.temporal.year_type,
130-
snapshot=pypsa_tables["snapshots"],
130+
snapshots=pypsa_tables["snapshots"],
131131
)
132132

133133

example_workflow.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@
7777
regional_granularity=config.network.nodes.regional_granularity,
7878
reference_year_mapping=reference_year_mapping,
7979
year_type=config.temporal.year_type,
80-
snapshot=pypsa_friendly_input_tables["snapshots"],
80+
snapshots=pypsa_friendly_input_tables["snapshots"],
8181
)
8282

8383
# Build a PyPSA network object.

ispypsa_runs/development/ispypsa_inputs/ispypsa_config.yaml

Lines changed: 6 additions & 1 deletion
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,8 +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]
51+
# List of investment period start years. An investment period runs until the next the
52+
# periods begins.
53+
investment_periods: [2025, 2026]
4954
aggregation:
5055
# Representative weeks to use instead of full yearly temporal representation.
5156
# Options:

src/ispypsa/config/validators.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ class TemporalConfig(BaseModel):
3131
start_year: int
3232
end_year: int
3333
reference_year_cycle: list[int]
34+
investment_periods: list[int]
3435
aggregation: TemporalAggregationConfig
3536

3637
@field_validator("operational_temporal_resolution_min")
@@ -86,11 +87,27 @@ def validate_end_year(cls, end_year: float, info):
8687
)
8788
return end_year
8889

90+
@field_validator("investment_periods")
91+
@classmethod
92+
def validate_investment_periods(cls, investment_periods: float, info):
93+
if min(investment_periods) != info.data.get("start_year"):
94+
raise ValueError(
95+
"config first investment period must be equal to start_year"
96+
)
97+
if len(investment_periods) != len(set(investment_periods)):
98+
raise ValueError("config all years in investment_periods must be unique")
99+
if sorted(investment_periods) != investment_periods:
100+
raise ValueError(
101+
"config investment_periods must be provided in sequential order"
102+
)
103+
return investment_periods
104+
89105

90106
class ModelConfig(BaseModel):
91107
ispypsa_run_name: str
92108
scenario: Literal[tuple(_ISP_SCENARIOS)]
93109
wacc: float
110+
discount_rate: float
94111
network: NetworkConfig
95112
temporal: TemporalConfig
96113
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(

src/ispypsa/model/buses.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,12 @@ def _add_bus_to_network(
2424
demand_trace_path = path_to_demand_traces / Path(f"{bus_name}.parquet")
2525
if demand_trace_path.exists():
2626
demand = pd.read_parquet(demand_trace_path)
27-
demand = demand.set_index("Datetime")
27+
demand = demand.set_index(["investment_periods", "snapshots"])
2828
network.add(
2929
class_name="Load",
3030
name=f"load_{bus_name}",
3131
bus=bus_name,
32-
p_set=demand["Value"],
32+
p_set=demand["p_set"],
3333
)
3434

3535

src/ispypsa/model/generators.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ def _add_generator_to_network(
5151
trace_data = None
5252

5353
if trace_data is not None:
54-
generator_definition["p_max_pu"] = trace_data.set_index("Datetime")["Value"]
54+
trace_data = trace_data.set_index(["investment_periods", "snapshots"])
55+
generator_definition["p_max_pu"] = trace_data["p_max_pu"]
5556

5657
network.add(**generator_definition)
5758

src/ispypsa/model/initialise.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ def _initialise_network(snapshots: pd.DataFrame) -> pypsa.Network:
1212
Returns:
1313
`pypsa.Network` object
1414
"""
15-
snapshots = pd.to_datetime(snapshots["snapshots"])
16-
network = pypsa.Network(snapshots=snapshots)
15+
snapshots["snapshots"] = pd.to_datetime(snapshots["snapshots"])
16+
snapshots_as_indexes = pd.MultiIndex.from_arrays(
17+
[snapshots["investment_periods"], snapshots["snapshots"]]
18+
)
19+
network = pypsa.Network(
20+
snapshots=snapshots_as_indexes,
21+
investment_periods=snapshots["investment_periods"].unique(),
22+
)
1723
return 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/templater/nodes.py

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -197,13 +197,9 @@ def _request_transmission_substation_coordinates() -> pd.DataFrame:
197197
data = xmltodict.parse(r.content)
198198
features = data["wfs:FeatureCollection"]["wfs:member"]
199199
for feature in features:
200-
substation = feature[
201-
"Foundation_Electricity_Infrastructure:Transmission_Substations"
202-
]
203-
name = substation.get("Foundation_Electricity_Infrastructure:NAME")
204-
coordinates = substation["Foundation_Electricity_Infrastructure:SHAPE"][
205-
"gml:Point"
206-
]["gml:pos"]
200+
substation = feature["esri:Transmission_Substations"]
201+
name = substation.get("esri:NAME")
202+
coordinates = substation["esri:SHAPE"]["gml:Point"]["gml:pos"]
207203
lat, long = coordinates.split(" ")
208204
substation_coordinates[name] = {
209205
"substation_latitude": lat,

src/ispypsa/translator/buses.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ def create_pypsa_friendly_bus_demand_timeseries(
7070
regional_granularity: str,
7171
reference_year_mapping: dict[int:int],
7272
year_type: Literal["fy", "calendar"],
73-
snapshot: pd.DataFrame,
73+
snapshots: pd.DataFrame,
7474
) -> None:
7575
"""Gets trace data for operational demand by constructing a timeseries from the
7676
start to end year using the reference year cycle provided.
@@ -93,7 +93,7 @@ def create_pypsa_friendly_bus_demand_timeseries(
9393
year with start_year and end_year specifiying the financial year to return
9494
data for, using year ending nomenclature (2016 ->FY2015/2016). If
9595
'calendar', then filtering is by calendar year.
96-
snapshot: pd.DataFrame containing the expected time series values.
96+
snapshots: pd.DataFrame containing the expected time series values.
9797
9898
Returns:
9999
None
@@ -134,13 +134,18 @@ def create_pypsa_friendly_bus_demand_timeseries(
134134
node_trace = node_traces.groupby("Datetime", as_index=False)["Value"].sum()
135135
# datetime in nanoseconds required by PyPSA
136136
node_trace["Datetime"] = node_trace["Datetime"].astype("datetime64[ns]")
137-
node_trace = _time_series_filter(node_trace, snapshot)
137+
node_trace = node_trace.rename(
138+
columns={"Datetime": "snapshots", "Value": "p_set"}
139+
)
140+
node_trace = _time_series_filter(node_trace, snapshots)
138141
_check_time_series(
139-
node_trace["Datetime"],
140-
snapshot["snapshots"],
142+
node_trace["snapshots"],
143+
snapshots["snapshots"],
141144
"demand data",
142145
demand_node,
143146
)
147+
node_trace = pd.merge(node_trace, snapshots, on="snapshots")
148+
node_trace = node_trace.loc[:, ["investment_periods", "snapshots", "p_set"]]
144149
node_trace.to_parquet(
145150
Path(output_trace_path, f"{demand_node}.parquet"), index=False
146151
)

src/ispypsa/translator/create_pypsa_friendly_inputs.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +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 _create_complete_snapshots_index
29+
from ispypsa.translator.snapshots import (
30+
_add_investment_periods,
31+
_create_complete_snapshots_index,
32+
_create_investment_period_weightings,
33+
)
3034
from ispypsa.translator.temporal_filters import _filter_snapshots
3135

3236
_BASE_TRANSLATOR_OUPUTS = [
3337
"snapshots",
38+
"investment_period_weights",
3439
"buses",
3540
"lines",
3641
"generators",
@@ -83,8 +88,16 @@ def create_pypsa_friendly_inputs(
8388
year_type=config.temporal.year_type,
8489
)
8590

86-
pypsa_inputs["snapshots"] = _filter_snapshots(
87-
config=config.temporal, snapshots=snapshots
91+
snapshots = _filter_snapshots(config=config.temporal, snapshots=snapshots)
92+
93+
pypsa_inputs["snapshots"] = _add_investment_periods(
94+
snapshots, config.temporal.investment_periods, config.temporal.year_type
95+
)
96+
97+
pypsa_inputs["investment_period_weights"] = _create_investment_period_weightings(
98+
config.temporal.investment_periods,
99+
config.temporal.end_year,
100+
config.discount_rate,
88101
)
89102

90103
pypsa_inputs["generators"] = _translate_ecaa_generators(

src/ispypsa/translator/generators.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,8 +128,11 @@ def create_pypsa_friendly_existing_generator_timeseries(
128128
)
129129
# datetime in nanoseconds required by PyPSA
130130
trace["Datetime"] = trace["Datetime"].astype("datetime64[ns]")
131+
trace = trace.rename(columns={"Datetime": "snapshots", "Value": "p_max_pu"})
131132
trace = _time_series_filter(trace, snapshots)
132133
_check_time_series(
133-
trace["Datetime"], snapshots["snapshots"], "generator trace data", gen
134+
trace["snapshots"], snapshots["snapshots"], "generator trace data", gen
134135
)
136+
trace = pd.merge(trace, snapshots, on="snapshots")
137+
trace = trace.loc[:, ["investment_periods", "snapshots", "p_max_pu"]]
135138
trace.to_parquet(Path(output_paths[gen_type], f"{gen}.parquet"), index=False)

src/ispypsa/translator/snapshot.py

Lines changed: 0 additions & 50 deletions
This file was deleted.

0 commit comments

Comments
 (0)