"""This module implements parsing RegNet models defined in
https://github.com/DARPA-ASKEM/Model-Representations/tree/main/regnet.
"""
__all__ = ["model_from_url", "model_from_json_file", "template_model_from_amr_json"]
import json
import sympy
import requests
from mira.metamodel import *
from mira.sources.util import get_sympy
[docs]def model_from_url(url: str) -> TemplateModel:
"""Return a model from a URL
Parameters
----------
url :
The URL to the JSON file.
Returns
-------
:
A TemplateModel object.
"""
res = requests.get(url)
model_json = res.json()
return template_model_from_amr_json(model_json)
[docs]def model_from_json_file(fname: str) -> TemplateModel:
"""Return a model from a JSON file.
Parameters
----------
fname :
The path to the JSON file.
Returns
-------
:
A TemplateModel object.
"""
with open(fname) as f:
model_json = json.load(f)
return template_model_from_amr_json(model_json)
[docs]def template_model_from_amr_json(model_json) -> TemplateModel:
"""Return a model from a JSON object.
Parameters
----------
model_json :
The JSON object.
Returns
-------
:
A TemplateModel object.
"""
# First we build a lookup of vertices turned into Concepts and then use
# these as arguments to Templates. Vertices can also have natural
# production/degradation rates, which we capture here.
model = model_json['model']
# Next we get concepts needed to make symbols for rate law
# interpretation
concepts = {}
for vertex in model.get('vertices', []):
concepts[vertex['id']] = vertex_to_concept(vertex)
# Get the rates by their target, the target here refers to a
# vertex or edge id
ode_semantics = model_json.get("semantics", {}).get("ode", {})
rates = {
rate['target']: rate for rate in ode_semantics.get('rates', [])
}
# 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'])
# Next we process any intrinsic positive/negative growth
# at the vertex level into templates
templates = []
for vertex in model.get('vertices', []):
template = vertex_to_template(vertex, concepts[vertex['id']])
if template:
rate_obj = rates.get(vertex['id'], {})
if rate_obj:
rate_law = get_sympy(rate_obj, local_dict=symbols)
template.rate_law = SympyExprStr(rate_law)
elif vertex.get('rate_constant') is not None:
template.set_mass_action_rate_law(vertex['rate_constant'])
templates.append(template)
# Next we process initial conditions
initials = {}
for vertex in model.get('vertices', []):
initial_expression = vertex.get('initial')
if isinstance(initial_expression, str):
initial_sympy = safe_parse_expr(initial_expression,
local_dict=symbols)
elif isinstance(initial_expression, (int, float)):
initial_sympy = sympy.sympify(initial_expression)
else:
continue
initial = Initial(concept=concepts[vertex['id']],
expression=initial_sympy)
initials[initial.concept.name] = initial
# Now we iterate over all the edges and build templates
for edge in model.get('edges', []):
edge_id = edge['id']
source = edge['source']
target = edge['target']
source_concept = concepts[source]
target_concept = concepts[target]
if edge['sign'] is True:
template = ControlledReplication(
name=edge_id,
controller=source_concept,
subject=target_concept
)
else:
template = ControlledDegradation(
name=edge_id,
controller=source_concept,
subject=target_concept
)
props = edge.get('properties', {})
rate_obj = rates.get(edge_id, {})
if rate_obj:
rate_law = get_sympy(rate_obj, local_dict=symbols)
template.rate_law = SympyExprStr(rate_law)
else:
rate_constant = props.get('rate_constant')
if rate_constant is not None:
template.set_mass_action_rate_law(rate_constant)
templates.append(template)
# We get the time variable from the semantics
time = ode_semantics.get("time")
if time:
time_units = time.get('units')
time_units_expr = get_sympy(time_units, UNIT_SYMBOLS)
time_units_obj = Unit(expression=time_units_expr) \
if time_units_expr else None
model_time = Time(name=time['id'], units=time_units_obj)
else:
model_time = None
# We get observables from the semantics
observables = {}
for observable in ode_semantics.get("observables", []):
observable_expr = get_sympy(observable, symbols)
if observable_expr is None:
continue
observable = Observable(name=observable['id'],
expression=observable_expr,
display_name=observable.get('name'))
observables[observable.name] = observable
# Finally, we gather some model-level annotations
name = model_json.get('header', {}).get('name')
description = model_json.get('header', {}).get('description')
anns = Annotations(name=name, description=description)
return TemplateModel(templates=templates,
parameters=mira_parameters,
initials=initials,
annotations=anns,
observables=observables,
time=model_time)
def vertex_to_concept(vertex):
"""Return a Concept from a vertex"""
name = vertex['name'] if vertex.get('name') else vertex['id']
grounding = vertex.get('grounding', {})
identifiers = grounding.get('identifiers', {})
context = grounding.get('context', {})
return Concept(name=name,
identifiers=identifiers,
context=context)
def vertex_to_template(vertex, concept):
rate_constant = vertex.get('rate_constant')
sign = vertex.get('sign')
if rate_constant is None:
return None
if sign is True:
template = NaturalReplication(subject=concept)
else:
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)