Skip to content

Commit 2f88ab2

Browse files
authored
Merge pull request #81 from staadecker/wind_to_solar_ratio
Wind to solar ratio
2 parents 05c1325 + ef289b2 commit 2f88ab2

File tree

6 files changed

+153
-11
lines changed

6 files changed

+153
-11
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*.vcf
1616
*.xml
1717
*.pickle
18+
examples/**/temp/**
1819
examples/**/outputs/*.csv
1920
examples/**/outputs/*.txt
2021
examples/**/graphs/*.png
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/*
2+
####################
3+
Add wind_to_solar_ratio column
4+
5+
Date applied: 2021-07-07
6+
Description:
7+
Adds a column called wind_to_solar_ratio to the database which is used by
8+
switch_model.policies.wind_to_solar_ratio
9+
#################
10+
*/
11+
12+
ALTER TABLE switch.scenario ADD COLUMN wind_to_solar_ratio real;
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
"""
2+
This module gives us the possibility to enforce a wind to solar capacity ratio.
3+
4+
It takes in wind_to_solar_ratio.csv that has the following format
5+
6+
PERIOD,wind_to_solar_ratio,wind_to_solar_ratio_const_gt
7+
2020,.,.
8+
2030,0.5,0
9+
2040,1,0
10+
2050,1.5,1
11+
12+
Here when wind_to_solar_ratio is specified (i.e. not '.') a constraint is activated that enforces that
13+
14+
Online wind capacity >=/<= Online solar capacity * wind_to_solar_ratio
15+
16+
for the entire period.
17+
18+
When wind_to_solar_ratio_const_gt is true (1) the constraint is a >= constraint.
19+
When wind_to_solar_ratio_const_gt is False (0) the constraint is a <= constraint.
20+
"""
21+
import os
22+
23+
import pandas as pd
24+
from pyomo.environ import *
25+
26+
from switch_model.reporting import write_table
27+
28+
_WIND_ENERGY_TYPE = "Wind"
29+
_SOLAR_ENERGY_TYPE = "Solar"
30+
31+
32+
def define_components(mod):
33+
mod.WindCapacity = Expression(
34+
mod.PERIODS,
35+
rule=lambda m, p: sum(
36+
m.GenCapacity[g, p] for g in m.VARIABLE_GENS if m.gen_energy_source[g] == _WIND_ENERGY_TYPE
37+
)
38+
)
39+
40+
mod.SolarCapacity = Expression(
41+
mod.PERIODS,
42+
rule=lambda m, p: sum(
43+
m.GenCapacity[g, p] for g in m.VARIABLE_GENS if m.gen_energy_source[g] == _SOLAR_ENERGY_TYPE
44+
)
45+
)
46+
47+
mod.wind_to_solar_ratio = Param(
48+
mod.PERIODS,
49+
default=0, # 0 means the constraint is inactive
50+
within=NonNegativeReals
51+
)
52+
53+
mod.wind_to_solar_ratio_const_gt = Param(
54+
mod.PERIODS,
55+
default=True,
56+
within=Boolean
57+
)
58+
59+
# We use a scaling factor to improve the numerical properties
60+
# of the model.
61+
# Learn more by reading the documentation on Numerical Issues.
62+
# 1e-3 was picked since this value is normally on the order of GW instead of MW
63+
scaling_factor = 1e-3
64+
65+
def wind_to_solar_ratio_const_rule(m, p):
66+
if m.wind_to_solar_ratio[p] == 0: # 0 means Constraint is inactive
67+
return Constraint.Skip
68+
69+
lhs = m.WindCapacity[p] * scaling_factor
70+
rhs = m.SolarCapacity[p] * m.wind_to_solar_ratio[p] * scaling_factor
71+
if m.wind_to_solar_ratio_const_gt[p]:
72+
return lhs >= rhs
73+
else:
74+
return lhs <= rhs
75+
76+
mod.wind_to_solar_ratio_const = Constraint(mod.PERIODS, rule=wind_to_solar_ratio_const_rule)
77+
78+
79+
def load_inputs(mod, switch_data, inputs_dir):
80+
switch_data.load_aug(
81+
filename=os.path.join(inputs_dir, 'wind_to_solar_ratio.csv'),
82+
auto_select=True,
83+
param=(mod.wind_to_solar_ratio, mod.wind_to_solar_ratio_const_gt),
84+
optional=True # We want to allow including this module even if the file isn't there
85+
)
86+
87+
88+
def post_solve(m, outdir):
89+
df = pd.DataFrame({
90+
"WindCapacity (GW)": value(m.WindCapacity[p]) / 1000,
91+
"SolarCapacity (GW)": value(m.SolarCapacity[p]) / 1000,
92+
"ComputedRatio": value(m.WindCapacity[p] / m.SolarCapacity[p]) if value(m.SolarCapacity[p]) != 0 else ".",
93+
"ExpectedRatio": value(m.wind_to_solar_ratio[p]) if m.wind_to_solar_ratio[p] != 0 else "."
94+
} for p in m.PERIODS)
95+
write_table(m, output_file=os.path.join(outdir, "wind_to_solar_ratio.csv"), df=df, index=False)

switch_model/tools/templates/config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ get_inputs:
4040
# enable_planning_reserves:
4141
# generation_plant_technologies_scenario_id:
4242
# variable_o_m_cost_scenario_id:
43+
# wind_to_solar_ratio:
4344
# add_storage was used by Martin when studying LDES
4445
# you likely don't need to use these parameters
4546
# they won't impact your runs

