Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Parse Vensim/Stella models into MIRA template models #272

Merged
merged 50 commits into from
Jan 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
341c402
Initial vensim commit,downloading file and reading bytes
nanglo123 Dec 19, 2023
eaf06c6
Create initial implementation of vensim digestion with while loop
nanglo123 Dec 22, 2023
4f61f93
Add handling for local mdl files
nanglo123 Dec 22, 2023
f9b9284
Add handling for non-integral multi-line expressions and add more com…
nanglo123 Dec 22, 2023
4908159
Handle variable names that cannot be parsed by sympy
nanglo123 Jan 2, 2024
8b42fe3
Handle multi-line comments
nanglo123 Jan 3, 2024
75bb46e
Remove print debugging statements and unused flag variable
nanglo123 Jan 3, 2024
a67bbc5
Handle multi-line expressions in a more robust way
nanglo123 Jan 3, 2024
078a0d1
Add method handling multi-line expressions into first pass of mdl fil…
nanglo123 Jan 3, 2024
314827b
Import pysd library and read vensim file
nanglo123 Jan 4, 2024
a77e083
Create template model from information extract from pysd except for t…
nanglo123 Jan 5, 2024
362d4a1
Add processing for observables
nanglo123 Jan 8, 2024
ebb289c
Add initial implementation of automated processing of templates for s…
nanglo123 Jan 9, 2024
dd09287
Remove addition of initials to template model and make demo notebook …
nanglo123 Jan 9, 2024
c90bb0c
Change notebook stratify on new age stratum and rerun it
nanglo123 Jan 9, 2024
87c5ce5
Change file path for sir.mdl file
nanglo123 Jan 9, 2024
cee229f
Load sir.mdl file with url rather than relying on local copy
nanglo123 Jan 9, 2024
877bc08
Update notebook to compare parameters and rate laws between stockflow…
nanglo123 Jan 9, 2024
95d2760
Re-order cells of notebook
nanglo123 Jan 9, 2024
d6591bd
Format rate law printing in a neater way
nanglo123 Jan 9, 2024
fb747ca
Rerun cell
nanglo123 Jan 9, 2024
a4651af
Change vensim notebook based off suggestions
nanglo123 Jan 9, 2024
ff05a5f
Use temporary file to store data retrieved from url when creating a v…
nanglo123 Jan 9, 2024
a6b4b48
Add changed notebook
nanglo123 Jan 9, 2024
cd0b46a
Add handling for rate laws that may not have an input or output
nanglo123 Jan 11, 2024
3c08ce0
Add docstrings to module and methods, change process expression metho…
nanglo123 Jan 16, 2024
59cd0a6
Add unit handling for parameters and initials that may be missing units
nanglo123 Jan 16, 2024
7f6a6f8
Add updated comments
nanglo123 Jan 17, 2024
f083a6f
Delete example directory and add example sir mdl to module docstring
nanglo123 Jan 17, 2024
0c03b68
Add pysd to list of required installs in setup.cfg
nanglo123 Jan 22, 2024
7e2dd13
Add initial values and remove initial expression from parameter
nanglo123 Jan 23, 2024
71d0fc5
Add option for processing local files and add API functions for vensim
nanglo123 Jan 24, 2024
967d91a
Standardize units
nanglo123 Jan 25, 2024
2218c7f
Update docstrings for modules and add vensim and stella module to sou…
nanglo123 Jan 25, 2024
f7039ca
Change url type in docstring and add initial test file for system dyn…
nanglo123 Jan 25, 2024
d35697a
Generalize logic for finding controllers
nanglo123 Jan 25, 2024
3b41a4f
Extract initial state values from model state property rather than si…
nanglo123 Jan 25, 2024
97fad13
Add support for processing stella models through file or url, refacto…
nanglo123 Jan 25, 2024
ef3423a
Update docstring for stella and vensim file
nanglo123 Jan 26, 2024
2afa0b5
change variable name and remove pass statement from test_system_dynamics
nanglo123 Jan 26, 2024
3015c97
Extend system dynamics notebook by ingesting SIR stella model and cha…
nanglo123 Jan 26, 2024
06154af
Delete original Vensim notebook due to renaming issue
nanglo123 Jan 29, 2024
b1764f7
Refactor stella code to account for order of operations, clean up par…
nanglo123 Jan 29, 2024
bffd3f0
Remove pass statement from model parsing file
nanglo123 Jan 30, 2024
3f873ea
Rename variables and dictionary fields for easier readability
nanglo123 Jan 30, 2024
3361fcf
Rename pysd parsing module, add it to sources and change code to refl…
nanglo123 Jan 30, 2024
d680664
Pass a sympy object into SympyExprStr wrapper to avoid error in io mo…
nanglo123 Jan 30, 2024
f207291
Further add to system dynamics test to test for correctness and add e…
nanglo123 Jan 30, 2024
3b22826
Add extra barline under module name in sources.rst
nanglo123 Jan 30, 2024
5d74509
Add all to module level
bgyori Jan 30, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions docs/source/sources.rst
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,21 @@ Utility Methods (:py:mod:`mira.sources.util`)
.. automodule:: mira.sources.util
:members:
:show-inheritance:

