Skip to content

Commit

Permalink
Merge pull request #358 from gyorilab/distribution_expr
Browse files Browse the repository at this point in the history
Support expressions in parameter's of distributions
  • Loading branch information
bgyori committed Sep 10, 2024
2 parents 3043c9a + fa8e730 commit ebd00b9
Show file tree
Hide file tree
Showing 9 changed files with 135 additions and 36 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
pip install "tox<4.0.0"
- name: Test with pytest
run: |
export MIRA_REST_URL=http://34.230.33.149:8771
export MIRA_REST_URL=http://mira-epi-dkg-lb-c7b58edea41524e6.elb.us-east-1.amazonaws.com:8771
tox -e py
# - name: Upload coverage report to codecov
# uses: codecov/codecov-action@v1
Expand Down
14 changes: 11 additions & 3 deletions mira/metamodel/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -946,15 +946,23 @@
"properties": {
"type": {
"title": "Type",
"description": "The type of distribution, e.g. 'uniform', 'normal', etc.",
"description": "The type of distribution as provided by ProbOnto e.g. 'StandardUniform1', 'Beta1', etc.",
"type": "string"
},
"parameters": {
"title": "Parameters",
"description": "The parameters of the distribution.",
"description": "The parameters of the distribution keyed by parameter names controlled by ProbOnto and values that are either floating point values or symbolic expressions over other parameters.",
"type": "object",
"additionalProperties": {
"type": "number"
"anyOf": [
{
"type": "number"
},
{
"type": "string",
"example": "2*x"
}
]
}
}
},
Expand Down
10 changes: 7 additions & 3 deletions mira/metamodel/template_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,10 +104,14 @@ class Distribution(BaseModel):
"""A distribution of values for a parameter."""

