332 lines
10 KiB
Python
332 lines
10 KiB
Python
"""
|
|
cmdline parser
|
|
"""
|
|
import os
|
|
import re
|
|
from ast import literal_eval
|
|
import numpy as np
|
|
import pandas as pd
|
|
from argparse import ArgumentParser, Action
|
|
|
|
from pysd import __version__
|
|
from pysd.translators.vensim.vensim_utils import supported_extensions as\
|
|
vensim_extensions
|
|
from pysd.translators.xmile.xmile_utils import supported_extensions as\
|
|
xmile_extensions
|
|
|
|
docs = "https://pysd.readthedocs.io/en/master/command_line_usage.html"
|
|
|
|
parser = ArgumentParser(
|
|
description='Simulating System Dynamics Models in Python',
|
|
prog='PySD')
|
|
|
|
|
|
#########################
|
|
# functions and actions #
|
|
#########################
|
|
|
|
def check_output(string):
|
|
"""
|
|
Checks that out put file ends with .tab or .csv
|
|
|
|
"""
|
|
if not string.endswith(('.tab', '.csv', '.nc')):
|
|
parser.error(
|
|
f'when parsing {string}'
|
|
'\nThe output file name must be .tab, .csv or .nc...')
|
|
|
|
return string
|
|
|
|
|
|
def check_model(string):
|
|
"""
|
|
Checks that model file ends with .py .mdl or .xmile and that exists.
|
|
|
|
"""
|
|
suffixes = [".py"] + vensim_extensions + xmile_extensions
|
|
if not any(string.lower().endswith(suffix) for suffix in suffixes):
|
|
parser.error(
|
|
f"when parsing {string} \nThe model file name must be a Vensim"
|
|
f" ({', '.join(vensim_extensions)}), a Xmile "
|
|
f"({', '.join(xmile_extensions)}) or a PySD (.py) model file...")
|
|
|
|
if not os.path.isfile(string):
|
|
parser.error(
|
|
f"when parsing {string}"
|
|
"\nThe model file does not exist...")
|
|
|
|
return string
|
|
|
|
|
|
def check_data_file(string):
|
|
"""
|
|
Check that data file is a tab or csv file and that exists.
|
|
"""
|
|
if not string.endswith(('.tab', '.csv', '.nc')):
|
|
parser.error(
|
|
f'when parsing {string}'
|
|
'\nThe data file name must be .tab, .csv or .nc...')
|
|
elif not os.path.isfile(string):
|
|
parser.error(
|
|
f'when parsing {string}'
|
|
'\nThe data file does not exist...')
|
|
else:
|
|
return string
|
|
|
|
|
|
def split_files(string):
|
|
"""
|
|
Splits the data files and returns and error if file doesn't exists
|
|
--data 'file1.tab, file2.csv' -> ['file1.tab', 'file2.csv']
|
|
--data file1.tab -> ['file1.tab']
|
|
|
|
"""
|
|
return [check_data_file(s.strip()) for s in string.split(',')]
|
|
|
|
|
|
def split_columns(string):
|
|
"""
|
|
Splits the return-columns argument or reads it from .txt
|
|
--return-columns 'temperature c, "heat$"' -> ['temperature c', '"heat$"']
|
|
--return-columns my_vars.txt -> ['temperature c', '"heat$"']
|
|
|
|
"""
|
|
if string.endswith('.txt'):
|
|
with open(string, 'r') as file:
|
|
return [col.rstrip('\n').strip() for col in file]
|
|
|
|
return [s.strip() for s in string.split(',')]
|
|
|
|
|
|
def split_timestamps(string):
|
|
"""
|
|
Splits the return-timestamps argument
|
|
--return-timestamps '1, 5, 15' -> array([1., 5., 15.])
|
|
|
|
"""
|
|
try:
|
|
return np.array([s.strip() for s in string.split(',')], dtype=float)
|
|
except Exception:
|
|
# error
|
|
raise parser.error(
|
|
f'when parsing {string}'
|
|
'\nThe return time stamps must be separated by commas...\n'
|
|
f'See {docs} for examples.')
|
|
|
|
|
|
def split_vars(string):
|
|
"""
|
|
Splits the arguments from new_values.
|
|
'a=5' -> {'a': ('param', 5.)}
|
|
'b=[[1,2],[1,10]]' -> {'b': ('param', pd.Series(index=[1,2], data=[1,10]))}
|
|
'a:5' -> {'a': ('initial', 5.)}
|
|
|
|
"""
|
|
try:
|
|
if '=' in string:
|
|
# new variable value
|
|
var, value = string.split('=')
|
|
type = 'param'
|
|
|
|
if ':' in string:
|
|
# initial time value
|
|
var, value = string.split(':')
|
|
type = 'initial'
|
|
|
|
if re.match(r"^[+-]?(\d*\.)?\d+$", value.strip()):
|
|
# value is a number
|
|
return {var.strip(): (type, float(value))}
|
|
|
|
# value is series
|
|
assert type == 'param'
|
|
value = literal_eval(value)
|
|
assert len(value) == 2
|
|
assert len(value[0]) == len(value[1])
|
|
return {var.strip(): (type,
|
|
pd.Series(index=value[0], data=value[1]))}
|
|
|
|
except Exception:
|
|
# error
|
|
raise parser.error(
|
|
f'when parsing {string}'
|
|
'\nYou must use variable=new_value to redefine values or '
|
|
'variable:initial_value to define initial value.'
|
|
'variable must be a model component, new_value can be a '
|
|
'float or a list of two list, initial_value must be a float'
|
|
'...\n'
|
|
f'See {docs} for examples.')
|
|
|
|
|
|
class SplitVarsAction(Action):
|
|
"""
|
|
Convert the list of split variables from new_values to a dictionary.
|
|
[{'a': 5.}, {'b': pd.Series(index=[1, 2], data=[1, 10])}] ->
|
|
{'a': 5., 'b': pd.Series(index=[1, 2], data=[1, 10])}
|
|
"""
|
|
def __init__(self, option_strings, dest, **kwargs):
|
|
super().__init__(option_strings, dest, **kwargs)
|
|
|
|
def __call__(self, parser, namespace, values, option_string=None):
|
|
main_dict = {'param': {}, 'initial': {}}
|
|
for var in values:
|
|
for var_name, (type, val) in var.items():
|
|
main_dict[type][var_name] = val
|
|
setattr(namespace, self.dest, main_dict)
|
|
|
|
|
|
###########
|
|
# options #
|
|
###########
|
|
|
|
parser.add_argument(
|
|
'-v', '--version',
|
|
action='version', version=f'PySD {__version__}')
|
|
|
|
parser.add_argument(
|
|
'-o', '--output-file', dest='output_file',
|
|
type=check_output, metavar='FILE',
|
|
help='output file to save run outputs (.tab, .csv or .nc)')
|
|
|
|
parser.add_argument(
|
|
'-p', '--progress', dest='progress',
|
|
action='store_true', default=False,
|
|
help='show progress bar during model integration')
|
|
|
|
parser.add_argument(
|
|
'-r', '--return-columns', dest='return_columns',
|
|
action='store', type=split_columns,
|
|
metavar='\'var1, var2, .., varN\' or FILE (.txt)',
|
|
help='provide the return columns separated by commas or a .txt file'
|
|
' where each row is a variable')
|
|
|
|
parser.add_argument(
|
|
'-e', '--export', dest='export_file',
|
|
type=str, metavar='FILE',
|
|
help='export to a pickle stateful objects states at the end of the '
|
|
'simulation')
|
|
|
|
parser.add_argument(
|
|
'-i', '--import-initial', dest='import_file',
|
|
type=str, metavar='FILE',
|
|
help='import stateful objects states from a pickle file,'
|
|
'if given initial conditions from var:value will be ignored')
|
|
|
|
|
|
###################
|
|
# Model arguments #
|
|
###################
|
|
|
|
model_arguments = parser.add_argument_group(
|
|
'model arguments',
|
|
'Modify model control variables.')
|
|
|
|
model_arguments.add_argument(
|
|
'-I', '--initial-time', dest='initial_time',
|
|
action='store', type=float, metavar='VALUE',
|
|
help='modify initial time of the simulation')
|
|
|
|
model_arguments.add_argument(
|
|
'-F', '--final-time', dest='final_time',
|
|
action='store', type=float, metavar='VALUE',
|
|
help='modify final time of the simulation')
|
|
|
|
model_arguments.add_argument(
|
|
'-T', '--time-step', dest='time_step',
|
|
action='store', type=float, metavar='VALUE',
|
|
help='modify time step of the simulation')
|
|
|
|
model_arguments.add_argument(
|
|
'-S', '--saveper', dest='saveper',
|
|
action='store', type=float, metavar='VALUE',
|
|
help='modify time step of the output')
|
|
|
|
model_arguments.add_argument(
|
|
'-R', '--return-timestamps', dest='return_timestamps',
|
|
action='store', type=split_timestamps,
|
|
metavar='\'value1, value2, .., valueN\'',
|
|
help='provide the return time stamps separated by commas, if given '
|
|
'--saveper will be ignored')
|
|
|
|
model_arguments.add_argument(
|
|
'-D', '--data', dest='data_files',
|
|
action='store', type=split_files, metavar='\'FILE1, FILE2, .., FILEN\'',
|
|
help='input data file or files to run the model')
|
|
|
|
#########################
|
|
# Translation arguments #
|
|
#########################
|
|
|
|
trans_arguments = parser.add_argument_group(
|
|
'translation arguments',
|
|
'Configure the translation of the original model.')
|
|
|
|
trans_arguments.add_argument(
|
|
'--translate', dest='run',
|
|
action='store_false', default=True,
|
|
help='only translate the model_file, '
|
|
'it does not run it after translation')
|
|
|
|
trans_arguments.add_argument(
|
|
'--split-views', dest='split_views',
|
|
action='store_true', default=False,
|
|
help='parse the sketch to detect model elements in each model view,'
|
|
' and then translate each view in a separate Python file')
|
|
|
|
trans_arguments.add_argument(
|
|
'--subview-sep', dest='subview_sep',
|
|
action='store', nargs="*", default=[],
|
|
metavar='separator_1 separator_2 ... separator_n',
|
|
help='further division of views into subviews, by identifying the '
|
|
'separator string in the view name, only availabe if --split-views'
|
|
' is used. Passing positional arguments after this argument will'
|
|
' not work')
|
|
|
|
|
|
#######################
|
|
# Warnings and errors #
|
|
#######################
|
|
|
|
warn_err_arguments = parser.add_argument_group(
|
|
'warning and errors arguments',
|
|
'Modify warning and errors management.')
|
|
|
|
warn_err_arguments.add_argument(
|
|
'--missing-values', dest='missing_values', default="warning",
|
|
action='store', type=str, choices=['warning', 'raise', 'ignore', 'keep'],
|
|
help='exception with missing values, \'warning\' (default) shows a '
|
|
'warning message and interpolates the values, \'raise\' raises '
|
|
'an error, \'ignore\' interpolates the values without showing '
|
|
'anything, \'keep\' keeps the missing values')
|
|
|
|
|
|
########################
|
|
# Positional arguments #
|
|
########################
|
|
|
|
parser.add_argument('model_file', metavar='model_file', type=check_model,
|
|
help='Vensim, Xmile or PySD model file')
|
|
|
|
parser.add_argument('new_values',
|
|
metavar='variable=new_value', type=split_vars,
|
|
nargs='*', action=SplitVarsAction,
|
|
help='redefine the value of variable with new value. '
|
|
'variable must be a model component, new_value may be a '
|
|
'float or a list of two lists')
|
|
|
|
# The destination new_values2 will never be used as the previous argument
|
|
# is given also with nargs='*'. Nevertheless, the following variable
|
|
# is declared for documentation
|
|
parser.add_argument('new_values2',
|
|
metavar='variable:initial_value', type=split_vars,
|
|
nargs='*', action=SplitVarsAction,
|
|
help='redefine the initial value of variable. '
|
|
'variable must be a model stateful element, initial_value'
|
|
' must be a float')
|
|
|
|
|
|
#########
|
|
# Usage #
|
|
#########
|
|
|
|
parser.usage = parser.format_usage().replace("usage: PySD", "python -m pysd")
|