Source code for mira.sources.system_dynamics.vensim

"""This module implements an API interface for retrieving Vensim models by Ventana Systems
denoted by the .mdl extension through a locally downloaded file or URL. We then
convert the Vensim model into a generic pysd model object that will be parsed and converted to an
equivalent MIRA template model. We preprocess the vensim file to extract variable expressions.

Vensim model documentation:https://www.vensim.com/documentation/sample_models.html

Repository of sample Vensim models: https://github.com/SDXorg/test-models/tree/master/samples
"""

import tempfile
import re

import pysd
from pysd.translators.vensim.vensim_file import VensimFile
import requests

from mira.metamodel import TemplateModel, Initial
from mira.sources.system_dynamics.pysd import (
    template_model_from_pysd_model,
    ifthenelse_to_piecewise,
    with_lookup_to_piecewise,
)

__all__ = ["template_model_from_mdl_file", "template_model_from_mdl_url"]

NEW_CONTROL_DELIMETER = (
    " ******************************************************** .Control "
    "********************************************************"
)
SKETCH_DELIMETER = (
    "\\\---/// Sketch information - do not modify anything except names"
)
UTF_ENCODING = "{UTF-8} "


[docs]def template_model_from_mdl_file( fname, *, grounding_map=None, initials=None, initials_from_integ: bool = False ) -> TemplateModel: """Return a template model from a local Vensim file Parameters ---------- fname : str or pathlib.Path The path to the local Vensim file grounding_map: dict[str, Concept] A grounding map, a map from label to Concept initials: dict[str, float] Explicit initial values to use for compartments in the model. Will overwrite model-internal definitions. initials_from_integ : bool If true, gets initial values from INTEG expressions. If ``initials`` are given, they override anything from INTEG expressions Returns ------- : A MIRA template model """ pysd_model = pysd.read_vensim(fname) vensim_file = VensimFile(fname) expression_map, initials_map = extract_vensim_variable_expressions( vensim_file.model_text, initials_from_integ=initials_from_integ, ) if initials: initials_map.update(initials) return template_model_from_pysd_model( pysd_model, expression_map, grounding_map=grounding_map, initials_map=initials_map, )
[docs]def template_model_from_mdl_url( url, *, grounding_map=None, initials=None, initials_from_integ: bool = False ) -> TemplateModel: """Return a template model from a Vensim file provided by an url Parameters ---------- url : str The url to the mdl file grounding_map: dict[str, Concept] A grounding map, a map from label to Concept initials: dict[str, float] Explicit initial values to use for compartments in the model. Will overwrite model-internal definitions. initials_from_integ : bool If true, gets initial values from INTEG expressions. If ``initials`` are given, they override anything from INTEG expressions Returns ------- : A MIRA Template Model """ data = requests.get(url).content temp_file = tempfile.NamedTemporaryFile( mode="w+b", suffix=".mdl", delete=False ) with temp_file as file: file.write(data) return template_model_from_mdl_file( temp_file.name, grounding_map=grounding_map, initials=initials, initials_from_integ=initials_from_integ, )
# look past control section def extract_vensim_variable_expressions( model_text, *, initials_from_integ: bool = False ): """Method that extracts expressions for each variable in a Vensim file Parameters ---------- model_text : str The plain-text information about the Vensim file initials_from_integ : bool If true, gets initial values from INTEG expressions Returns ------- : dict[str,str] Mapping of variable name to string variable expression """ expression_map = {} initial_values = {} # Model text is a single string that represents the entire contents of the Vensim model. # We split model text into a list with elements delimited by "|" # variable declaration in vensim files are delimited by the "|" character model_split_text = model_text.split("|") for text in model_split_text: # signifies end of model if SKETCH_DELIMETER in text: break # signifies start of control section, continue if NEW_CONTROL_DELIMETER in text: continue # if no variable declaration, continue if "=" not in text: continue # first entry usually has encoding type if UTF_ENCODING in text: text = text.replace(UTF_ENCODING, "") # Throw away every text after the "~" and split the remaining text by "=" to get # variable name and accompanying expression var_declaration = text.split("~")[0].split("=") old_var_name = var_declaration[0].strip() text_expression = var_declaration[1].strip() # account for variables with expressions that have "=" in them besides the # initial "=" character for var declaration, stitch together the expression if len(var_declaration) > 2: for part_expression_text in var_declaration[2:]: text_expression += "=" + part_expression_text # vensim has several builtin functions, like MIN(), MAX(), XIDZ(), INTEG() # we pass these along for sympy to just consider like function calls. # Hackathon file does not use any built-in functions that don't take a single argument # Can account for single argument Vensim functions as well # List of Vensim functions: https://www.vensim.com/documentation/22300.html # "INTEG" is the keyword used to define a state/stock # however, we can't yet handle if/then/else constructs, so we skip them if "if then else" in text_expression.lower(): text_expression = ifthenelse_to_piecewise(text_expression) if "with lookup" in text_expression.lower(): text_expression = with_lookup_to_piecewise(text_expression) # If there's an INTEG expression, we can extract an initial value, # otherwise, it is none. initial = None # If we come across a state, get the expression for the state only # For the hackathon Vensim file, we can use a new regex that gets not only the expression # but the initial value as well. Because when pysd ingests the hackathon Vensim file, # it will have 44 initial values for only 19 states. if "INTEG" in text_expression: match = re.search(r"\(([^,]+),\s*(.*)?\)", text_expression) text_expression = match.group(1) initial = match.group(2) expression_map[old_var_name] = text_expression if initials_from_integ: # CTH: I couldn't find where this normalization happens # between the vensim and pysd code, so I added it again here. # also, the initial value might be an expression that needs normalizing initial_values[_norm(old_var_name)] = initial and _norm(initial) return expression_map, initial_values def _norm(s): return s.lower().replace(" ", "_")