switch_model/utilities/__init__.py

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -582,14 +582,21 @@ def load_aug(switch_data, optional=False, auto_select=False,
582582
switch_data.load(**kwds)
583583
return
584584

585+
# Use our custom DataManager to allow 'inf' in csvs.
586+
if extension == ".csv":
587+
kwds['using'] = "switch_csv"
588+
585589
# copy the optional_params to avoid side-effects when the list is altered below
586590
optional_params=list(optional_params)
587591
# Parse header and first row
588592
with open(path) as infile:
589593
headers_line = infile.readline()
590594
second_line = infile.readline()
591-
file_is_empty = (headers_line == '')
592-
file_has_no_data_rows = (second_line == '')
595+
596+
# Skip if the file is empty.
597+
if optional and headers_line == '':
598+
return
599+
593600
suffix = path.split('.')[-1]
594601
if suffix in {'tab', 'tsv'}:
595602
separator = '\t'
@@ -599,9 +606,7 @@ def load_aug(switch_data, optional=False, auto_select=False,
599606
raise InputError(f'Unrecognized file type for input file {path}')
600607
# TODO: parse this more formally, e.g. using csv module
601608
headers = headers_line.strip().split(separator)
602-
# Skip if the file is empty.
603-
if optional and file_is_empty:
604-
return
609+
605610
# Try to get a list of parameters. If param was given as a
606611
# singleton or a tuple, make it into a list that can be edited.
607612
params = []
@@ -681,14 +686,11 @@ def load_aug(switch_data, optional=False, auto_select=False,
681686
del kwds['select'][i]
682687
del kwds['param'][p_i]
683688

684-
if optional and file_has_no_data_rows:
685-
# Skip the file. Note that we are only doing this after having
689+
if optional and second_line == '':
690+
# Skip the file if it has no data. Note that we are only doing this after having
686691
# validated the file's column headings.
687692
return
688693

689-
# Use our custom DataManager to allow 'inf' in csvs.
690-
if kwds["filename"][-4:] == ".csv":
691-
kwds['using'] = "switch_csv"
692694
# All done with cleaning optional bits. Pass the updated arguments
693695
# into the DataPortal.load() function.
694696
switch_data.load(**kwds)

switch_model/wecc/get_inputs/get_inputs.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ def write_csv(data: Iterable[List], fname, headers: List[str], log=True):
6969
"switch_model.policies.rps_unbundled",
7070
"switch_model.policies.min_per_tech", # Always include since it provides useful outputs even when unused
7171
# "switch_model.reporting.basic_exports_wecc",
72+
# Always include since by default it does nothing except output useful data
73+
"switch_model.policies.wind_to_solar_ratio",
7274
]
7375

7476

@@ -103,7 +105,8 @@ def query_db(full_config, skip_cf):
103105
"ca_policies_scenario_id",
104106
"enable_planning_reserves",
105107
"generation_plant_technologies_scenario_id",
106-
"variable_o_m_cost_scenario_id"
108+
"variable_o_m_cost_scenario_id",
109+
"wind_to_solar_ratio"
107110
]
108111

109112
db_cursor.execute(
@@ -140,6 +143,7 @@ def query_db(full_config, skip_cf):
140143
enable_planning_reserves = s_details[18]
141144
generation_plant_technologies_scenario_id =s_details[19]
142145
variable_o_m_cost_scenario_id = s_details[20]
146+
wind_to_solar_ratio = s_details[21]
143147

144148
print(f"Scenario: {scenario_id}: {name}.")
145149

@@ -856,6 +860,7 @@ def query_db(full_config, skip_cf):
856860
)
857861

858862
ca_policies(db_cursor, ca_policies_scenario_id, study_timeframe_id)
863+
write_wind_to_solar_ratio(wind_to_solar_ratio)
859864
if enable_planning_reserves:
860865
planning_reserves(db_cursor, time_sample_id, hydro_simple_scenario_id)
861866
create_modules_txt()
@@ -868,6 +873,32 @@ def query_db(full_config, skip_cf):
868873
shutil.copy(os.path.join(graph_config, "graph_tech_types.csv"), "graph_tech_types.csv")
869874

870875

876+
def write_wind_to_solar_ratio(wind_to_solar_ratio):
877+
# TODO ideally we'd have a table where we can specify the wind_to_solar_ratios per period.
878+
# At the moment only the wind_to_solar_ratio is specified and which doesn't allow different values per period
879+
if wind_to_solar_ratio is None:
880+
return
881+
882+
print("wind_to_solar_ratio.csv...")
883+
df = pd.read_csv("periods.csv")[["INVESTMENT_PERIOD"]]
884+
df["wind_to_solar_ratio"] = wind_to_solar_ratio
885+
886+
# wind_to_solar_ratio.csv requires a column called wind_to_solar_ratio_const_gt that is True (1) or False (0)
887+
# This column specifies whether the constraint is a greater than constraint or a less than constraint.
888+
# In our case we want it to be a greater than constraint if we're trying to force wind-to-solar ratio above its default
889+
# and we want it to be a less than constraint if we're trying to force the ratio below its default.
890+
# Here the default is the ratio if we didn't have the constraint.
891+
cutoff_ratio = 0.28
892+
warnings.warn(
893+
"To determine the sign of the wind-to-solar ratio constraint we have "
894+
f"assumed that without the constraint, the wind-to-solar ratio is {cutoff_ratio}. "
895+
f"This value was accurate for Martin's LDES runs however it may not be accurate for you. "
896+
f"You should update this value in get_inputs or manually specify whether you want a greater than "
897+
f"or a less than constraint."
898+
)
899+
df["wind_to_solar_ratio_const_gt"] = 1 if wind_to_solar_ratio > cutoff_ratio else 0
900+
901+
df.to_csv("wind_to_solar_ratio.csv", index=False)
871902

872903
def ca_policies(db_cursor, ca_policies_scenario_id, study_timeframe_id):
873904
if ca_policies_scenario_id is None:

0 commit comments

Comments
 (0)