Skip to content

Commit 1bf8fd3

Browse files
committed
Merge remote-tracking branch 'rael/wecc' into make-mps
2 parents d1480ba + 945189c commit 1bf8fd3

26 files changed

+730
-223
lines changed

REAM Model Changelog.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,5 @@ Changes are listed from oldest (first line) to newest (last line of table).
1515
| #36 | May 2021 | Correct inputs to only list transmission lines in one direction. |
1616
| #56 | June 2021 | Convert 2020 predetermined build years to 2019 in `get_inputs.py` to avoid conflicts with 2020 period. |
1717
| #57 | June 2021 | Specify predetermined storage energy capacity in inputs (previously left unspecified). |
18-
| #68 | June 2021 | Change financial params to 2018 dollars & 5% interest rate. Start using terrain multipliers (which now include the economic multiplier). |
18+
| #68 | June 2021 | Change financial params to 2018 dollars & 5% interest rate. Start using terrain multipliers (which now include the economic multiplier). |
19+
| #72 | July 2021 | Drop build and O&M costs of existing transmission lines. |
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
####################
3+
Add generation plants groups
4+
5+
Date applied:
6+
Description:
7+
This script adds the option to specify generation plant groups.
8+
The generation groups are specified in the table generation_plant_group.
9+
Plants are assigned to a group by adding them to the many-to-many table generation_plant_group_member.
10+
Groups are assigned to a generation_plant_scenario_id by specifying them in generation_plant_scenario_group_member
11+
#################
12+
*/
13+
14+
CREATE TABLE switch.generation_plant_group
15+
(
16+
generation_plant_group_id serial NOT NULL,
17+
description text NOT NULL,
18+
name character varying(30) NOT NULL,
19+
PRIMARY KEY (generation_plant_group_id)
20+
);
21+
22+
COMMENT ON TABLE switch.generation_plant_group
23+
IS 'This table specifies all the generation plant groups. Every group has a set of generation plants (see generation_plant_group_member). Groups can be assigned to a generation_plant_scenario (see generation_plant_scenario_group_member).';
24+
25+
CREATE TABLE switch.generation_plant_group_member
26+
(
27+
generation_plant_group_id integer,
28+
generation_plant_id integer,
29+
PRIMARY KEY (generation_plant_group_id, generation_plant_id)
30+
);
31+
32+
ALTER TABLE switch.generation_plant_group_member
33+
ADD CONSTRAINT generation_plant_group_member_group_id_fkey
34+
FOREIGN KEY (generation_plant_group_id)
35+
REFERENCES switch.generation_plant_group (generation_plant_group_id);
36+
37+
ALTER TABLE switch.generation_plant_group_member
38+
ADD CONSTRAINT generation_plant_group_member_generation_plant_id_fkey
39+
FOREIGN KEY (generation_plant_id)
40+
REFERENCES switch.generation_plant (generation_plant_id);
41+
42+
COMMENT ON TABLE switch.generation_plant_group_member
43+
IS 'This table is a many-to-many table that specifies the generation plants that are associated with a generation group.';
44+
45+
CREATE TABLE switch.generation_plant_scenario_group_member
46+
(
47+
generation_plant_scenario_id integer,
48+
generation_plant_group_id integer,
49+
PRIMARY KEY (generation_plant_scenario_id, generation_plant_group_id)
50+
);
51+
52+
ALTER TABLE switch.generation_plant_scenario_group_member
53+
ADD CONSTRAINT generation_plant_scenario_group_member_scenario_id_fkey
54+
FOREIGN KEY (generation_plant_scenario_id)
55+
REFERENCES switch.generation_plant_scenario (generation_plant_scenario_id);
56+
57+
ALTER TABLE switch.generation_plant_scenario_group_member
58+
ADD CONSTRAINT generation_plant_scenario_group_member_group_id_fkey
59+
FOREIGN KEY (generation_plant_group_id)
60+
REFERENCES switch.generation_plant_group (generation_plant_group_id);
61+
62+
COMMENT ON TABLE switch.generation_plant_scenario_group_member
63+
IS 'This table is a many-to-many table that specifies which generation plant groups belong to which generation plant scenarios';
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/*
2+
####################
3+
Add column gen_store_energy_to_power_ratio
4+
5+
Date applied: 2021-06-18
6+
Description:
7+
This script adds a column to the generation_plant
8+
table called gen_storage_energy_to_power_ratio specifying
9+
the storage duration
10+
#################
11+
*/
12+
13+
ALTER TABLE switch.generation_plant ADD COLUMN gen_storage_energy_to_power_ratio real;
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*
2+
####################
3+
Add transmission options
4+
5+
Date applied: 2021-06-23
6+
Description:
7+
Adds two rows to table transmission_base_capital_cost_scenario_id
8+
1. A scenario where transmission costs are zero.
9+
2. A scenario where transmission costs are infinity (building not allowed).
10+
#################
11+
*/
12+
13+
INSERT INTO switch.transmission_base_capital_cost (transmission_base_capital_cost_scenario_id,
14+
trans_capital_cost_per_mw_km, description)
15+
VALUES (3, 'Infinity', 'For scenarios where building transmission is forbidden.');
16+
17+
INSERT INTO switch.transmission_base_capital_cost (transmission_base_capital_cost_scenario_id,
18+
trans_capital_cost_per_mw_km, description)
19+
VALUES (4, 0, 'For scenarios where transmission is unlimited.');
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/*
2+
####################
3+
Add transmission options
4+
5+
Date applied: 2021-06-29
6+
Description:
7+
Adds an extra scenario to the database for a 10x increase in transmission costs.
8+
#################
9+
*/
10+
11+
INSERT INTO switch.transmission_base_capital_cost (transmission_base_capital_cost_scenario_id,
12+
trans_capital_cost_per_mw_km, description)
13+
VALUES (5, 9600, '10x the costs of scenario #2. Approximates the no TX case.');