nanglo123 marked this conversation as resolved.
Show resolved Hide resolved
Vensim (:py:mod:`mira.sources.system_dynamics.vensim`)
------------------------------------------------------
.. automodule:: mira.sources.system_dynamics.vensim
:members:
:show-inheritance:

Stella (:py:mod:`mira.sources.system_dynamics.stella`)
------------------------------------------------------
.. automodule:: mira.sources.system_dynamics.stella
:members:
:show-inheritance:

PYSD Model Parsing (:py:mod:`mira.sources.system_dynamics.pysd`)
----------------------------------------------------------------
.. automodule:: mira.sources.system_dynamics.pysd
:members:
:show-inheritance:
15 changes: 6 additions & 9 deletions mira/modeling/amr/stockflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,13 +116,12 @@ def __init__(self, model: Model):

# If the parameter is not a base level model parameter and is present within a flow rate expression
if not key.startswith('p_') and used_parameter_flag:
aux_key = key + '_aux'
auxiliary_dict = {'id': aux_key}
auxiliary_dict['name'] = aux_key
expression = sympy.Symbol(aux_key)
auxiliary_dict['expression'] = str(expression)
auxiliary_dict = {'id': key}
auxiliary_dict['name'] = key
expression = sympy.Symbol(key)
auxiliary_dict['expression'] = key
auxiliary_dict['expression_mathml'] = expression_to_mathml(expression)
auxiliary_mapping[key] = aux_key
auxiliary_mapping[key] = key
self.auxiliaries.append(auxiliary_dict)
elif key.startswith('p_'):
auxiliary_dict = {'id': key[2:]}
Expand Down Expand Up @@ -179,11 +178,9 @@ def __init__(self, model: Model):
for symbol in flow.template.rate_law.free_symbols:
link_dict = {'id': f'link{link_id}'}
str_symbol = str(symbol)
if str_symbol in model.parameters:
str_symbol = str_symbol[2:]

link_dict['source'] = str_symbol
link_dict['target'] = fid
link_dict['target'] = "flow" + fid
link_id += 1
self.links.append(link_dict)

Expand Down
11 changes: 0 additions & 11 deletions mira/sources/amr/stockflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,14 +187,3 @@ def model_from_url(url: str) -> TemplateModel:
model_json = res.json()
return template_model_from_amr_json(model_json)


def main():
sfamr = requests.get("https://raw.githubusercontent.com/DARPA-ASKEM/Model-Representations/" \
"7f5e377225675259baa6486c64102f559edfd79f/stockflow/examples/sir.json").json()

tm = template_model_from_amr_json(sfamr)
return tm


if __name__ == "__main__":
tm = main()
11 changes: 11 additions & 0 deletions mira/sources/system_dynamics/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
__all__ = [
"template_model_from_mdl_file",
"template_model_from_mdl_url",
"template_model_from_stella_model_file",
"template_model_from_stella_model_url",
"template_model_from_pysd_model",
]

from mira.sources.system_dynamics.vensim import *
from mira.sources.system_dynamics.stella import *
from mira.sources.system_dynamics.pysd import *
307 changes: 307 additions & 0 deletions mira/sources/system_dynamics/pysd.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,307 @@
"""This module implements parsing of a generic pysd model irrespective of source and source type
and extracting its contents to create an equivalent MIRA template model.
"""

import pandas as pd
import sympy