type: str = Field(
description="The type of distribution, e.g. 'uniform', 'normal', etc."
description="The type of distribution as provided by ProbOnto "
"e.g. 'StandardUniform1', 'Beta1', etc."
)
parameters: Dict[str, float] = Field(
description="The parameters of the distribution."
parameters: Dict[str, Union[float, SympyExprStr]] = Field(
description="The parameters of the distribution keyed by parameter names "
"controlled by ProbOnto and values that are either floating "
"point values or symbolic expressions over other "
"parameters."
)


Expand Down
11 changes: 9 additions & 2 deletions mira/modeling/amr/petrinet.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

from pydantic import BaseModel, Field

from mira.metamodel import expression_to_mathml, TemplateModel
from mira.metamodel import expression_to_mathml, TemplateModel, SympyExprStr
from mira.sources.amr import sanity_check_amr

from .. import Model
Expand Down Expand Up @@ -188,9 +188,16 @@ def __init__(self, model: Model):
elif param.distribution.type is None:
logger.warning("can not add distribution without type: %s", param.distribution)
else:
serialized_distr_parameters = {}
for param_key, param_value in param.distribution.parameters.items():
if isinstance(param_value, SympyExprStr):
serialized_distr_parameters[param_key] = \
str(param_value.args[0])
else:
serialized_distr_parameters[param_key] = param_value
param_dict['distribution'] = {
'type': param.distribution.type,
'parameters': param.distribution.parameters,
'parameters': serialized_distr_parameters,
}
if param.concept and param.concept.units:
param_dict['units'] = {
Expand Down
13 changes: 7 additions & 6 deletions mira/sources/amr/petrinet.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,15 +109,16 @@ def template_model_from_amr_json(model_json) -> TemplateModel:
# }
ode_semantics = model_json.get("semantics", {}).get("ode", {})
symbols = {state_id: sympy.Symbol(state_id) for state_id in concepts}
mira_parameters = {}

# We first make symbols for all the parameters
for parameter in ode_semantics.get('parameters', []):
mira_parameters[parameter['id']] = parameter_to_mira(parameter)
symbols[parameter['id']] = sympy.Symbol(parameter['id'])

param_values = {
p['id']: p['value'] for p in ode_semantics.get('parameters', [])
if p.get('value') is not None
}
# We then process the parameters into MIRA Parameter objects
mira_parameters = {}
for parameter in ode_semantics.get('parameters', []):
mira_parameters[parameter['id']] = \
parameter_to_mira(parameter, param_symbols=symbols)

# Next we process initial conditions
initials = {}
Expand Down
18 changes: 6 additions & 12 deletions mira/sources/amr/regnet.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import requests

from mira.metamodel import *
from mira.sources.util import get_sympy
from mira.sources.util import get_sympy, parameter_to_mira


def model_from_url(url: str) -> TemplateModel:
Expand Down Expand Up @@ -83,11 +83,14 @@ def template_model_from_amr_json(model_json) -> TemplateModel:
# Next, we capture all symbols in the model, including states and
# parameters. We also extract parameters at this point.
symbols = {state_id: sympy.Symbol(state_id) for state_id in concepts}
mira_parameters = {}
for parameter in model.get('parameters', []):
mira_parameters[parameter['id']] = parameter_to_mira(parameter)
symbols[parameter['id']] = sympy.Symbol(parameter['id'])

mira_parameters = {}
for parameter in model.get('parameters', []):
mira_parameters[parameter['id']] = \
parameter_to_mira(parameter, param_symbols=symbols)

# Next we process any intrinsic positive/negative growth
# at the vertex level into templates
templates = []
Expand Down Expand Up @@ -213,12 +216,3 @@ def vertex_to_template(vertex, concept):
template = NaturalDegradation(subject=concept)
template.set_mass_action_rate_law(rate_constant)
return template


def parameter_to_mira(parameter):
"""Return a MIRA parameter from a parameter"""
distr = Distribution(**parameter['distribution']) \
if parameter.get('distribution') else None
return Parameter(name=parameter['id'],
value=parameter.get('value'),
distribution=distr)
10 changes: 7 additions & 3 deletions mira/sources/amr/stockflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,17 @@ def template_model_from_amr_json(model_json) -> TemplateModel:
all_stocks.add(stock['id'])
symbols[stock['id']] = sympy.Symbol(stock['id'])

# Process parameters
# Process parameters, first to get all symbols, then
# processing the parameters to get the MIRA parameters
ode_semantics = model_json.get("semantics", {}).get("ode", {})
mira_parameters = {}
for parameter in ode_semantics.get('parameters', []):
mira_parameters[parameter['id']] = parameter_to_mira(parameter)
symbols[parameter['id']] = sympy.Symbol(parameter['id'])

mira_parameters = {}
for parameter in ode_semantics.get('parameters', []):
mira_parameters[parameter['id']] = \
parameter_to_mira(parameter, param_symbols=symbols)

# Process auxiliaries
aux_expressions = {}
for auxiliary in auxiliaries:
Expand Down
28 changes: 22 additions & 6 deletions mira/sources/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ def transition_to_templates(
)


def parameter_to_mira(parameter) -> Parameter:
def parameter_to_mira(parameter, param_symbols=None) -> Parameter:
"""
Return a MIRA parameter from a mapping of MIRA Parameter attributes to
values.
Expand All @@ -170,17 +170,33 @@ def parameter_to_mira(parameter) -> Parameter:
----------
parameter : Dict[str,Any]
A mapping containing MIRA Parameter attributes to values.
param_symbols : Optional[Dict]
An optional dict of all parameter symbols that are needed if expressions
are used in parameter distributions so that these can be
recognized as symbols.
Returns
-------
:
The corresponding MIRA Parameter.
"""
distr = (
Distribution(**parameter["distribution"])
if parameter.get("distribution")
else None
)
distr_json = parameter.get("distribution")
if distr_json:
distr_type = distr_json.get("type") if distr_json else None
# We need to check for symbolic expressions in parameters
processed_distr_parameters = {}
for param_key, param_value in distr_json.get("parameters", {}).items():
if isinstance(param_value, float):
processed_distr_parameters[param_key] = param_value
else:
processed_distr_parameters[param_key] = \
safe_parse_expr(param_value)
distr = Distribution(
type=distr_type,
parameters=processed_distr_parameters,
)
else:
distr = None
data = {
"name": parameter["id"],
"display_name": parameter.get("name"),
Expand Down
65 changes: 65 additions & 0 deletions tests/test_distributions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import sympy

from mira.metamodel import *
from mira.modeling import Model
from mira.modeling.amr.petrinet import template_model_to_petrinet_json
from mira.sources.amr import model_from_json


def test_distribution_expressions():
beta_mean = Parameter(name='beta_mean',
distribution=Distribution(type="Beta1",
parameters={'alpha': sympy.Integer(1),
'beta': sympy.Integer(10)}))
gamma_mean = Parameter(name='gamma_mean',
distribution=Distribution(type="Beta1",
parameters={'alpha': sympy.Integer(10),
'beta': sympy.Integer(10)}))
beta = Parameter(name='beta',
distribution=Distribution(type="InverseGamma1",
parameters={'shape': sympy.Symbol('beta_mean'),
'scale': sympy.Float(0.01)}))
gamma = Parameter(name='gamma',
distribution=Distribution(type="InverseGamma1",
parameters={'shape': sympy.Symbol('gamma_mean'),
'scale': sympy.Float(0.01)}))

# Make an SIR model with beta and gamma in rate laws
sir_model = TemplateModel(
templates=[
ControlledConversion(
subject=Concept(name='S'),
outcome=Concept(name='I'),
controller=Concept(name='I'),
rate_law=sympy.Symbol('S') * sympy.Symbol('I') * sympy.Symbol('beta')
),
NaturalConversion(
subject=Concept(name='I'),
outcome=Concept(name='R'),
rate_law=sympy.Symbol('I') * sympy.Symbol('gamma')
),
],
parameters={
'beta': beta,
'gamma': gamma,
'beta_mean': beta_mean,
'gamma_mean': gamma_mean,
}
)

model = Model(sir_model)
pn_json = template_model_to_petrinet_json(sir_model)
params = pn_json['semantics']['ode']['parameters']
assert {p['id'] for p in params} == \
{'beta_mean', 'gamma_mean', 'beta', 'gamma'}
beta = [p for p in params if p['id'] == 'beta'][0]
assert beta['distribution']['type'] == 'InverseGamma1'
assert beta['distribution']['parameters']['shape'] == 'beta_mean'

# Now read the model back and check if it is deserialized
tm = model_from_json(pn_json)
assert tm.parameters['beta'].distribution.type == 'InverseGamma1'
assert isinstance(tm.parameters['beta'].distribution.parameters['shape'],
SympyExprStr)
assert tm.parameters['beta'].distribution.parameters['shape'].args[0] == \
sympy.Symbol('beta_mean')

0 comments on commit ebd00b9

Please sign in to comment.