switch_model/__main__.py

Lines changed: 59 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -4,74 +4,70 @@
44
"""Script to handle switch <cmd> calls from the command line."""
55
from __future__ import print_function
66

7-
import sys, os
7+
import argparse
8+
import importlib
9+
import sys
810
import switch_model
11+
from switch_model.utilities import get_git_branch
12+
13+
def version():
14+
print("Switch model version " + switch_model.__version__)
15+
branch = get_git_branch()
16+
if branch is not None:
17+
print(f"Switch Git branch: {branch}")
18+
return 0
19+
20+
def help_text():
21+
print(
22+
f"Must specifiy one of the following commands: {list(cmds.keys())}.\nE.g. Run 'switch solve' or 'switch get_inputs'.")
23+
24+
25+
def get_module_runner(module):
26+
def runner():
27+
importlib.import_module(module).main()
28+
return runner
29+
30+
31+
cmds = {
32+
"solve": get_module_runner("switch_model.solve"),
33+
"solve-scenarios": get_module_runner("switch_model.solve_scenarios"),
34+
"test": get_module_runner("switch_model.test"),
35+
"upgrade": get_module_runner("switch_model.upgrade"),
36+
"get_inputs": get_module_runner("switch_model.wecc.get_inputs"),
37+
"drop": get_module_runner("switch_model.tools.drop"),
38+
"new": get_module_runner("switch_model.tools.new"),
39+
"graph": get_module_runner("switch_model.tools.graph.cli_graph"),
40+
"compare": get_module_runner("switch_model.tools.graph.cli_compare"),
41+
"db": get_module_runner("switch_model.wecc.__main__"),
42+
"help": help_text
43+
}
944

1045

