# -*- coding: utf-8 -*-
"""A command line interface to the system of systems framework
This command line interface implements a number of methods.
- `setup` creates an example project with the recommended folder structure
- `run` performs a simulation of an individual sector model, or the whole system
of systems model
- `validate` performs a validation check of the configuration file
- `app` runs the graphical user interface, opening in a web browser
Folder structure
----------------
When configuring a project for the CLI, the folder structure below should be
used. In this example, there is one system-of-systems model, combining a water
supply and an energy demand model::
/config
project.yaml
/sector_models
energy_demand.yml
water_supply.yml
/sos_models
energy_water.yml
/sos_model_runs
run_to_2050.yml
short_test_run.yml
...
/data
/initial_conditions
reservoirs.yml
/interval_definitions
annual_intervals.csv
/interventions
water_supply.yml
/narratives
high_tech_dsm.yml
/region_definitions
/oxfordshire
regions.geojson
/uk_nations_shp
regions.shp
/scenarios
population.csv
raininess.csv
/water_supply
/initial_system
/models
energy_demand.py
water_supply.py
/planning
expected_to_2020.yaml
national_infrastructure_pipeline.yml
The sector model implementations can be installed independently of the model run
configuration. The paths to python wrapper classes (implementing SectorModel)
should be specified in each ``sector_model/*.yml`` configuration.
The project.yaml file specifies the metadata shared by all elements of the
project; ``sos_models`` specify the combinations of ``sector_models`` and
``scenarios`` while individual ``model_runs`` specify the scenario, strategy
and narrative combinations to be used in each run of the models.
"""
from __future__ import print_function
import logging
import logging.config
import os
import pkg_resources
import re
import shutil
import sys
import time
import traceback
import webbrowser
from argparse import ArgumentParser
from multiprocessing import Process
from threading import Timer
import smif
from smif.data_layer import DatafileInterface, DataNotFoundError
from smif.convert.area import get_register as get_region_register
from smif.convert.area import RegionSet
from smif.convert.interval import get_register as get_interval_register
from smif.convert.interval import IntervalSet
from smif.http_api import create_app
from smif.parameters import Narrative
from smif.modelrun import ModelRunBuilder
from smif.model.sos_model import SosModelBuilder
from smif.model.sector_model import SectorModelBuilder
from smif.model.scenario_model import ScenarioModelBuilder
__author__ = "Will Usher, Tom Russell"
__copyright__ = "Will Usher, Tom Russell"
__license__ = "mit"
LOGGING_CONFIG = {
'version': 1,
'formatters': {
'default': {
'format': '%(asctime)s %(name)-12s: %(levelname)-8s %(message)s'
},
'message': {
'format': '\033[1;34m%(levelname)-8s\033[0m %(message)s'
}
},
'handlers': {
'file': {
'class': 'logging.FileHandler',
'level': 'DEBUG',
'formatter': 'default',
'filename': 'smif.log',
'mode': 'a',
'encoding': 'utf-8'
},
'stream': {
'class': 'logging.StreamHandler',
'formatter': 'message',
'level': 'DEBUG'
}
},
'root': {
'handlers': ['file', 'stream'],
'level': 'DEBUG'
}
}
# Configure logging once, outside of any dependency on argparse
VERBOSITY = None
if '--verbose' in sys.argv:
VERBOSITY = sys.argv.count('--verbose')
else:
for arg in sys.argv:
if re.match(r'\A-v+\Z', arg):
VERBOSITY = len(arg) - 1
break
if VERBOSITY is None:
LOGGING_CONFIG['root']['level'] = logging.WARNING
elif VERBOSITY == 1:
LOGGING_CONFIG['root']['level'] = logging.INFO
else:
LOGGING_CONFIG['root']['level'] = logging.DEBUG
logging.config.dictConfig(LOGGING_CONFIG)
LOGGER = logging.getLogger(__name__)
LOGGER.debug('Debug logging enabled.')
REGIONS = get_region_register()
INTERVALS = get_interval_register()
[docs]def setup_project_folder(args):
"""Creates folder structure in the target directory
Parameters
----------
args
"""
_recursive_overwrite('smif', 'sample_project', args.directory)
if args.directory == ".":
dirname = "the current directory"
else:
dirname = args.directory
LOGGER.info("Created sample project in %s", dirname)
def _recursive_overwrite(pkg, src, dest):
if pkg_resources.resource_isdir(pkg, src):
if not os.path.isdir(dest):
os.makedirs(dest)
contents = pkg_resources.resource_listdir(pkg, src)
for item in contents:
_recursive_overwrite(pkg,
os.path.join(src, item),
os.path.join(dest, item))
else:
filename = pkg_resources.resource_filename(pkg, src)
shutil.copyfile(filename, dest)
[docs]def load_region_sets(handler):
"""Loads the region sets into the project registries
Parameters
----------
handler: :class:`smif.data_layer.DataInterface`
"""
region_definitions = handler.read_region_definitions()
for region_def in region_definitions:
region_name = region_def['name']
region_data = handler.read_region_definition_data(region_name)
region_set = RegionSet(region_name, region_data)
REGIONS.register(region_set)
[docs]def load_interval_sets(handler):
"""Loads the time-interval sets into the project registries
Parameters
----------
handler: :class:`smif.data_layer.DataInterface`
"""
interval_definitions = handler.read_interval_definitions()
for interval_def in interval_definitions:
interval_name = interval_def['name']
interval_data = handler.read_interval_definition_data(interval_name)
interval_set = IntervalSet(interval_name, interval_data)
INTERVALS.register(interval_set)
[docs]def get_model_run_definition(args):
"""Builds the model run
Returns
-------
dict
The complete sos_model_run configuration dictionary with contained
ScenarioModel, SosModel and SectorModel objects
"""
handler = DatafileInterface(args.directory)
load_region_sets(handler)
load_interval_sets(handler)
try:
model_run_config = handler.read_sos_model_run(args.modelrun)
except DataNotFoundError:
LOGGER.error("Model run %s not found. Run 'smif list' to see available model runs.",
args.modelrun)
exit(-1)
LOGGER.info("Running %s", model_run_config['name'])
LOGGER.debug("Model Run: %s", model_run_config)
sos_model_config = handler.read_sos_model(model_run_config['sos_model'])
sector_model_objects = []
for sector_model in sos_model_config['sector_models']:
sector_model_config = handler.read_sector_model(sector_model)
absolute_path = os.path.join(args.directory,
sector_model_config['path'])
sector_model_config['path'] = absolute_path
intervention_files = sector_model_config['interventions']
intervention_list = []
for intervention_file in intervention_files:
interventions = handler.read_interventions(intervention_file)
intervention_list.extend(interventions)
sector_model_config['interventions'] = intervention_list
initial_condition_files = sector_model_config['initial_conditions']
initial_condition_list = []
for initial_condition_file in initial_condition_files:
initial_conditions = handler.read_initial_conditions(initial_condition_file)
initial_condition_list.extend(initial_conditions)
sector_model_config['initial_conditions'] = initial_condition_list
sector_model_builder = SectorModelBuilder(sector_model_config['name'])
LOGGER.debug("Sector model config: %s", sector_model_config)
sector_model_builder.construct(sector_model_config,
model_run_config['timesteps'])
sector_model_object = sector_model_builder.finish()
sector_model_objects.append(sector_model_object)
LOGGER.debug("Model inputs: %s", sector_model_object.inputs.names)
LOGGER.debug("Sector models: %s", sector_model_objects)
sos_model_config['sector_models'] = sector_model_objects
scenario_objects = []
for scenario_set, scenario_name in model_run_config['scenarios'].items():
scenario_definition = handler.read_scenario_definition(scenario_name)
LOGGER.debug("Scenario definition: %s", scenario_definition)
scenario_model_builder = ScenarioModelBuilder(scenario_set)
scenario_model_builder.construct(scenario_definition)
scenario_objects.append(scenario_model_builder.finish())
LOGGER.debug("Scenario models: %s", [model.name for model in scenario_objects])
sos_model_config['scenario_sets'] = scenario_objects
sos_model_builder = SosModelBuilder()
sos_model_builder.construct(sos_model_config)
sos_model_object = sos_model_builder.finish()
LOGGER.debug("Model list: %s", list(sos_model_object.models.keys()))
model_run_config['sos_model'] = sos_model_object
narrative_objects = get_narratives(handler,
model_run_config['narratives'])
model_run_config['narratives'] = narrative_objects
return model_run_config
[docs]def get_narratives(handler, narrative_config):
"""Load the narrative data from the sos model run configuration
Arguments
---------
handler: :class:`smif.data_layer.DataInterface`
narrative_config: dict
A dict with keys as narrative_set names and values as narrative names
Returns
-------
list
A list of :class:`smif.parameter.Narrative` objects populated with
data
"""
narrative_objects = []
for narrative_set, narrative_names in narrative_config.items():
LOGGER.info("Loading narrative data for narrative set '%s'",
narrative_set)
for narrative_name in narrative_names:
LOGGER.debug("Adding narrative entry '%s'", narrative_name)
definition = handler.read_narrative_definition(narrative_name)
narrative = Narrative(
narrative_name,
definition['description'],
narrative_set
)
narrative.data = handler.read_narrative_data(narrative_name)
narrative_objects.append(narrative)
return narrative_objects
[docs]def list_model_runs(args):
"""List the model runs defined in the config
"""
handler = DatafileInterface(args.directory)
model_run_configs = handler.read_sos_model_runs()
for run in model_run_configs:
print(run['name'])
[docs]def build_model_run(model_run_config):
"""Builds the model run
Arguments
---------
model_run_config: dict
A valid model run configuration dict with objects
Returns
-------
`smif.modelrun.ModelRun`
"""
try:
builder = ModelRunBuilder()
builder.construct(model_run_config)
modelrun = builder.finish()
except AssertionError as error:
err_type, err_value, err_traceback = sys.exc_info()
traceback.print_exception(err_type, err_value, err_traceback)
err_msg = str(error)
if err_msg:
LOGGER.error("An AssertionError occurred (%s) see details above.", err_msg)
else:
LOGGER.error("An AssertionError occurred, see details above.")
exit(-1)
return modelrun
[docs]def execute_model_run(args):
"""Runs the model run
Parameters
----------
args
"""
LOGGER.info("Getting model run definition")
model_run_config = get_model_run_definition(args)
LOGGER.info("Build model run from configuration data")
modelrun = build_model_run(model_run_config)
LOGGER.info("Running model run %s", modelrun.name)
store = DatafileInterface(args.directory)
modelrun.run(store)
print("Model run complete")
[docs]def make_get_data_interface(args):
"""Use args to make a function returning a suitable DataInterface
"""
def getter():
"""Return a DataInterface
"""
return DatafileInterface(args.directory)
return getter
def _open_browser():
print(" * Opening page in browser...")
webbrowser.open("http://localhost:5000/")
def _run_server(args):
app_folder = pkg_resources.resource_filename('smif', 'app/dist')
get_data_interface = make_get_data_interface(args)
app = create_app(
static_folder=app_folder,
template_folder=app_folder,
get_data_interface=get_data_interface
)
app.run()
[docs]def run_app(args):
"""Run the client/server application
Parameters
----------
args
"""
print("Opening smif application")
# avoid one of two error messages from 'forrtl error(200)' when running
# on windows cmd - seems related to scipy's underlying Fortran
os.environ['FOR_DISABLE_CONSOLE_CTRL_HANDLER'] = 'T'
# Create backend server process
server = Process(target=_run_server, args=(args,))
try:
print(" * Type CTRL-C to stop")
server.start()
Timer(2, _open_browser).start()
while True:
time.sleep(0.2)
except KeyboardInterrupt:
print(" * Stopping...")
server.terminate()
server.join()
[docs]def parse_arguments():
"""Parse command line arguments
Returns
=======
:class:`argparse.ArgumentParser`
"""
parser = ArgumentParser(description='Command line tools for smif')
parser.add_argument('-V', '--version',
action='version',
version="smif " + smif.__version__,
help='show the current version of smif')
parser.add_argument('-v', '--verbose',
action='count',
help='show messages: -v to see messages reporting on progress, ' +
'-vv to see debug messages.')
subparsers = parser.add_subparsers(help='available commands')
# SETUP
parser_setup = subparsers.add_parser('setup',
help='Setup the project folder')
parser_setup.set_defaults(func=setup_project_folder)
parser_setup.add_argument('-d', '--directory',
default='.',
help="Path to the project directory")
# LIST
parser_list = subparsers.add_parser('list',
help='List available model runs')
parser_list.set_defaults(func=list_model_runs)
parser_list.add_argument('-d', '--directory',
default='.',
help="Path to the project directory")
# APP
parser_app = subparsers.add_parser('app',
help='Open smif app')
parser_app.set_defaults(func=run_app)
parser_app.add_argument('-d', '--directory',
default='.',
help="Path to the project directory")
# RUN
parser_run = subparsers.add_parser('run',
help='Run a model')
parser_run.set_defaults(func=execute_model_run)
parser_run.add_argument('-d', '--directory',
default='.',
help="Path to the project directory")
parser_run.add_argument('modelrun',
help="Name of the model run to run")
return parser
[docs]def confirm(prompt=None, response=False):
"""Prompts for a yes or no response from the user
Arguments
---------
prompt : str, default=None
response : bool, default=False
Returns
-------
bool
True for yes and False for no.
Notes
-----
`response` should be set to the default value assumed by the caller when
user simply types ENTER.
Examples
--------
>>> confirm(prompt='Create Directory?', response=True)
Create Directory? [y]|n:
True
>>> confirm(prompt='Create Directory?', response=False)
Create Directory? [n]|y:
False
>>> confirm(prompt='Create Directory?', response=False)
Create Directory? [n]|y: y
True
"""
if prompt is None:
prompt = 'Confirm'
if response:
prompt = '{} [{}]|{}: '.format(prompt, 'y', 'n')
else:
prompt = '{} [{}]|{}: '.format(prompt, 'n', 'y')
while True:
ans = input(prompt)
if not ans:
return response
if ans not in ['y', 'Y', 'n', 'N']:
print('please enter y or n.')
continue
if ans in ['y', 'Y']:
return True
if ans in ['n', 'N']:
return False
[docs]def main(arguments=None):
"""Parse args and run
"""
parser = parse_arguments()
args = parser.parse_args(arguments)
if 'func' in args:
args.func(args)
else:
parser.print_help()
if __name__ == '__main__':
main(sys.argv[1:])