Skip to content

Commit 3981a82

Browse files
authored
Merge pull request #70 from staadecker/info-file
Add framework for a results.txt file
2 parents 1f54be2 + b7fa68e commit 3981a82

File tree

6 files changed

+99
-50
lines changed

6 files changed

+99
-50
lines changed

docs/Contribute.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,36 @@ Read [`docs/Graphs.md`](./Graphs.md) to see learn to add graphs.
4444
## Modifying the database
4545

4646
Read [`docs/Database.md`](./Database.md) to learn about the database.
47+
48+
## Outputting results
49+
50+
Once the model is solved, the `post_solve()` function in each module is called.
51+
Within the `post_solve()` function you may
52+
53+
- Call `write_table()` to create
54+
a .csv file with data from the solution (see existing modules for examples).
55+
56+
- Call `add_info()` (from `utilities/result_info.py`) to add a line
57+
of information to the `outputs/info.txt` file. `add_info()` can also be added to `graph()`.
58+
59+
### Example
60+
61+
```python
62+
from switch_model.utilities.results_info import add_info
63+
from switch_model.reporting import write_table
64+
import os
65+
...
66+
def post_solve(instance, outdir):
67+
...
68+
# This will add the a line to info.txt in the outputs folder
69+
add_info("Some important value", instance.important_value)
70+
...
71+
# This will create my_table.csv
72+
write_table(
73+
instance,
74+
instance.TIMEPOINTS, # Set that the values function will iterate over
75+
output_file=os.path.join(outdir, "my_table.csv"),
76+
headings=("timepoint", "some value"),
77+
values=lambda m, t: (t, m.some_value[t])
78+
)
79+
```

switch_model/generators/core/dispatch.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -589,7 +589,10 @@ def graph_curtailment_per_tech(tools):
589589
# Get axes to graph on
590590
ax = tools.get_new_axes(out="curtailment_per_period", title="Percent of total dispatchable capacity curtailed")
591591
# Plot
592-
df.plot(ax=ax, kind='line', color=tools.get_colors(), xlabel='Period')
592+
color = tools.get_colors()
593+
kwargs = dict() if color is None else dict(color=color)
594+
df.plot(ax=ax, kind='line', xlabel='Period', **kwargs)
595+
593596
# Set the y-axis to use percent
594597
ax.yaxis.set_major_formatter(tools.mplt.ticker.PercentFormatter(1.0))
595598
# Horizontal line at 100%