1146
def main():
12-
# TODO make a proper command line tool with help information for each option
13-
cmds = [
14-
"solve",
15-
"solve-scenarios",
16-
"test",
17-
"upgrade",
18-
"get_inputs",
19-
"--version",
20-
"drop",
21-
"new",
22-
"graph",
23-
"compare",
24-
"sampling",
25-
]
26-
if len(sys.argv) >= 2 and sys.argv[1] in cmds:
27-
# If users run a script from the command line, the location of the script
28-
# gets added to the start of sys.path; if they call a module from the
29-
# command line then an empty entry gets added to the start of the path,
30-
# indicating the current working directory. This module is often called
31-
# from a command-line script, but we want the current working
32-
# directory in the path because users may try to load local modules via
33-
# the configuration files, so we make sure that's always in the path.
34-
sys.path[0] = ""
35-
36-
# adjust the argument list to make it look like someone ran "python -m <module>" directly
37-
cmd = sys.argv[1]
38-
sys.argv[0] += " " + cmd
47+
parser = argparse.ArgumentParser(add_help=False)
48+
parser.add_argument("--version", default=False, action="store_true", help="Get version info")
49+
parser.add_argument("subcommand", choices=cmds.keys(), help="The possible switch subcommands", nargs="?",
50+
default="help")
51+
52+
# If users run a script from the command line, the location of the script
53+
# gets added to the start of sys.path; if they call a module from the
54+
# command line then an empty entry gets added to the start of the path,
55+
# indicating the current working directory. This module is often called
56+
# from a command-line script, but we want the current working
57+
# directory in the path because users may try to load local modules via
58+
# the configuration files, so we make sure that's always in the path.
59+
sys.path[0] = ""
60+
61+
args, remaining_args = parser.parse_known_args()
62+
63+
if args.version:
64+
return version()
65+
66+
# adjust the argument list to make it look like someone ran "python -m <module>" directly
67+
if len(sys.argv) > 1:
68+
sys.argv[0] += " " + sys.argv[1]
3969
del sys.argv[1]
40-
if cmd == "--version":
41-
print("Switch model version " + switch_model.__version__)
42-
from switch_model.utilities import get_git_branch
43-
branch = get_git_branch()
44-
if branch is not None:
45-
print(f"Switch Git branch: {branch}")
46-
return 0
47-
if cmd == "solve":
48-
from switch_model.solve import main
49-
elif cmd == "solve-scenarios":
50-
from switch_model.solve_scenarios import main
51-
elif cmd == "test":
52-
from switch_model.test import main
53-
elif cmd == "upgrade":
54-
from switch_model.upgrade import main
55-
elif cmd == "get_inputs":
56-
from switch_model.wecc.get_inputs import main
57-
elif cmd == "sampling":
58-
from switch_model.wecc.sampling import main
59-
elif cmd == "drop":
60-
from switch_model.tools.drop import main
61-
elif cmd == "new":
62-
from switch_model.tools.new import main
63-
elif cmd == "graph":
64-
from switch_model.tools.graph.cli_graph import main
65-
elif cmd == "compare":
66-
from switch_model.tools.graph.cli_compare import main
67-
main()
68-
else:
69-
print(
70-
"Usage: {} {{{}}} ...".format(
71-
os.path.basename(sys.argv[0]), ", ".join(cmds)
72-
)
73-
)
74-
print("Use one of these commands with --help for more information.")
70+
cmds[args.subcommand]()
7571

7672

7773
if __name__ == "__main__":