from mira.metamodel import *
from mira.metamodel.utils import safe_parse_expr
from mira.metamodel import Concept, TemplateModel
from mira.sources.util import (
parameter_to_mira,
transition_to_templates,
get_sympy,
)

CONTROL_VARIABLE_NAMES = {"FINALTIME", "INITIALTIME", "SAVEPER", "TIMESTEP"}
UNITS_MAPPING = {
sympy.Symbol("Person"): sympy.Symbol("person"),
sympy.Symbol("Persons"): sympy.Symbol("person"),
sympy.Symbol("Day"): sympy.Symbol("day"),
sympy.Symbol("Days"): sympy.Symbol("day"),
}

__all__ = ["template_model_from_pysd_model"]


def template_model_from_pysd_model(pysd_model, expression_map) -> TemplateModel:
"""Given a model and its accompanying expression_map, extract information from the arguments
to create an equivalent MIRA template model.

Parameters
----------
pysd_model : Model
The pysd model object
expression_map : dict[str,str]
Map of variable name to expression

Returns
-------
:
MIRA template model
"""
model_doc_df = pysd_model.doc
state_initial_values = pysd_model.state
processed_expression_map = {}

# Mapping of variable name in vensim model to variable python-equivalent name
old_name_new_pyname_map = dict(
zip(model_doc_df["Real Name"], model_doc_df["Py Name"])
)

# preprocess expression text to make it sympy parseable
for var_name, var_expression in expression_map.items():
new_var_name = old_name_new_pyname_map[var_name]
processed_expression_map[new_var_name] = preprocess_text(var_expression)

symbols = dict(
zip(
model_doc_df["Py Name"],
list(map(lambda x: sympy.Symbol(x), model_doc_df["Py Name"])),
)
)

new_symbols = dict(
zip(
model_doc_df["Real Name"],
list(map(lambda x: sympy.Symbol(x), model_doc_df["Py Name"])),
)
)

sympy_expression_map = {}
model_states = model_doc_df[model_doc_df["Type"] == "Stateful"]
concepts = {}
all_states = set()

# map between states and incoming/out-coming rate laws
state_rate_map = {}
# map between states and sympy version of the INTEG expression representing that state
state_sympy_map = {}

# process states and build mapping of state to input rate laws and output rate laws
for index, state in model_states.iterrows():
concept_state = state_to_concept(state)
concepts[concept_state.name] = concept_state
all_states.add(concept_state.name)
symbols[concept_state.name] = sympy.Symbol(concept_state.name)

state_name = state["Py Name"]
state_rate_map[state_name] = {"input_rates": [], "output_rates": []}
state_expr_text = processed_expression_map[state_name]

state_arg_sympy = safe_parse_expr(state_expr_text, new_symbols)
sympy_expression_map[state_name] = state_arg_sympy
# map of states to rate laws that affect the state
state_sympy_map[state_name] = state_arg_sympy

# Create a map of states and whether the rate-law/s involved with a state are going in
# or out of the state
if state_arg_sympy.args:
# if it's just the negation of a single symbol
if (
sympy.core.numbers.NegativeOne() in state_arg_sympy.args
and len(state_arg_sympy.args) == 2
):
str_symbol = str(state_arg_sympy)
state_rate_map[state_name]["output_rates"].append(
str_symbol[1:]
)
else:
for rate_free_symbol in state_arg_sympy.args:
str_rate_free_symbol = str(rate_free_symbol)
if "-" in str_rate_free_symbol:
# Add the symbol to outputs symbol without the negative sign
state_rate_map[state_name]["output_rates"].append(
str_rate_free_symbol[1:]
)
else:
state_rate_map[state_name]["input_rates"].append(
str_rate_free_symbol
)
else:
# if it's just a single symbol (i.e. no negation), args property will be empty
state_rate_map[state_name]["input_rates"].append(
str(state_arg_sympy)
)

# process initials, currently we use the value of the state at timestamp 0
mira_initials = {}
for state_initial_value, (state_name, state_concept) in zip(
state_initial_values, concepts.items()
):
initial = Initial(
concept=concepts[state_name].copy(deep=True),
expression=SympyExprStr(sympy.Float(state_initial_value)),
)
mira_initials[initial.concept.name] = initial