switch_model/reporting/__init__.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
2020
"""
2121
from __future__ import print_function
22-
from switch_model.utilities import string_types
22+
from switch_model.utilities import string_types, add_info
2323
from switch_model.utilities.scaling import get_unscaled_var
2424
dependencies = 'switch_model.financials'
2525

@@ -208,8 +208,10 @@ def get_value(obj):
208208

209209

210210
def save_total_cost_value(instance, outdir):
211+
total_cost = round(value(instance.SystemCost), ndigits=2)
212+
add_info("Total Cost", f"$ {total_cost}")
211213
with open(os.path.join(outdir, 'total_cost.txt'), 'w') as fh:
212-
fh.write('{}\n'.format(round(value(instance.SystemCost), ndigits=2)))
214+
fh.write(f'{total_cost}\n')
213215

214216

215217
def save_cost_components(m, outdir):

switch_model/solve.py

Lines changed: 22 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,19 @@
1010

1111
import sys, os, shlex, re, inspect, textwrap, types, pickle, traceback, gc
1212
import warnings
13+
import datetime
14+
import platform
15+
16+
from pyomo.solvers.plugins.solvers.direct_or_persistent_solver import DirectOrPersistentSolver
1317

1418
import switch_model
1519
from switch_model.utilities import (
16-
create_model, _ArgumentParser, StepTimer, make_iterable, LogOutput, warn, query_yes_no, create_info_file,
17-
get_module_list, add_module_args, _ScaledVariable
20+
create_model, _ArgumentParser, StepTimer, make_iterable, LogOutput, warn, query_yes_no,
21+
get_module_list, add_module_args, _ScaledVariable, add_git_info
1822
)
1923
from switch_model.upgrade import do_inputs_need_upgrade, upgrade_inputs
2024
from switch_model.tools.graphing import graph
25+
from switch_model.utilities.results_info import save_info, add_info, ResultsInfoSection
2126

2227

2328
def main(args=None, return_model=False, return_instance=False, attach_data_portal=False):
@@ -101,6 +106,9 @@ def debug(type, value, tb):
101106
if not os.path.isdir(model.options.outputs_dir):
102107
raise IOError("Directory specified for prior solution does not exist.")
103108

109+
add_info("Host name", platform.node(), section=ResultsInfoSection.GENERAL)
110+
add_git_info()
111+
104112
# get a list of modules to iterate through
105113
iterate_modules = get_iteration_list(model)
106114

@@ -109,7 +117,7 @@ def debug(type, value, tb):
109117
print("Switch {}, http://switch-model.org".format(switch_model.__version__))
110118
print("=======================================================================")
111119
print("Arguments:")
112-
print(", ".join(k+"="+repr(v) for k, v in model.options.__dict__.items() if v))
120+
print(", ".join(k + "=" + repr(v) for k, v in model.options.__dict__.items() if v))
113121
print("Modules:\n"+", ".join(m for m in modules))
114122
if iterate_modules:
115123
print("Iteration modules:", iterate_modules)
@@ -186,6 +194,7 @@ def debug(type, value, tb):
186194
# (repeated if model is reloaded, to automatically run any new export code)
187195
if not instance.options.no_post_solve:
188196
if instance.options.verbose:
197+
timer.step_time()
189198
print("Executing post solve functions...")
190199
instance.post_solve()
191200
if instance.options.verbose:
@@ -195,7 +204,15 @@ def debug(type, value, tb):
195204
graph.main(args=["--overwrite"])
196205

197206
total_time = start_to_end_timer.step_time_as_str()
198-
create_info_file(getattr(instance.options, "outputs_dir", "outputs"), run_time=total_time)
207+
add_info("Total run time", total_time, section=ResultsInfoSection.GENERAL)
208+
209+
add_info("End date", datetime.datetime.now().strftime('%Y-%m-%d'), section=ResultsInfoSection.GENERAL)
210+
add_info("End time", datetime.datetime.now().strftime('%H:%M:%S'), section=ResultsInfoSection.GENERAL)
211+
212+
save_info(
213+
os.path.join(getattr(instance.options, "outputs_dir", "outputs"),
214+
"info.txt")
215+
)
199216

200217
if instance.options.verbose:
201218
print(f"Total time spent running SWITCH: {total_time}.")
@@ -749,6 +766,7 @@ def solve(model):
749766
keepfiles=model.options.keepfiles,
750767
tee=model.options.tee,
751768
symbolic_solver_labels=model.options.symbolic_solver_labels,
769+
save_results=model.options.save_solution if isinstance(solver, DirectOrPersistentSolver) else None,
752770
)
753771

754772
if model.options.warm_start is not None:
@@ -811,30 +829,6 @@ def solve(model):
811829
print('solver_options_string and calling this script with "--suffixes iis" or "--gurobi-find-iis".')
812830
raise RuntimeError("Infeasible model")
813831

814-
# Raise an error if the solver failed to produce a solution
815-
# Note that checking for results.solver.status in {SolverStatus.ok,
816-
# SolverStatus.warning} is not enough because with a warning there will
817-
# sometimes be a solution and sometimes not.
818-
# Note: the results object originally contains values for model components
819-
# in results.solution.variable, etc., but pyomo.solvers.solve erases it via
820-
# result.solution.clear() after calling model.solutions.load_from() with it.
821-
# load_from() loads values into the model.solutions._entry, so we check there.
822-
# (See pyomo.PyomoModel.ModelSolutions.add_solution() for the code that
823-
# actually creates _entry).
824-
# Another option might be to check that model.solutions[-1].status (previously
825-
# result.solution.status, but also cleared) is in
826-
# pyomo.opt.SolutionStatus.['optimal', 'bestSoFar', 'feasible', 'globallyOptimal', 'locallyOptimal'],
827-
# but this seems pretty foolproof (if undocumented).
828-
if len(model.solutions[-1]._entry['variable']) == 0:
829-
# no solution returned
830-
print("Solver terminated without a solution.")
831-
print(" Solver Status: ", results.solver.status)
832-
print(" Solution Status: ", model.solutions[-1].status)
833-
print(" Termination Condition: ", results.solver.termination_condition)
834-
if model.options.solver == 'glpk' and results.solver.termination_condition == TerminationCondition.other:
835-
print("Hint: glpk has been known to classify infeasible problems as 'other'.")
836-
raise RuntimeError("Solver failed to find an optimal solution.")
837-
838832
# Report any warnings; these are written to stderr so users can find them in
839833
# error logs (e.g. on HPC systems). These can occur, e.g., if solver reaches
840834
# time limit or iteration limit but still returns a valid solution

switch_model/utilities/__init__.py

Lines changed: 9 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,15 @@
77
from __future__ import print_function
88

99
import os, types, sys, argparse, time, datetime, traceback, subprocess, platform
10+
import warnings
1011

1112
import switch_model.__main__ as main
1213
from pyomo.environ import *
1314
from pyomo.core.base.set import UnknownSetDimen
1415
from pyomo.dataportal import DataManagerFactory
1516
from pyomo.dataportal.plugins.csv_table import CSVTable
17+
18+
from switch_model.utilities.results_info import add_info, ResultsInfoSection
1619
from switch_model.utilities.scaling import _ScaledVariable, _get_unscaled_expression
1720
import pyomo.opt
1821

@@ -883,27 +886,14 @@ def query_yes_no(question, default="yes"):
883886
def run_command(command):
884887
return subprocess.check_output(command.split(" "), cwd=os.path.dirname(__file__)).strip().decode("UTF-8")
885888

886-
def get_git_hash():
887-
return run_command("git rev-parse HEAD")
888-
889-
def get_git_branch():
890-
return run_command("git rev-parse --abbrev-ref HEAD")
891-
892-
def create_info_file(output_path, run_time=None):
893-
content = ""
894-
content += f"End date: {datetime.datetime.now().strftime('%Y-%m-%d')}\n"
895-
content += f"End time: {datetime.datetime.now().strftime('%H:%M:%S')}\n"
889+
def add_git_info():
896890
try:
897-
content += f"SWITCH Git Commit Hash: {get_git_hash()}\n"
898-
content += f"SWITCH Git Branch: {get_git_branch()}\n"
891+
commit_num = run_command("git rev-parse HEAD")
892+
branch = run_command("git rev-parse --abbrev-ref HEAD")
893+
add_info("Git Commit", commit_num, section=ResultsInfoSection.GENERAL)
894+
add_info("Git Branch", branch, section=ResultsInfoSection.GENERAL)
899895
except:
900-
print("Warning: failed to get commit hash or branch for info.txt.")
901-
if run_time is not None:
902-
content += f"Run time: {run_time}\n"
903-
content += f"Host name: {platform.node()}\n"
904-
with open(os.path.join(output_path, "info.txt"), "w") as f:
905-
f.write(content)
906-
896+
warnings.warn("Failed to get Git Branch or Commit Hash for info.txt.")
907897

908898
def get_module_list(args=None, include_solve_module=True):
909899
# parse module options
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import enum
2+
3+
4+
class ResultsInfoSection(enum.Enum):
5+
GENERAL = "1. General"
6+
RESULTS = "2. Results"
7+
8+
9+
info = {}
10+
11+
12+
def add_info(name: str, value="", section=ResultsInfoSection.RESULTS):
13+
if section not in info:
14+
info[section] = []
15+
16+
info[section].append(
17+
str(name) + ": " + str(value)
18+
)
19+
20+
21+
def save_info(filepath):
22+
with open(filepath, "w") as f:
23+
for section, rows in sorted(info.items(), key=lambda x: x[0].value):
24+
f.write(f"##########\n"
25+
f"{section.value}\n"
26+
f"##########\n\n")
27+
f.write("\n".join(rows) + "\n\n")

0 commit comments

Comments
 (0)