switch_model/energy_sources/fuel_costs/markets.py

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,10 @@ def define_components(mod):
204204
become non-linear.
205205
206206
"""
207+
# When this variable is True we only allow positive fuel costs
208+
# This simplifies the model since we can set some of our constraints
209+
# as greater than instead of equals.
210+
ONLY_POSITIVE_RFM_COSTS = False
207211

208212
mod.REGIONAL_FUEL_MARKETS = Set(dimen=1)
209213
mod.rfm_fuel = Param(mod.REGIONAL_FUEL_MARKETS, within=mod.FUELS)
@@ -234,7 +238,8 @@ def zone_rfm_init(m, load_zone, fuel):
234238
dimen=3, validate=lambda m, r, p, st: (
235239
r in m.REGIONAL_FUEL_MARKETS and p in m.PERIODS))
236240
mod.rfm_supply_tier_cost = Param(
237-
mod.RFM_SUPPLY_TIERS, within=Reals)
241+
mod.RFM_SUPPLY_TIERS,
242+
within=PositiveReals if ONLY_POSITIVE_RFM_COSTS else Reals)
238243
mod.rfm_supply_tier_limit = Param(
239244
mod.RFM_SUPPLY_TIERS, within=NonNegativeReals, default=float('inf'))
240245
mod.min_data_check(
@@ -333,12 +338,16 @@ def GENS_FOR_RFM_PERIOD_rule(m, rfm, p):
333338
enforce_fuel_consumption_scaling_factor = 1e-2
334339

335340
def Enforce_Fuel_Consumption_rule(m, rfm, p):
336-
return m.FuelConsumptionInMarket[rfm, p] * enforce_fuel_consumption_scaling_factor \
337-
== enforce_fuel_consumption_scaling_factor * sum(
338-
m.GenFuelUseRate[g, t, m.rfm_fuel[rfm]] * m.tp_weight_in_year[t]
339-
for g in m.GENS_FOR_RFM_PERIOD[rfm, p]
340-
for t in m.TPS_IN_PERIOD[p]
341-
)
341+
lhs = m.FuelConsumptionInMarket[rfm, p] * enforce_fuel_consumption_scaling_factor
342+
rhs = enforce_fuel_consumption_scaling_factor * sum(
343+
m.GenFuelUseRate[g, t, m.rfm_fuel[rfm]] * m.tp_weight_in_year[t] for g in m.GENS_FOR_RFM_PERIOD[rfm, p] for
344+
t in m.TPS_IN_PERIOD[p])
345+
# If we have only positive costs, FuelConsumptionInMarket will automatically
346+
# try to be minimized in which case we can use a one-sided constraint
347+
if ONLY_POSITIVE_RFM_COSTS:
348+
return lhs >= rhs
349+
else:
350+
return lhs == rhs
342351
mod.Enforce_Fuel_Consumption = Constraint(
343352
mod.REGIONAL_FUEL_MARKETS, mod.PERIODS,
344353
rule=Enforce_Fuel_Consumption_rule)

switch_model/generators/core/dispatch.py

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
import os, collections
1414

15+
from pyomo.core.base.misc import sorted_robust
1516
from pyomo.environ import *
1617
import pandas as pd
1718

@@ -234,10 +235,47 @@ def init(m, gen, period):
234235
mod.DispatchGen = Var(
235236
mod.GEN_TPS,
236237
within=NonNegativeReals)
237-
mod.DispatchGenByFuel = Var(mod.GEN_TP_FUELS, within=NonNegativeReals)
238+
239+
##########################################
240+
# Define DispatchGenByFuel
241+
#
242+
# Previously DispatchGenByFuel was simply a Variable for all the projects and a constraint ensured
243+
# that the sum of DispatchGenByFuel across all fuels was equal the total dispatch for that project.
244+
# However this approach creates extra variables in our model for projects that have only one fuel.
245+
# Although these extra variables likely get removed during Gurobi pre-solve, we've nonetheless
246+
# simplified the model here to reduce time in presolve and ensure the model is always
247+
# simplified regardless of the solving method.
248+
#
249+
# To do this we redefine DispatchGenByFuel to be an
250+
# expression that is equal to DispatchGenByFuelVar when we have multiple fuels but
251+
# equal to DispatchGen when we have only one fuel.
252+
253+
# Define a set that is used to define DispatchGenByFuelVar
254+
mod.GEN_TP_FUELS_FOR_MULTIFUELS = Set(
255+
dimen=3,
256+
initialize=mod.GEN_TP_FUELS,
257+
filter=lambda m, g, t, f: g in m.MULTIFUEL_GENS,
258+
doc="Same as GEN_TP_FUELS but only includes multi-fuel projects"
259+
)
260+
# DispatchGenByFuelVar is a variable that exists only for multi-fuel projects.
261+
mod.DispatchGenByFuelVar = Var(mod.GEN_TP_FUELS_FOR_MULTIFUELS, within=NonNegativeReals)
262+
# DispatchGenByFuel_Constraint ensures that the sum of all the fuels is DispatchGen
238263
mod.DispatchGenByFuel_Constraint = Constraint(
239264
mod.FUEL_BASED_GEN_TPS,
240-
rule=lambda m, g, t: sum(m.DispatchGenByFuel[g, t, f] for f in m.FUELS_FOR_GEN[g]) == m.DispatchGen[g, t])
265+
rule=lambda m, g, t:
266+
(Constraint.Skip if g not in m.MULTIFUEL_GENS
267+
else sum(m.DispatchGenByFuelVar[g, t, f] for f in m.FUELS_FOR_MULTIFUEL_GEN[g]) == m.DispatchGen[g, t])
268+
)
269+
270+
# Define DispatchGenByFuel to equal the matching variable if we have many fuels but to equal
271+
# the total dispatch if we have only one fuel.
272+
mod.DispatchGenByFuel = Expression(
273+
mod.GEN_TP_FUELS,
274+
rule=lambda m, g, t, f: m.DispatchGenByFuelVar[g, t, f] if g in m.MULTIFUEL_GENS else m.DispatchGen[g, t]
275+
)
276+
277+
# End Defining DispatchGenByFuel
278+
##########################################
241279

242280
# Only used to improve the performance of calculating ZoneTotalCentralDispatch and ZoneTotalDistributedDispatch
243281
mod.GENS_FOR_ZONE_TPS = Set(
@@ -413,16 +451,18 @@ def post_solve(instance, outdir):
413451
dispatch_annual_summary.pdf - A figure of annual summary data. Only written
414452
if the ggplot python library is installed.
415453
"""
454+
sorted_gen = sorted_robust(instance.GENERATION_PROJECTS)
416455
write_table(
417456
instance, instance.TIMEPOINTS,
418457
output_file=os.path.join(outdir, "dispatch-wide.csv"),
419-
headings=("timestamp",) + tuple(sorted(instance.GENERATION_PROJECTS)),
458+
headings=("timestamp",) + tuple(sorted_gen),
420459
values=lambda m, t: (m.tp_timestamp[t],) + tuple(
421460
m.DispatchGen[p, t] if (p, t) in m.GEN_TPS
422461
else 0.0
423-
for p in sorted(m.GENERATION_PROJECTS)
462+
for p in sorted_gen
424463
)
425464
)
465+
del sorted_gen
426466

427467
def c(func):
428468
return (value(func(g, t)) for g, t in instance.GEN_TPS)

0 commit comments

Comments
 (0)