Skip to content

Commit 2f1a546

Browse files
pesapstaadecker
authored andcommitted
Merge pull request #94 from staadecker/save_scenario
Add script to save new scenarios to the database
2 parents 610e1ae + 3f5189b commit 2f1a546

File tree

6 files changed

+164
-11
lines changed

6 files changed

+164
-11
lines changed

docs/Database.md

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,18 @@ The command to enter PostgreSQL while SSH'd into the server is `psql wecc`.
4141

4242
## Making changes to the database
4343

44-
Whether it's adding data to the database or changing its schema, it's important
45-
to proceed carefully when making changes to the database. Always make sure to
44+
### Common operations
45+
46+
Some database changes are common operations that are used repeatedly.
47+
For these operations, it's helpful to make a command line tool to make the process easy.
48+
49+
For example, adding a scenario to the database is a common operation. Therefore, we have built
50+
the `switch db save_scenario` command to easily do this. (Run `switch db save_scenario --help` for details).
51+
52+
### Custom operations
53+
54+
When running custom operations on the database it's very important
55+
to proceed carefully. Always make sure to
4656
**test and keep track of your changes**.
4757

4858
Here are the steps to make a change.
@@ -61,9 +71,7 @@ to the convention `YYYY-MM-DD_<script_name>`.
6171
5. Open a pull request to add your script to the repository (see [`docs/Contribute.md`](Contribute.md))
6272
so we can keep track of the changes that have been made.
6373

64-
### Bigger changes
65-
66-
Sometimes, it isn't feasible to have your entire change as a single SQL script.
74+
Note that sometimes, it isn't feasible to have your entire change as a single SQL script.
6775
One way to make bigger changes is to use a Python script. Use the same process
6876
as for the SQL scripts. That is save your Python scripts in the `/database` folder.
6977

switch_model/tools/templates/config.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ get_inputs:
2121
# the following parameters are optional and will override the defaults for that scenario
2222
# this should only be used for preliminary exploration of scenarios or for testing
2323
# if a scenario is part of research, it should be included in the database as a row in the scenarios table.
24+
#
25+
# -1 is equivalent to null
2426
# study_timeframe_id:
2527
# time_sample_id:
2628
# demand_scenario_id:

switch_model/wecc/__main__.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,25 @@
55
from __future__ import print_function
66

77
import argparse
8+
import importlib
89
import sys
9-
import switch_model.wecc.sampling.cli as sample
10+
11+
12+
def get_module_runner(module):
13+
def runner():
14+
importlib.import_module(module).main()
15+
16+
return runner
1017

1118

1219
cmds = {
13-
"sample": sample.main,
20+
"sample": get_module_runner("switch_model.wecc.sampling.cli"),
21+
"save_scenario": get_module_runner("switch_model.wecc.save_scenario"),
1422
}
1523

1624

1725
def main(args=None):
18-
parser = argparse.ArgumentParser()
26+
parser = argparse.ArgumentParser(add_help=False)
1927
parser.add_argument(
2028
"subcommand", choices=cmds.keys(), help="The possible switch subcommands"
2129
)
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
class ScenarioParams:
2+
def __init__(self, scenario_id=None):
3+
# This list of attributes defines the columns that we query from the database.
4+
# Therefore attribute names matter!
5+
self.scenario_id = scenario_id
6+
self.name = None
7+
self.description = None
8+
self.study_timeframe_id = None
9+
self.time_sample_id = None
10+
self.demand_scenario_id = None
11+
self.fuel_simple_price_scenario_id = None
12+
self.generation_plant_scenario_id = None
13+
self.generation_plant_cost_scenario_id = None
14+
self.generation_plant_existing_and_planned_scenario_id = None
15+
self.hydro_simple_scenario_id = None
16+
self.carbon_cap_scenario_id = None
17+
self.supply_curves_scenario_id = None
18+
self.regional_fuel_market_scenario_id = None
19+
self.rps_scenario_id = None
20+
self.enable_dr = None
21+
self.enable_ev = None
22+
self.transmission_base_capital_cost_scenario_id = None
23+
self.ca_policies_scenario_id = None
24+
self.enable_planning_reserves = None
25+
self.generation_plant_technologies_scenario_id = None
26+
self.variable_o_m_cost_scenario_id = None
27+
self.wind_to_solar_ratio = None
28+
29+
30+
def load_scenario_from_config(config, db_cursor) -> ScenarioParams:
31+
config = config["get_inputs"] # Only keep the config that matters
32+
33+
# Create the ScenarioParams object
34+
params = ScenarioParams(scenario_id=config["scenario_id"])
35+
param_names = list(params.__dict__.keys())
36+
print(param_names)
37+
38+
# Read from the database all the parameters.
39+
db_cursor.execute(
40+
f"""SELECT {",".join(param_names)}
41+
FROM scenario
42+
WHERE scenario_id = {params.scenario_id};"""
43+
)
44+
db_values = list(db_cursor.fetchone())
45+
46+
# Allow overriding from config
47+
for i, param_name in enumerate(param_names):
48+
value = config[param_name] if param_name in config else db_values[i]
49+
if value == -1:
50+
value = None
51+
setattr(params, param_name, value)
52+
53+
return params

