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

Draft implementation of compositional grounding #129

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
311 changes: 267 additions & 44 deletions mira/metamodel/templates.py

Large diffs are not rendered by default.

11 changes: 6 additions & 5 deletions mira/modeling/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
__all__ = ["Model", "Transition", "Variable", "ModelParameter"]

import logging
from typing import Mapping, Optional
from typing import Dict, Hashable, Mapping, Optional

from mira.metamodel import (
Concept,
ControlledConversion,
ControlledProduction,
GroupedControlledConversion,
Expand Down Expand Up @@ -53,13 +54,13 @@ def get_parameter_key(transition_key, action):
class Model:
def __init__(self, template_model):
self.template_model = template_model
self.variables = {}
self.variables: Dict[Hashable, Variable] = {}
self.parameters = {}
self.transitions = {}
self.make_model()

def assemble_variable(
self, concept, initials: Optional[Mapping[str, Initial]] = None,
self, concept: Concept, initials: Optional[Mapping[str, Initial]] = None,
):
"""Assemble a variable from a concept and optional
dictionary of initial values.
Expand All @@ -80,8 +81,8 @@ def assemble_variable(
("identity", f"{k}:{v}")
for k, v in concept.get_included_identifiers().items()
)
context_key = sorted(concept.context.items())
key = [concept.name] + grounding_key + context_key
context_key = concept.get_properties_key()
key = [concept.name, *grounding_key, *context_key]
key = tuple(key) if len(key) > 1 else key[0]
if key in self.variables:
return self.variables[key]
Expand Down
10 changes: 9 additions & 1 deletion mira/modeling/viz.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@
]


def _process_context_keys(contexts):
# see Concept.get_properties_key for the structure
for context in contexts:
yield context[1], context[3] or context[4]

class GraphicalModel:
"""Create a graphical representation of a transition model."""

Expand All @@ -30,7 +35,10 @@ def __init__(self, model: Model):
label = name
shape = "oval"
else:
cc = " | ".join(f"{{{k} | {v}}}" for k, v in itt.chain(identifiers, contexts))
cc = " | ".join(
f"{{{k} | {v}}}"
for k, v in itt.chain(identifiers, _process_context_keys(contexts))
)
label = f"{{{name} | {cc}}}"
shape = "record"
self.graph.add_node(
Expand Down
12 changes: 7 additions & 5 deletions mira/sources/petri.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,11 +95,13 @@ def state_to_concept(state):
identifiers = {}
# Example: 'mira_context': "[('city', 'geonames:5128581')]"
context = dict(ast.literal_eval(state['mira_context']))
return Concept(name=state['sname'],
identifiers=identifiers,
context=context,
initial_value=state.get('mira_initial_value'))

concept = Concept(
name=state['sname'],
identifiers=identifiers,
initial_value=state.get('mira_initial_value'),
)
concept = concept.with_context(**context)
return concept

def transition_to_templates(transition, input_concepts, output_concepts,
controller_concepts):
Expand Down
30 changes: 20 additions & 10 deletions mira/sources/sbml.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ def __init__(self):

def parse_uri(self, uri):
if self.converter is None:
# self.converter = bioregistry.get_default_converter()
self.converter = curies.Converter.from_reverse_prefix_map(
bioregistry.manager.get_reverse_prefix_map(include_prefixes=True)
)
Expand Down Expand Up @@ -451,7 +452,7 @@ def _extract_concepts(sbml_model, *, model_id: Optional[str] = None) -> Mapping[
annotation_string = species.getAnnotationString()
if not annotation_string:
logger.debug(f"[{model_id} species:{species_id}] had no annotations")
concepts[species_id] = Concept(name=species_name, identifiers={}, context={})
concepts[species_id] = Concept(name=species_name)
continue

annotation_tree = etree.fromstring(annotation_string)
Expand Down Expand Up @@ -494,23 +495,32 @@ def _extract_concepts(sbml_model, *, model_id: Optional[str] = None) -> Mapping[
name=species_name or species_id,
identifiers=identifiers,
# TODO how to handle multiple properties? can we extend context to allow lists?
context=context,
)
).with_context(**context)
concept = grounding_normalize(concept)
concepts[species_id] = concept

return concepts


def grounding_normalize(concept):
def grounding_normalize(concept: Concept) -> Concept:
# Has property acquired immunity == immune population
if not concept.get_curie()[0] and \
concept.context == {'property': 'ido:0000621'}:
concept_curie = concept.get_curie()

# Rewrite property to main identifier - this is specific to BioModels
if (
not concept_curie[0] # i.e., not grounded
and concept.has_object_property("property", "ido:0000621")
):
concept.identifiers['ido'] = '0000592'
concept.context = {}
elif concept.get_curie() == ('ido', '0000514') and \
concept.context == {'property': 'ido:0000468'}:
concept.context = {}
concept.properties = []

# Delete annotation of "susceptibility to infectious agent" -
# this is specific to BioModels.
elif (
concept_curie == ('ido', '0000514')
and concept.has_object_property("property", "ido:0000468")
):
concept.properties = []
return concept


Expand Down
188 changes: 102 additions & 86 deletions notebooks/ASKEM MIRA demo.ipynb

Large diffs are not rendered by default.

158 changes: 158 additions & 0 deletions notebooks/compositional_grounding_demo.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "3196f03c",
"metadata": {},
"source": [
"# Compositional Grounding\n",
"\n",
"**Who cares?**\n",
"\n",
"Anyone using DKG to endow semantics on to their data strutures (TA1, TA2, TA3) but\n",
"want to make new terms based on existing terms\n",
"\n",
"## First Steps - Make a Concept"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "285b19b3",
"metadata": {},
"outputs": [],
"source": [
"from mira.metamodel.templates import Concept\n",
"\n",
"susceptible = Concept(\n",
" name=\"susceptible_population\",\n",
" # Correspond to nodes in the DKG\n",
" identifiers={\"ido\": \"0000514\"},\n",
")"
]
},
{
"cell_type": "markdown",
"id": "c759e283",
"metadata": {},
"source": [
"## Old Way to add \"Context\""
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "49266bce",
"metadata": {},
"outputs": [],
"source": [
"susceptible_vaccinated = Concept(\n",
" name=\"susceptible_population\",\n",
" # Correspond to nodes in the DKG\n",
" identifiers={\"ido\": \"0000514\"},\n",
" context={\n",
" \"vaccination_status\": \"vo:0001376\",\n",
" }\n",
")"
]
},
{
"cell_type": "markdown",
"id": "3678a60a",
"metadata": {},
"source": [
"## Problems with the Old Way\n",
"\n",
"1. Not much structure\n",
"2. Doesn't enable ascribing semantics to what each entry in this \"context\" dictionary means\n",
"3. Can't differentiate between references to ontology terms or primitive data types (int, float, bool, str)"
]
},
{
"cell_type": "markdown",
"id": "5ba61bff",
"metadata": {},
"source": [
"## New Way\n",
"\n",
"Flavor 1: use ontology terms:"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "6c157048",
"metadata": {},
"outputs": [],
"source": [
"from mira.metamodel.template import ObjectProperty, Term\n",
"\n",
"susceptible_vaccinated = Concept(\n",
" name=\"susceptible_population\",\n",
" identifiers={\"ido\": \"0000514\"},\n",
" properties=[\n",
" ObjectProperty(\n",
" predicate=Term(name=\"vaccination_status\"),\n",
" value=Term(name=\"vaccinated\", identifiers={\"vo\", \"0001376\"})\n",
" )\n",
" ]\n",
")"
]
},
{
"cell_type": "markdown",
"id": "842515df",
"metadata": {},
"source": [
"Flavor 2: use booleans (e.g., to reduce complexity)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "b46d78a8",
"metadata": {},
"outputs": [],
"source": [
"from mira.metamodel.template import DataProperty\n",
"\n",
"Concept.parse_raw({...})\n",
"\n",
"susceptible_vaccinated = Concept(\n",
" name=\"susceptible_population\",\n",
" identifiers={\"ido\": \"0000514\"},\n",
" properties=[\n",
" DataProperty(\n",
" predicate=Term(\n",
" name=\"vaccination_status\", \n",
" identifiers={\"vo\", \"0001376\"},\n",
" )\n",
" value=True\n",
" )\n",
" ]\n",
")"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.0"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
10 changes: 9 additions & 1 deletion tests/test_petri_source.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from mira.examples.sir import sir, susceptible, infected, recovered
from mira.metamodel import *
from mira.metamodel.templates import ObjectProperty, Term, _index_properties
from mira.modeling import Model
from mira.modeling.petri import PetriNetModel
from mira.sources.petri import state_to_concept, template_model_from_petri_json
Expand All @@ -12,7 +13,14 @@ def test_state_to_concept():
concept = state_to_concept(state)
assert concept.name == 'susceptible_population'
assert concept.identifiers == {'ido': '0000514'}
assert concept.context == {'city': 'geonames:5128581'}

properties_index = _index_properties(concept.properties)
assert properties_index == {
("", "city"): ObjectProperty(
value=Term(name="", identifiers={"geonames": "5128581" }),
predicate=Term(name="city", identifiers={})
)
}


def test_petri_reverse():
Expand Down
34 changes: 30 additions & 4 deletions tests/test_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from mira.metamodel import ControlledConversion, Concept, NaturalConversion, \
NaturalDegradation, Template, GroupedControlledConversion, \
GroupedControlledProduction
from mira.metamodel.templates import Config
from mira.metamodel.templates import Config, DataProperty, ObjectProperty, Term
from mira.dkg.web_client import is_ontological_child_web

# Provide to tests that are not meant to test ontological refinements;
Expand Down Expand Up @@ -94,14 +94,35 @@ def test_concepts_equal():
c1_w_ctx = c1.with_context(location="Berlin")
c2 = Concept(name="infected population", identifiers={"ido": "0000511"})
c2_w_ctx = c2.with_context(location="Stockholm")
c3 = Concept(name="infected population", context={"location": "Stockholm"})
c4 = Concept(name="infected population", context={"location": "Berlin"})
c3 = Concept(name="infected population", properties=[
ObjectProperty(
predicate=Term(name="location"),
value=Term(name="Stockholm"),
)
])
c4 = Concept(name="infected population", properties=[
ObjectProperty(
predicate=Term(name="location"),
value=Term(name="Berlin"),
)
])
c5 = Concept(name="infected population", properties=[
ObjectProperty(
predicate=Term(name="location"),
value=Term(name="Berlin"),
),
DataProperty(
predicate=Term(name="vaccination status"),
value=True,
),
])

assert c1.is_equal_to(c2)
assert not c1_w_ctx.is_equal_to(c2_w_ctx, with_context=True)
assert c1_w_ctx.is_equal_to(c2_w_ctx, with_context=False)
assert c3.is_equal_to(c4, with_context=False)
assert not c3.is_equal_to(c4, with_context=True)
assert not c4.is_equal_to(c5, with_context=True)


def test_template_type_inequality_is_equal():
Expand Down Expand Up @@ -206,7 +227,8 @@ def test_concept_refinement_grounding():
def test_concept_refinement_simple_context():
spatial_region_gnd = Concept(name="spatial region", identifiers={"bfo": "0000006"})
spatial_region_ctx = spatial_region_gnd.with_context(location="Stockholm")
assert len(spatial_region_ctx.context)
spatial_region_ctx_2 = spatial_region_ctx.with_context(status="Yup")
assert 1 == len(spatial_region_ctx.properties)
kw = {"refinement_func": is_ontological_child_web, "with_context": True}

# Test both empty
Expand All @@ -218,6 +240,10 @@ def test_concept_refinement_simple_context():
# Test other has context, refined does not
assert not spatial_region_gnd.refinement_of(spatial_region_ctx, **kw)

# Test 2-deep refinement
assert spatial_region_ctx_2.refinement_of(spatial_region_ctx, **kw)
assert not spatial_region_ctx.refinement_of(spatial_region_ctx_2, **kw)


def test_concept_refinement_context():
spatial_region_gnd = Concept(name="spatial region", identifiers={"bfo": "0000006"})
Expand Down