From e69a0cdc09a0de1b3ea2a8d305f6d74b4114a6b2 Mon Sep 17 00:00:00 2001 From: nanglo123 Date: Thu, 27 Jun 2024 13:08:34 -0400 Subject: [PATCH 1/5] Convert datetime objects to strings --- mira/modeling/amr/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mira/modeling/amr/utils.py b/mira/modeling/amr/utils.py index 27249b191..d701ffca9 100644 --- a/mira/modeling/amr/utils.py +++ b/mira/modeling/amr/utils.py @@ -3,7 +3,7 @@ def add_metadata_annotations(metadata, model): metadata['annotations'] = {} return annotations_subset = { - k: v + k: str(v) if k in ["time_start", "time_end"] else v for k, v in model.template_model.annotations.dict().items() if k not in ["name", "description"] # name and description already have a privileged place From c7b45d6ab7b06555d88072754e167157700473bd Mon Sep 17 00:00:00 2001 From: nanglo123 Date: Thu, 27 Jun 2024 13:12:38 -0400 Subject: [PATCH 2/5] Add metadata annotation for conversion to stockflow amr --- mira/modeling/amr/stockflow.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/mira/modeling/amr/stockflow.py b/mira/modeling/amr/stockflow.py index d75bd4e1a..3faa4a5ad 100644 --- a/mira/modeling/amr/stockflow.py +++ b/mira/modeling/amr/stockflow.py @@ -4,10 +4,14 @@ __all__ = ["AMRStockFlowModel", "template_model_to_stockflow_json"] +import logging + import sympy + from mira.modeling import Model from mira.metamodel import * -import logging +from .utils import add_metadata_annotations + logger = logging.getLogger(__name__) @@ -193,6 +197,8 @@ def __init__(self, model: Model): link_id += 1 self.links.append(link_dict) + add_metadata_annotations(self.metadata, model) + def to_json(self): """Return a JSON dict structure of the Stock and Flow model.""" return { From 49d5d3c2c1fc177420e939c5709e12d90c8528ed Mon Sep 17 00:00:00 2001 From: nanglo123 Date: Thu, 27 Jun 2024 14:03:53 -0400 Subject: [PATCH 3/5] Add regression test for serialization of template model into different frameworks --- tests/test_amr_source.py | 90 ++++++++++++++++++++++++++-------------- 1 file changed, 58 insertions(+), 32 deletions(-) diff --git a/tests/test_amr_source.py b/tests/test_amr_source.py index 6b6db4728..d5af156d9 100644 --- a/tests/test_amr_source.py +++ b/tests/test_amr_source.py @@ -1,18 +1,51 @@ import requests +import json + import sympy + from mira.metamodel import * from mira.sources.amr import model_from_url from mira.sources.amr import petrinet from mira.sources.amr import regnet from mira.sources.amr import stockflow from mira.modeling.amr.regnet import template_model_to_regnet_json +from mira.modeling.amr.petrinet import template_model_to_petrinet_json +from mira.modeling.amr.stockflow import template_model_to_stockflow_json petrinet_example = 'https://raw.githubusercontent.com/DARPA-ASKEM/' \ - 'Model-Representations/main/petrinet/examples/sir.json' + 'Model-Representations/main/petrinet/examples/sir.json' regnet_example = 'https://raw.githubusercontent.com/DARPA-ASKEM/' \ - 'Model-Representations/main/regnet/examples/lotka_volterra.json' + 'Model-Representations/main/regnet/examples/lotka_volterra.json' stockflow_example = 'https://raw.githubusercontent.com/DARPA-ASKEM/' \ - 'Model-Representations/7f5e377225675259baa6486c64102f559edfd79f/stockflow/examples/sir.json' + 'Model-Representations/7f5e377225675259baa6486c64102f559edfd79f/stockflow/examples/sir.json' + +template_model = TemplateModel( + templates=[ + ControlledReplication( + name='replication', + controller=Concept(name='A'), + subject=Concept(name='B'), + rate_law=SympyExprStr(sympy.sympify('k * A * B / (1 + B)')) + ), + NaturalDegradation( + name='degradation', + subject=Concept(name='B'), + rate_law=SympyExprStr(sympy.sympify('k * B')) + ) + ], + observables={ + 'obs1': Observable( + name='obs1', + expression=SympyExprStr(sympy.sympify('A + B')), + display_name='obs1' + ) + }, + annotations=Annotations( + time_start="2020-03-01T00:00:00", + time_end="2020-08-01T00:00:00" + ), + time=Time(name='timexx'), +) def stockflow_set_up_file(): @@ -76,21 +109,25 @@ def test_stockflow_flow_to_template(): def test_stockflow_parameter_to_mira(): sfamr = stockflow_set_up_file() tm = stockflow.model_from_url(stockflow_example) - for amr_param, tm_param in zip(sfamr['semantics']['ode']['parameters'], tm.parameters.values()): + for amr_param, tm_param in zip(sfamr['semantics']['ode']['parameters'], + tm.parameters.values()): assert amr_param['id'] == tm_param.name assert amr_param['name'] == tm_param.display_name assert amr_param['description'] == tm_param.description assert amr_param['value'] == tm_param.value - assert amr_param.get('units', {}) == (tm_param.units if tm_param.units else {}) + assert amr_param.get('units', {}) == ( + tm_param.units if tm_param.units else {}) def test_stockflow_initial_to_mira(): sfamr = stockflow_set_up_file() tm = stockflow.model_from_url(stockflow_example) - for amr_initial, tm_initial in zip(sfamr['semantics']['ode']['initials'], tm.initials.values()): + for amr_initial, tm_initial in zip(sfamr['semantics']['ode']['initials'], + tm.initials.values()): assert amr_initial['target'] == tm_initial.concept.name assert amr_initial['expression'] == str(tm_initial.expression) - assert amr_initial['expression_mathml'] == expression_to_mathml(tm_initial.expression) + assert amr_initial['expression_mathml'] == expression_to_mathml( + tm_initial.expression) def test_stockflow_stock_to_concept(): @@ -118,33 +155,22 @@ def test_regnet_rate_laws(): # Make a simple template model with rate laws, then export # into AMR, ingest, then make sure we get proper rate laws back # out. - template_model = TemplateModel( - templates=[ - ControlledReplication( - name='replication', - controller=Concept(name='A'), - subject=Concept(name='B'), - rate_law=SympyExprStr(sympy.sympify('k * A * B / (1 + B)')) - ), - NaturalDegradation( - name='degradation', - subject=Concept(name='B'), - rate_law=SympyExprStr(sympy.sympify('k * B')) - ) - ], - observables={ - 'obs1': Observable( - name='obs1', - expression=SympyExprStr(sympy.sympify('A + B')), - display_name='obs1' - ) - }, - time=Time(name='timexx') - ) amr_json = template_model_to_regnet_json(template_model) tm = regnet.template_model_from_amr_json(amr_json) assert isinstance(tm.templates[0].rate_law, SympyExprStr) assert isinstance(tm.templates[1].rate_law, SympyExprStr) - assert tm.templates[1].rate_law.args[0].equals(sympy.sympify('k * A * B / (1 + B)')) + assert tm.templates[1].rate_law.args[0].equals( + sympy.sympify('k * A * B / (1 + B)')) assert tm.time.name == 'timexx' - assert isinstance(tm.observables['obs1'].expression, SympyExprStr) \ No newline at end of file + assert isinstance(tm.observables['obs1'].expression, SympyExprStr) + + +def test_serialization(): + """Test to see if we can serialize template models that contain + datetime in their annotations into different frameworks""" + amrs = [template_model_to_regnet_json(template_model), + template_model_to_petrinet_json(template_model), + template_model_to_stockflow_json(template_model) + ] + for amr in amrs: + json.dumps(amr) From 8bd0532b4ea8587f3657230159193f8dfb38b6cf Mon Sep 17 00:00:00 2001 From: nanglo123 Date: Fri, 28 Jun 2024 14:37:47 -0400 Subject: [PATCH 4/5] Extract all annotation properties and update test --- mira/metamodel/template_model.py | 1 + mira/sources/amr/petrinet.py | 11 +++++++++- mira/sources/amr/regnet.py | 11 +++++++++- mira/sources/amr/stockflow.py | 10 ++++++++- tests/test_amr_source.py | 36 +++++++++++++++++++++++++++++--- 5 files changed, 63 insertions(+), 6 deletions(-) diff --git a/mira/metamodel/template_model.py b/mira/metamodel/template_model.py index d525db05e..6aa94911a 100644 --- a/mira/metamodel/template_model.py +++ b/mira/metamodel/template_model.py @@ -8,6 +8,7 @@ "Time", "model_has_grounding", "Concept", + "Author" ] import datetime diff --git a/mira/sources/amr/petrinet.py b/mira/sources/amr/petrinet.py index 4cf22c875..970da47b2 100644 --- a/mira/sources/amr/petrinet.py +++ b/mira/sources/amr/petrinet.py @@ -216,7 +216,16 @@ def template_model_from_amr_json(model_json) -> TemplateModel: # 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) + + annotations = model_json.get('metadata', {}).get('annotations', {}) + annotation_attributes = {"name": name, "description": description} + for key, val in annotations.items(): + # convert list of author names to list of author objects + if key == "authors": + val = [Author(name=author_dict["name"]) for author_dict in val] + annotation_attributes[key] = val + + anns = Annotations(**annotation_attributes) return TemplateModel(templates=templates, parameters=mira_parameters, initials=initials, diff --git a/mira/sources/amr/regnet.py b/mira/sources/amr/regnet.py index 39c06d1d8..ed6646823 100644 --- a/mira/sources/amr/regnet.py +++ b/mira/sources/amr/regnet.py @@ -173,7 +173,16 @@ def template_model_from_amr_json(model_json) -> TemplateModel: # 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) + + annotations = model_json.get('metadata', {}).get('annotations', {}) + annotation_attributes = {"name": name, "description": description} + for key, val in annotations.items(): + # convert list of author names to list of author objects + if key == "authors": + val = [Author(name=author_dict["name"]) for author_dict in val] + annotation_attributes[key] = val + + anns = Annotations(**annotation_attributes) return TemplateModel(templates=templates, parameters=mira_parameters, initials=initials, diff --git a/mira/sources/amr/stockflow.py b/mira/sources/amr/stockflow.py index 206a4be46..169b428c2 100644 --- a/mira/sources/amr/stockflow.py +++ b/mira/sources/amr/stockflow.py @@ -135,8 +135,16 @@ def template_model_from_amr_json(model_json) -> TemplateModel: # 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) + annotations = model_json.get('metadata', {}).get('annotations', {}) + annotation_attributes = {"name": name, "description": description} + for key, val in annotations.items(): + # convert list of author names to list of author objects + if key == "authors": + val = [Author(name=author_dict["name"]) for author_dict in val] + annotation_attributes[key] = val + + anns = Annotations(**annotation_attributes) return TemplateModel(templates=templates, parameters=mira_parameters, initials=initials, diff --git a/tests/test_amr_source.py b/tests/test_amr_source.py index d5af156d9..0b290f5a7 100644 --- a/tests/test_amr_source.py +++ b/tests/test_amr_source.py @@ -41,8 +41,19 @@ ) }, annotations=Annotations( + name="test_name", + description="test_description", + diseases=["test_disease"], + hosts=["test_host"], + license="test_license", + locations=["test_location"], + model_types=["test_model_type"], + pathogens=["test_pathogen"], + references=["test_reference"], + authors=[Author(name="test_author")], time_start="2020-03-01T00:00:00", - time_end="2020-08-01T00:00:00" + time_end="2020-08-01T00:00:00", + time_scale="days" ), time=Time(name='timexx'), ) @@ -165,12 +176,31 @@ def test_regnet_rate_laws(): assert isinstance(tm.observables['obs1'].expression, SympyExprStr) -def test_serialization(): +def test_annotation_serialization_ingestion(): """Test to see if we can serialize template models that contain - datetime in their annotations into different frameworks""" + datetime in their annotations into different frameworks. + + Also test to see if we can extract all annotation related attributes + when we ingest an amr + """ amrs = [template_model_to_regnet_json(template_model), template_model_to_petrinet_json(template_model), template_model_to_stockflow_json(template_model) ] for amr in amrs: json.dumps(amr) + + # test to see if we can extract all annotation attributes during ingestion + # for each framework + regnet_tm = regnet.template_model_from_amr_json(amrs[0]) + petrinet_tm = petrinet.template_model_from_amr_json(amrs[1]) + stockflow_tm = stockflow.template_model_from_amr_json(amrs[2]) + + zipped_annotations = zip(regnet_tm.annotations.dict().values(), + petrinet_tm.annotations.dict().values(), + stockflow_tm.annotations.dict().values()) + + for annotation_attribute_tuple in zipped_annotations: + assert annotation_attribute_tuple[0] + assert annotation_attribute_tuple[1] + assert annotation_attribute_tuple[2] From d2a62b0e2a8964f1dfef2b4cc643fa2bcf065bc8 Mon Sep 17 00:00:00 2001 From: nanglo123 Date: Fri, 28 Jun 2024 16:15:52 -0400 Subject: [PATCH 5/5] Don't convert time to string if the time is none for annotations --- mira/modeling/amr/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mira/modeling/amr/utils.py b/mira/modeling/amr/utils.py index d701ffca9..ef975b713 100644 --- a/mira/modeling/amr/utils.py +++ b/mira/modeling/amr/utils.py @@ -3,7 +3,7 @@ def add_metadata_annotations(metadata, model): metadata['annotations'] = {} return annotations_subset = { - k: str(v) if k in ["time_start", "time_end"] else v + k: (str(v) if k in ["time_start", "time_end"] and v is not None else v) for k, v in model.template_model.annotations.dict().items() if k not in ["name", "description"] # name and description already have a privileged place