switch_model/wecc/save_scenario.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import argparse
2+
import sys
3+
4+
import yaml
5+
6+
from switch_model.utilities import query_yes_no
7+
from switch_model.wecc.get_inputs.scenario import load_scenario_from_config
8+
from switch_model.wecc.utilities import connect, load_config
9+
10+
11+
def main():
12+
# Start CLI
13+
parser = argparse.ArgumentParser(
14+
description="Creates a new scenario in the database by using the values in"
15+
" config.yaml. Therefore the new scenario will have the same values"
16+
" as the base scenario but you can override specific columns by "
17+
" specifying them in config.yaml."
18+
)
19+
parser.add_argument(
20+
"scenario_id", type=int, help="The id of the new scenario to add to db."
21+
)
22+
parser.add_argument(
23+
"--name", required=True, help="The name of the new scenario to add in db."
24+
)
25+
parser.add_argument(
26+
"--description",
27+
required=True,
28+
help="The new scenario description to add in db.",
29+
)
30+
parser.add_argument(
31+
"--db-env-var", default="DB_URL", help="The connection environment variable."
32+
)
33+
34+
# Optional arguments
35+
parser.add_argument(
36+
"--config_file",
37+
default="config.yaml",
38+
type=str,
39+
help="Configuration file to use.",
40+
)
41+
42+
args = parser.parse_args()
43+
44+
# Start db connection
45+
db_conn = connect(connection_env_var=args.db_env_var)
46+
db_cursor = db_conn.cursor()
47+
48+
# # Exit if you are not sure if you want to overwrite
49+
# if args.overwrite:
50+
51+
config = load_config()
52+
scenario_params = load_scenario_from_config(config, db_cursor)
53+
54+
# Override the given parameters
55+
scenario_params.name = args.name
56+
scenario_params.description = args.description
57+
scenario_params.scenario_id = args.scenario_id
58+
59+
ordered_params = list(
60+
filter(lambda v: v[1] is not None, scenario_params.__dict__.items())
61+
)
62+
columns = ",".join(v[0] for v in ordered_params)
63+
values = ",".join(
64+
f"'{v[1]}'" if type(v[1]) == str else str(v[1]) for v in ordered_params
65+
)
66+
67+
query = f"""INSERT INTO scenario({columns}) VALUES ({values});"""
68+
69+
print(f"\n{query}\n")
70+
71+
if not query_yes_no(
72+
f"Are you sure you want to run the above query.?", default="no"
73+
):
74+
sys.exit()
75+
76+
db_cursor.execute(query)
77+
db_conn.commit()
78+
db_cursor.close()
79+
db_conn.close()
80+
81+
print(f"Ran query.")

switch_model/wecc/utilities.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def load_dotenv():
2525
pass
2626

2727

28-
def connect(schema="switch"):
28+
def connect(schema="switch", connection_env_var="DB_URL"):
2929
"""Connects to the Postgres DB
3030
3131
This function uses the environment variables to get the URL to connect to the DB. Both
@@ -34,16 +34,17 @@ def connect(schema="switch"):
3434
Parameters
3535
----------
3636
schema: str Schema of the DB to look for tables. Default is switch
37+
connection_env_var: The environment variable to use as the connection string
3738
3839
Returns
3940
-------
4041
conn: Database connection object from psycopg2
4142
"""
4243
load_dotenv()
43-
db_url = os.getenv("DB_URL")
44+
db_url = os.getenv(connection_env_var)
4445
if db_url is None:
4546
raise Exception(
46-
"Please set the environment variable 'DB_URL' to the database URL."
47+
f"Please set the environment variable '{connection_env_var}' to the database URL."
4748
"The format is normally: postgresql://<user>:<password>@<host>:5432/<database>"
4849
)
4950

0 commit comments

Comments
 (0)