# process parameters
mira_parameters = {}
for name, expression in processed_expression_map.items():
if expression.replace(".", "").replace(" ", "").isdecimal():
model_parameter_info = model_doc_df[model_doc_df["Py Name"] == name]
if model_parameter_info["Units"].values[0]:
unit_text = (
model_parameter_info["Units"].values[0].replace(" ", "")
)
parameter = {
"id": name,
"value": float(expression),
"description": model_parameter_info["Comment"].values[0],
"units": {"expression": unit_text},
}
else:
# if units don't exist
parameter = {
"id": name,
"value": float(expression),
"description": model_parameter_info["Comment"].values[0],
}

mira_parameters[name] = parameter_to_mira(parameter)

# standardize parameter units if they exist
if mira_parameters[name].units:
param_unit = mira_parameters[name].units
for old_unit_symbol, new_unit_symbol in UNITS_MAPPING.items():
param_unit.expression = param_unit.expression.subs(
old_unit_symbol, new_unit_symbol
)

# construct transitions mapping that determine inputs and outputs states to a rate-law
transition_map = {}
auxiliaries = model_doc_df[model_doc_df["Type"] == "Auxiliary"]
for index, aux_tuple in auxiliaries.iterrows():
if (
aux_tuple["Subtype"] == "Normal"
and aux_tuple["Real Name"] not in CONTROL_VARIABLE_NAMES
):
rate_name = aux_tuple["Py Name"]
rate_expr = safe_parse_expr(
preprocess_text(processed_expression_map[rate_name]),
symbols,
)
input_state, output_state, controller = None, None, None

# If we come across a rate-law that is leaving a state, we add the state as an input
# to the rate-law, vice-versa if a rate-law is going into a state.
for state_name, in_out in state_rate_map.items():
if rate_name in in_out["output_rates"]:
input_state = state_name
if rate_name in in_out["input_rates"]:
output_state = state_name
# if a state isn't consumed by a flow (the flow isn't listed as an output of
# the state) but affects the rate of a flow, then that state is a controller
if (
sympy.Symbol(state_name) in rate_expr.free_symbols
and rate_name
not in state_rate_map[state_name]["output_rates"]
):
controller = output_state

transition_map[rate_name] = {
"name": rate_name,
"expression": rate_expr,
"input": input_state,
"output": output_state,
"controller": controller,
}

used_states = set()

# Create templates from transitions
templates_ = []
for template_id, (transition_name, transition) in enumerate(
transition_map.items()
):
input_concepts = []
output_concepts = []
controller_concepts = []
input_name, output_name, controller_name = None, None, None
if transition.get("input"):
input_name = transition.get("input")
input_concepts.append(concepts[input_name].copy(deep=True))
if transition.get("output"):
output_name = transition.get("output")
output_concepts.append(concepts[output_name].copy(deep=True))
if transition.get("controller"):
controller_name = transition.get("controller")
controller_concepts.append(
concepts[controller_name].copy(deep=True)
)

used_states |= {input_name, output_name}

templates_.extend(
transition_to_templates(
input_concepts=input_concepts,
output_concepts=output_concepts,
controller_concepts=controller_concepts,
transition_rate=transition["expression"],
transition_id=str(template_id + 1),
transition_name=transition_name,
)
)

return TemplateModel(
templates=templates_,
parameters=mira_parameters,
initials=mira_initials,
)


def state_to_concept(state) -> Concept:
"""Create a MIRA Concept from a state

Parameters
----------
state : pd.Series
The series that contains state data

Returns
-------
:
The MIRA concept created from the state
"""
name = state["Py Name"]
description = state["Comment"]
unit_dict = {
"expression": state["Units"].replace(" ", "")
if state["Units"]
else None
}
unit_expr = get_sympy(unit_dict, UNIT_SYMBOLS)
units_obj = Unit(expression=unit_expr) if unit_expr else None

return Concept(name=name, units=units_obj, description=description)


def preprocess_text(expr_text):
"""Preprocess a string expression to convert the expression into sympy parseable string

Parameters
----------
expr_text : str
The string expression

Returns
-------
: str
The processed string expression
"""
# strip leading and trailing white spaces
# remove spaces between operators and operands
# replace space between two words that makeup a variable name with "_"'
expr_text = (
expr_text.strip()
.replace(" * ", "*")
.replace(" - ", "-")
.replace(" / ", "/")
.replace(" + ", "+")
.replace("^", "**")
.replace(" ", "_")
.replace('"', "")
.lower()
)
return expr_text
Loading
Loading