import copy
__all__ = [
"Annotations",
"TemplateModel",
"Initial",
"Parameter",
"Distribution",
"Observable",
"Time",
"model_has_grounding",
"Concept",
"Author"
]
import datetime
import sys
from typing import List, Dict, Set, Optional, Mapping, Tuple
import networkx as nx
import sympy
import mira.metamodel.io
from .templates import *
from .units import Unit
from .utils import safe_parse_expr
[docs]class Initial:
"""Initial conditions for parameters in the model.
Attributes
----------
concept : Concept
The concept associated with the initial.
expression : sympy.Expr or float or int
The expression for the initial.
"""
def __init__(self, concept, expression):
self.concept = concept
if isinstance(expression, int):
expression = sympy.Integer(expression)
elif isinstance(expression, float):
expression = sympy.Float(expression)
self.expression = expression
def __repr__(self):
expr = self.expression
# Convert sympy.Float to Python float for cleaner
# display (avoids e.g. 5.00000000000000)
if isinstance(expr, sympy.Float):
expr = float(expr)
return f"Initial({self.concept}, {expr})"
def __str__(self):
return self.__repr__()
[docs] def to_json(self):
"""Return a JSON-compatible dict."""
return {
"concept": self.concept.to_json(),
"expression": str(self.expression),
}
[docs] @classmethod
def from_json(cls, data, locals_dict=None):
"""Return an Initial from a dictionary.
Parameters
----------
data : dict
Mapping of Initial attributes to values.
locals_dict : dict
Mapping of string symbols to their sympy equivalent.
Returns
-------
:
The newly created initial.
"""
expression_str = data.pop("expression")
concept_json = data.pop("concept")
concept = Concept.from_json(concept_json)
expression = safe_parse_expr(expression_str, local_dict=locals_dict)
return cls(concept=concept, expression=expression)
[docs] def substitute_parameter(self, name, value):
"""Substitute a parameter value into the initial expression.
Parameters
----------
name : str
The name of the parameter to substitute.
value :
The value to substitute.
"""
self.expression = self.expression.subs(sympy.Symbol(name), value)
[docs] def get_parameter_names(self, known_param_names):
"""Get the names of all parameters in the
expression.
Parameters
----------
known_param_names : list of str
List of symbols that are known to be parameters, typically from
the list of parameters of a model.
Returns
-------
:
The set of parameter names.
"""
param_names = {str(s) for s in self.expression.free_symbols} \
& set(known_param_names)
return param_names
[docs]class Distribution:
"""A distribution of values for a parameter.
Attributes
----------
type : str
The type of distribution as provided by ProbOnto,
e.g. 'StandardUniform1', 'Beta1', etc.
parameters : dict
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.
"""
def __init__(self, type, parameters):
self.type = type
self.parameters = parameters
def __repr__(self):
return f"Distribution({self.type}, {self.parameters})"
def __str__(self):
return self.__repr__()
[docs] def to_json(self):
"""Return a JSON-compatible dict."""
return {
"type": self.type,
"parameters": {
k: str(v) if isinstance(v, sympy.Expr) else v
for k, v in self.parameters.items()
},
}
[docs] def get_expression_parameter_names(self, known_param_names):
"""Get the names of all parameters used in expressions, if any.
Note this only applies to parameters that are referenced in custom
expressions defining the distribution parameters.
Parameters
----------
known_param_names : list of str
List of symbols that are known to be parameters, typically from
the list of parameters of a model.
Returns
-------
:
The set of parameter names.
"""
expr_params = set()
for param_expr in self.parameters.values():
if isinstance(param_expr, sympy.Expr):
expr_params |= {str(s) for s in param_expr.free_symbols}
return expr_params & set(known_param_names)
[docs] def substitute_parameter(self, name, value):
"""Substitute a value into the distribution parameter expressions.
Parameters
----------
name : str
The name of the parameter to substitute.
value :
The value to substitute.
"""
for k, v in self.parameters.items():
if isinstance(v, sympy.Expr):
self.parameters[k] = v.subs(sympy.Symbol(name), value)
[docs]class Parameter(Concept):
"""A Parameter is a special type of Concept that carries a value.
Attributes
----------
name : str
The name of the parameter.
value : Optional[float]
Value of the parameter.
distribution : Optional[Distribution]
A distribution of values for the parameter.
display_name : Optional[str]
An optional display name for the parameter.
description : Optional[str]
An optional description of the parameter.
identifiers : dict
A mapping of namespaces to identifiers.
context : dict
A mapping of context keys to values.
units : Optional[Unit]
The units of the parameter.
"""
def __init__(self, name, value=None, distribution=None, display_name=None,
description=None, identifiers=None, context=None,
units=None):
super().__init__(name=name, display_name=display_name,
description=description, identifiers=identifiers,
context=context, units=units)
self.value = value
self.distribution = distribution
def __repr__(self):
parts = [repr(self.name)]
if self.value is not None:
parts.append(f"value={self.value}")
if self.distribution:
parts.append(f"distribution={self.distribution}")
if self.identifiers:
parts.append(f"identifiers={self.identifiers}")
if self.units:
parts.append(f"units={self.units}")
return f"Parameter({', '.join(parts)})"
def __str__(self):
return self.__repr__()
[docs] @classmethod
def from_json(cls, data):
"""Return a Parameter from a dictionary."""
data = dict(data)
if data.get('units'):
data['units'] = Unit.from_json(data['units'])
if data.get('distribution'):
data['distribution'] = \
Distribution(**data['distribution'])
return cls(**data)
[docs] def to_json(self):
"""Return a JSON-compatible dict."""
d = super().to_json()
if self.value is not None:
d["value"] = self.value
if self.distribution is not None:
d["distribution"] = self.distribution.to_json()
return d
[docs]class Observable(Concept):
"""An observable is a special type of Concept that carries an expression.
Observables are used to define the readouts of a model, useful when a
readout is not defined as a state variable but is rather a function of
state variables.
Attributes
----------
name : str
The name of the observable.
expression : sympy.Expr
The expression for the observable.
display_name : Optional[str]
An optional display name for the observable.
description : Optional[str]
An optional description of the observable.
identifiers : dict
A mapping of namespaces to identifiers.
context : dict
A mapping of context keys to values.
units : Optional[Unit]
The units of the observable.
"""
def __init__(self, name, expression, display_name=None, description=None,
identifiers=None, context=None, units=None):
super().__init__(name=name, display_name=display_name,
description=description, identifiers=identifiers,
context=context, units=units)
self.expression = expression
def __repr__(self):
return f"Observable({self.name}, {self.expression})"
def __str__(self):
return self.__repr__()
[docs] def to_json(self):
"""Return a JSON-compatible dict."""
d = super().to_json()
d["expression"] = str(self.expression)
return d
[docs] def substitute_parameter(self, name, value):
"""Substitute a parameter value into the observable expression.
Parameters
----------
name : str
The name of the parameter to substitute.
value :
The value to substitute.
"""
self.expression = self.expression.subs(sympy.Symbol(name), value)
[docs] def get_parameter_names(self, known_param_names):
"""Get the names of all parameters in the expression.
Parameters
----------
known_param_names : list of str
List of symbols that are known to be parameters, typically from
the list of parameters of a model.
Returns
-------
:
The set of parameter names.
"""
return {str(s) for s in self.expression.free_symbols} & \
set(known_param_names)
[docs]class Time:
"""A special type of Concept that represents time.
Attributes
----------
name : str
The symbol of the time variable in the model.
units : Optional[Unit]
The units of the time variable.
"""
def __init__(self, name="t", units=None):
self.name = name
self.units = units
def __repr__(self):
parts = [repr(self.name)]
if self.units:
parts.append(f"units={self.units}")
return f"Time({', '.join(parts)})"
def __str__(self):
return self.__repr__()
[docs] def to_json(self):
"""Return a JSON-compatible dict."""
d = {"name": self.name}
if self.units is not None:
d["units"] = self.units.to_json()
return d
[docs]class Author:
"""A metadata model for an author.
Attributes
----------
name : str
The name of the author.
"""
def __init__(self, name):
self.name = name
def __repr__(self):
return f"Author('{self.name}')"
def __str__(self):
return self.__repr__()
[docs] def to_json(self):
"""Return a JSON-compatible dict."""
return {"name": self.name}
[docs]class Annotations:
"""A metadata model for model-level annotations.
Examples in this metadata model are taken from
https://www.ebi.ac.uk/biomodels/BIOMD0000000956,
a well-annotated SIR model in the BioModels database.
Attributes
----------
name : Optional[str]
A human-readable label for the model. Example: "SIR model of scenarios
of COVID-19 spread in CA and NY"
description : Optional[str]
A description of the model.
license : Optional[str]
Information about the licensing of the model artifact. Ideally given
as an SPDX identifier like CC0 or CC-BY-4.0. Models from the BioModels
databases are all licensed under CC0. Example: "CC0"
authors : list of Author
A list of authors/creators of the model. This is not the same as the
people who e.g., submitted the model to BioModels.
references : list of str
A list of CURIEs (i.e., <prefix>:<identifier>) corresponding to
literature references that describe the model. Do not duplicate
the same publication with different CURIEs (e.g., using pubmed, pmc,
and doi). Example: ["pubmed:32616574"]
time_scale : Optional[str]
The granularity of the time element of the model, typically on the
scale of days, weeks, or months for epidemiology models.
Example: "day"
time_start : Optional[datetime.datetime]
The start time of the applicability of a model, given as a datetime.
When the time scale is not so granular, leave the less granular
fields as default, i.e., if the time scale is on months, give dates
like YYYY-MM-01 00:00.
time_end : Optional[datetime.datetime]
The end time of the applicability of a model, given as a datetime.
locations : list of str
Locations where this model is applicable, ideally annotated using
CURIEs referencing a controlled vocabulary such as GeoNames.
Example: ["geonames:5128581", "geonames:5332921"]
pathogens : list of str
Pathogens present in the model, given with CURIEs referencing
vocabulary for taxa, ideally NCBI Taxonomy. Do not confuse with
terms for annotating the disease caused by the pathogen.
Example: ["ncbitaxon:2697049"]
diseases : list of str
Diseases caused by pathogens in the model, given with CURIEs
referencing vocabulary for diseases, such as DOID, EFO, or MONDO.
Example: ["doid:0080600"]
hosts : list of str
Hosts present in the model, given with CURIEs referencing vocabulary
for taxa, ideally NCBI Taxonomy. Note that some models have multiple
hosts. Example: ["ncbitaxon:9606"]
model_types : list of str
Type(s) of the model using the Mathematical Modeling Ontology (MAMO),
which has terms like 'ordinary differential equation model',
'population model', etc. Annotated as CURIEs in the form of
mamo:<local unique identifier>.
Example: ["mamo:0000028", "mamo:0000046"]
"""
def __init__(self, name=None, description=None,
license=None, authors=None,
references=None, time_scale=None,
time_start=None, time_end=None,
locations=None, pathogens=None,
diseases=None, hosts=None,
model_types=None):
self.name = name
self.description = description
self.license = license
self.authors = authors if authors is not None else []
self.references = \
references if references is not None else []
self.time_scale = time_scale
self.time_start = time_start
self.time_end = time_end
self.locations = \
locations if locations is not None else []
self.pathogens = \
pathogens if pathogens is not None else []
self.diseases = \
diseases if diseases is not None else []
self.hosts = hosts if hosts is not None else []
self.model_types = \
model_types if model_types is not None else []
def __repr__(self):
parts = []
if self.name:
parts.append(f"name='{self.name}'")
if self.description:
parts.append(f"description='{self.description}'")
if self.authors:
parts.append(f"authors={self.authors}")
if self.references:
parts.append(f"references={self.references}")
if self.license:
parts.append(f"license='{self.license}'")
if self.time_scale:
parts.append(f"time_scale='{self.time_scale}'")
if self.time_start:
parts.append(f"time_start={self.time_start}")
if self.time_end:
parts.append(f"time_end={self.time_end}")
if self.locations:
parts.append(f"locations={self.locations}")
if self.pathogens:
parts.append(f"pathogens={self.pathogens}")
if self.diseases:
parts.append(f"diseases={self.diseases}")
if self.hosts:
parts.append(f"hosts={self.hosts}")
if self.model_types:
parts.append(f"model_types={self.model_types}")
return f"Annotations({', '.join(parts)})"
def __str__(self):
return self.__repr__()
[docs] @classmethod
def from_json(cls, data):
"""Return an Annotations from a dictionary."""
data = dict(data)
if "authors" in data:
data["authors"] = [
Author(**a) for a in data["authors"]
]
for key in ("time_start", "time_end"):
if key in data:
data[key] = datetime.datetime.fromisoformat(data[key])
return cls(**data)
[docs] def to_json(self):
"""Return a JSON-compatible dict."""
d = {}
if self.name is not None:
d["name"] = self.name
if self.description is not None:
d["description"] = self.description
if self.license is not None:
d["license"] = self.license
if self.authors:
d["authors"] = [a.to_json() for a in self.authors]
if self.references:
d["references"] = self.references
if self.time_scale is not None:
d["time_scale"] = self.time_scale
if self.time_start is not None:
d["time_start"] = self.time_start.isoformat()
if self.time_end is not None:
d["time_end"] = self.time_end.isoformat()
if self.locations:
d["locations"] = self.locations
if self.pathogens:
d["pathogens"] = self.pathogens
if self.diseases:
d["diseases"] = self.diseases
if self.hosts:
d["hosts"] = self.hosts
if self.model_types:
d["model_types"] = self.model_types
return d
[docs]class TemplateModel:
"""A template model.
Attributes
----------
templates : list of Template
A list of any child class of Templates.
parameters : dict of str to Parameter
A dict of parameter values where keys correspond to how the parameter
appears in rate laws.
initials : dict of str to Initial
A dict of initial condition values where keys correspond to concept
names they apply to.
observables : dict of str to Observable
A dict of observables that are readouts from the model.
annotations : Optional[Annotations]
A structure containing model-level annotations. Note that all
annotations are optional.
time : Optional[Time]
A structure containing time-related annotations. Note that all
annotations are optional.
"""
def __init__(self, templates, parameters=None, initials=None,
observables=None, annotations=None, time=None):
self.templates = templates
self.parameters = parameters if parameters is not None else {}
self.initials = initials if initials is not None else {}
self.observables = observables if observables is not None else {}
self.annotations = annotations
self.time = time
def __repr__(self):
parts = [f"templates={self.templates}"]
if self.parameters:
parts.append(f"parameters={self.parameters}")
if self.initials:
parts.append(f"initials={self.initials}")
if self.observables:
parts.append(f"observables={self.observables}")
if self.annotations:
parts.append(f"annotations={self.annotations}")
if self.time:
parts.append(f"time={self.time}")
return f"TemplateModel({', '.join(parts)})"
def __str__(self):
return self.__repr__()
[docs] def to_json(self):
"""Return a JSON-compatible dict."""
d = {"templates": [t.to_json() for t in self.templates]}
if self.parameters:
d["parameters"] = {k: v.to_json() for k, v
in self.parameters.items()}
if self.initials:
d["initials"] = {k: v.to_json() for k, v
in self.initials.items()}
if self.observables:
d["observables"] = {k: v.to_json() for k, v
in self.observables.items()}
if self.annotations is not None:
d["annotations"] = self.annotations.to_json()
if self.time is not None:
d["time"] = self.time.to_json()
return d
[docs] def get_parameters_from_expression(self, expression) -> Set[str]:
"""Given a symbolic expression, find its elements that are model parameters.
Expressions such as rate laws consist of some combination of participants,
rate parameters and potentially other factors. This function finds those
elements of expressions that are rate parameters.
Parameters
----------
expression : sympy.Symbol | sympy.Expr
A sympy expression or symbol, whose parameters are extracted.
Returns
-------
:
A set of parameter names (as strings).
"""
if expression is None:
return set()
params = set()
if isinstance(expression, sympy.Symbol):
if expression.name in self.parameters:
# add the string name to the set
params.add(expression.name)
# There are many sympy classes that have args that can occur here
# so it's better to check for the presence of args
elif not hasattr(expression, "args"):
raise ValueError(
f"Rate law is of invalid type {type(expression)}: {expression}"
)
else:
for arg in expression.args:
params |= self.get_parameters_from_expression(arg)
return params
[docs] def get_parameters_from_rate_law(self, rate_law) -> Set[str]:
"""Given a rate law, find its elements that are model parameters.
Rate laws consist of some combination of participants, rate parameters
and potentially other factors. This function finds those elements of
rate laws that are rate parameters.
Parameters
----------
rate_law : sympy.Symbol | sympy.Expr
A sympy expression or symbol, whose parameters are extracted.
Returns
-------
:
A set of parameter names (as strings).
"""
return self.get_parameters_from_expression(rate_law)
[docs] def update_parameters(self, parameter_dict):
"""Update parameter values.
Parameters
----------
parameter_dict : Dict[str,float]
Mapping of parameter name to value.
"""
for k, v in parameter_dict.items():
if k in self.parameters:
self.parameters[k].value = v
else:
self.parameters[k] = Parameter(name=k, value=v)
[docs] def get_all_used_parameters(self) -> Set[str]:
"""Get all parameters that are actually used in the model
Usages include rate laws of templates, observable expressions
and initial expressions.
Returns
-------
:
A set of parameter names.
"""
used_parameters = set()
for template in self.templates:
used_parameters |= template.get_parameter_names()
for observable in self.observables.values():
used_parameters |= \
observable.get_parameter_names(list(self.parameters))
for initial in self.initials.values():
used_parameters |= \
initial.get_parameter_names(list(self.parameters))
for parameter in self.parameters.values():
if parameter.distribution:
used_parameters |= \
parameter.distribution.get_expression_parameter_names(
list(self.parameters)
)
return used_parameters
[docs] def eliminate_unused_parameters(self):
"""Remove parameters that are not used in rate laws."""
used_parameters = self.get_all_used_parameters()
for k in list(self.parameters.keys()):
if k not in used_parameters:
self.parameters.pop(k)
[docs] def eliminate_duplicate_parameter(self, redundant_parameter,
preserved_parameter):
"""Eliminate a duplicate parameter from the model.
This happens when there are two redundant parameters only one of which
is actually used in the model. This function removes the redundant
parameter and updates the rate laws to use the preserved parameter.
Parameters
----------
redundant_parameter : str
The name of the parameter to remove.
preserved_parameter : str
The new name of the parameter to preserve.
"""
# Update the rate laws
for template in self.templates:
template.update_parameter_name(
redundant_parameter, preserved_parameter
)
self.parameters.pop(redundant_parameter)
[docs] @classmethod
def from_json(cls, data) -> "TemplateModel":
"""
Return a template model from a dictionary
Parameters
----------
data : Dict[str,Any]
Mapping of template model attributes to their values.
Returns
-------
:
Returns the newly created template model.
"""
# Do a copy just to make sure we don't modify the original data
data = copy.deepcopy(data)
local_symbols = {p: sympy.Symbol(p) for p in data.get("parameters", [])}
for template_dict in data.get("templates", []):
# We need to figure out the template class based on the type
# entry in the data
template_cls = getattr(sys.modules[__name__], template_dict["type"])
for concept_key in template_cls.concept_keys:
# Note the special handling here for list-like vs single
# concepts
concept_data = template_dict.get(concept_key)
if concept_data:
if not isinstance(concept_data, list):
concept_data = [concept_data]
for concept_dict in concept_data:
if concept_dict.get("name"):
local_symbols[
concept_dict.get("name")
] = sympy.Symbol(concept_dict.get("name"))
# We can now use these symbols to deserialize rate laws
templates = [
Template.from_json(template, rate_symbols=local_symbols)
for template in data["templates"]
]
#: A lookup from concept name in the model to the full
#: concept object to be used for preparing initial values
concepts = {
concept.name: concept
for template in templates
for concept in template.get_concepts()
}
# Handle parameters
parameters = {
par_key: Parameter.from_json(par_dict)
for par_key, par_dict in data.get("parameters", {}).items()
}
initials = {}
for name, value in data.get("initials", {}).items():
if isinstance(value, float):
# If the data is just a float, upgrade it to
# a :class:`Initial` instance
initials[name] = Initial(
concept=concepts[name],
expression=sympy.Float(value),
)
else:
# If the data is not a float, assume it's JSON
# for a :class:`Initial` instance and parse it to Initial
local_symbols = {
p.name: sympy.Symbol(p.name) for p in parameters.values()
}
initials[name] = Initial.from_json(
value, locals_dict=local_symbols
)
return cls(
templates=templates,
parameters=parameters,
initials=initials,
annotations=Annotations.from_json(data["annotations"]) if data.get("annotations") else None,
)
[docs] def generate_model_graph(self, use_display_name: bool = False,
concepts_only: bool = False) -> nx.DiGraph:
"""
Generate a graph based off the template model.
Parameters
----------
use_display_name :
Whether to use the display_name attribute of the concepts as the
label in the graph.
Returns
-------
:
A graph
"""
graph = nx.DiGraph()
for template in self.templates:
# Add node for template itself
node_id = get_template_graph_key(template)
if not concepts_only:
graph.add_node(
node_id,
type=template.type,
template_key=template.get_key(),
label=template.type,
color="orange",
shape="record",
)
# Add in/outgoing nodes for the concepts of this template
for role, concepts in template.get_concepts_by_role().items():
for concept in (
concepts if isinstance(concepts, list) else [concepts]
):
# Note: this includes the node's name as well as its
# grounding
concept_key = get_concept_graph_key(concept)
# Note that this doesn't include the concept's name
# in the key
concept_identity_key = concept.get_key()
context_str = "\n".join(
f"{k}-{v}" for k, v in concept.context.items()
)
context_str = "\n" + context_str if context_str else ""
if use_display_name:
name = concept.display_name or concept.name
else:
name = concept.name
if concept.get_included_identifiers():
label = (
f"{name}\n({concept.get_curie_str()})"
f"{context_str}"
)
else:
label = f"{name}{context_str}" if context_str else name
graph.add_node(
concept_key,
label=label,
color="orange",
concept_identity_key=concept_identity_key,
)
role_label = "controller" if role == "controllers" else role
if role_label in {"controller", "subject"}:
source, target = concept_key, node_id
else:
source, target = node_id, concept_key
if not concepts_only:
graph.add_edge(source, target, label=role_label)
return graph
[docs] def set_rate_law(self, template_name, rate_law, local_dict=None):
"""Set the rate law of a template with a given name."""
for template in self.templates:
if template.name == template_name:
template.set_rate_law(rate_law, local_dict=local_dict)
[docs] def draw_graph(
self,
path: str,
use_display_name: bool = False,
prog: str = "dot",
args: str = "",
format: Optional[str] = None,
):
"""Draw a pygraphviz graph of the TemplateModel.
Parameters
----------
path :
The path to the output file.
use_display_name :
Whether to use the display_name attribute of the concepts as the
label in the graph.
prog :
The graphviz layout program to use, such as "dot", "neato", etc.
format :
Set the file format explicitly.
args :
Additional arguments to pass to the graphviz bash program as a
string. Example: args="-Nshape=box -Edir=forward -Ecolor=red".
"""
# draw graph
graph = self.generate_model_graph(use_display_name=use_display_name)
agraph = nx.nx_agraph.to_agraph(graph)
agraph.draw(path, format=format, prog=prog, args=args)
[docs] def draw_jupyter(
self,
path: str = "model.png",
use_display_name: bool = False,
prog: str = "dot",
args: str = "",
format: Optional[str] = None,
):
"""
Display in jupyter.
Parameters
----------
path :
The path to the output file.
use_display_name :
Whether to use the display_name attribute of the concepts as the
label in the graph.
prog :
The graphviz layout program to use, such as "dot", "neato", etc.
format :
Set the file format explicitly.
args :
Additional arguments to pass to the graphviz bash program as a
string. Example: args="-Nshape=box -Edir=forward -Ecolor=red".
Returns
-------
: Image
The image of the graph.
"""
from IPython.display import Image
self.draw_graph(path=path, use_display_name=use_display_name,
prog=prog, args=args, format=format)
return Image(path)
[docs] def graph_as_json(self) -> Dict:
"""
Serialize the TemplateModel graph as node-link data.
Returns
-------
:
The node-link data as a dictionary.
"""
graph = self.generate_model_graph()
return nx.node_link_data(graph)
[docs] def print_params_table(self):
"""Print the table full of parameters."""
import tabulate
contexts = set()
for key, param in self.parameters.items():
contexts |= set(param.context.keys())
header = ["name", "identifier"] + sorted(contexts)
rows = [header]
for key, param in self.parameters.items():
identifier_curie = ":".join(list(param.identifiers.items())[0])
context_entries = [
param.context.get(context) for context in sorted(contexts)
]
rows.append([key, identifier_curie] + context_entries)
print(tabulate.tabulate(rows, headers="firstrow"))
[docs] def get_concepts_map(self):
"""
Return a mapping from concept keys to concepts that
appear in this template model's templates.
Returns
-------
: Dict[tuple, Concept]
The mapping of concept keys to concepts that appear in this
template model's templates.
"""
return {concept.get_key(): concept for concept in _iter_concepts(self)}
[docs] def get_concepts_name_map(self):
"""
Return a mapping from concept names to concepts that
appear in this template model's templates.
Returns
-------
: Dict[str,Concept]
Mapping of concept names to concepts that appear in this
template model's templates.
"""
return {concept.name: concept for concept in _iter_concepts(self)}
[docs] def get_concept(self, name: str) -> Optional[Concept]:
"""
Return the first concept that has the given name.
Parameters
----------
name :
The name to be queried for.
Returns
-------
:
The first concept that has the given name if it's present in the
TemplateModel.
"""
names = self.get_concepts_by_name(name)
if names:
return names[0]
return None
[docs] def reset_base_names(self):
"""Reset the base names of all concepts in this model
to the current name."""
for template in self.templates:
for concept in template.get_concepts():
concept._base_name = concept.name
for initial in self.initials.values():
initial.concept._base_name = initial.concept.name
[docs] def get_concepts_by_name(self, name: str) -> List[Concept]:
"""Return a list of all concepts that have the given name.
.. warning::
this could give duplicates if there are nodes with
compositional grounding.
Parameters
----------
name :
The name to be queried for.
Returns
-------
:
A list of concepts that have the given name.
"""
name = name.casefold()
return [
concept
for template in self.templates
for concept in template.get_concepts()
if concept.name.casefold() == name
]
[docs] def extend(
self,
template_model: "TemplateModel",
parameter_mapping: Optional[Mapping[str, Parameter]] = None,
initial_mapping: Optional[Mapping[str, Initial]] = None,
):
"""
Extend this template model with another template model.
Parameters
----------
template_model :
The template model to add
parameter_mapping :
Mapping of parameter names to `Parameter`
initial_mapping :
Mapping of initial names to `Initial`
Returns
-------
: TemplateModel
The template model with added templates from the added
template model
"""
model = self
for template in template_model.templates:
model = model.add_template(
template,
parameter_mapping=parameter_mapping,
initial_mapping=initial_mapping,
)
return model
[docs] def add_template(
self,
template: Template,
parameter_mapping: Optional[Mapping[str, Parameter]] = None,
initial_mapping: Optional[Mapping[str, Initial]] = None,
) -> "TemplateModel":
"""Add a template to the model.
Parameters
----------
template :
The template to add.
parameter_mapping :
A mapping from parameter names in the template to Parameters in
the model.
initial_mapping :
A mapping from concept names in the template to Initials in the
model.
Returns
-------
:
A new model with the additional template
"""
# todo: handle adding parameters and initials
if parameter_mapping is None and initial_mapping is None:
return TemplateModel(
templates=self.templates + [template],
parameters=self.parameters,
initials=self.initials,
observables=self.observables,
annotations=self.annotations,
time=self.time,
)
elif parameter_mapping is None:
initials = self.initials or {}
initials.update(initial_mapping or {})
return TemplateModel(
templates=self.templates + [template],
initials=initials,
parameters=self.parameters,
annotations=self.annotations,
observables=self.observables,
time=self.time,
)
elif initial_mapping is None:
parameters = self.parameters or {}
parameters.update(parameter_mapping or {})
return TemplateModel(
templates=self.templates + [template],
parameters=parameters,
initials=self.initials,
annotations=self.annotations,
observables=self.observables,
time=self.time,
)
else:
initials = self.initials or {}
initials.update(initial_mapping or {})
parameters = self.parameters or {}
parameters.update(parameter_mapping or {})
return TemplateModel(
templates=self.templates + [template],
parameters=parameters,
initials=initials,
annotations=self.annotations,
observables=self.observables,
time=self.time,
)
[docs] def add_transition(
self,
transition_name: str = None,
subject_concept: Concept = None,
outcome_concept: Concept = None,
rate_law_sympy=None,
params_dict: Mapping = None,
mass_action_parameter: Optional[Parameter] = None,
) -> "TemplateModel":
"""
Add a transition to a template model. Only Natural
templates between a source and an outcome are supported.
Multiple parameters can be added explicitly or implicitly.
Parameters
----------
transition_name :
Name of the new transition to be added.
subject_concept :
The subject of the new transition.
outcome_concept :
The outcome of the new transition.
rate_law_sympy :
The rate law associated with the new transition.
params_dict :
Mapping of parameter attribute to their respective
values.
mass_action_parameter :
The mass action parameter that will be set to the transition's
mass action rate law if provided.
Returns
-------
:
The new template model with the added transition.
"""
if subject_concept and outcome_concept:
template = NaturalConversion(
name=transition_name,
subject=subject_concept,
outcome=outcome_concept,
rate_law=rate_law_sympy,
)
elif subject_concept and outcome_concept is None:
template = NaturalDegradation(
name=transition_name,
subject=subject_concept,
rate_law=rate_law_sympy,
)
else:
template = NaturalProduction(
name=transition_name,
outcome=outcome_concept,
rate_law=rate_law_sympy,
)
if params_dict and template.rate_law:
# add explicitly parameters to template model
for free_symbol_sympy in template.rate_law.free_symbols:
free_symbol_str = str(free_symbol_sympy)
if free_symbol_str in params_dict:
name = params_dict[free_symbol_str].get("display_name")
description = params_dict[free_symbol_str].get(
"description"
)
value = params_dict[free_symbol_str].get("value")
units = params_dict[free_symbol_str].get("units")
distribution = params_dict[free_symbol_str].get(
"distribution"
)
self.add_parameter(
parameter_id=free_symbol_str,
name=name,
description=description,
value=value,
distribution=distribution,
units_mathml=units,
)
# If there are no explicitly defined parameters
# Extract new parameters from rate laws without any other information about that parameter
elif template.rate_law:
free_symbol_str = {
str(symbol) for symbol in template.rate_law.free_symbols
}
# Remove subject name from list of free symbols if the template is not NaturalProduction
if not isinstance(template, NaturalProduction):
free_symbol_str -= {template.subject.name}
free_symbol_str -= set(self.parameters)
for new_param in free_symbol_str:
self.add_parameter(new_param)
elif mass_action_parameter:
template.set_mass_action_rate_law(mass_action_parameter.name)
pm = (
{mass_action_parameter.name: mass_action_parameter}
if mass_action_parameter
else None
)
return self.add_template(template, parameter_mapping=pm)
[docs] def substitute_parameter(self, name, value=None):
"""Substitute a parameter with the value argument if supplied,
else substitute the parameter with the parameter's value.
Parameters
----------
name : str
The name of the parameter to substitute.
value :
The value to substitute.
Returns
-------
:
`None` if there does not exist a parameter with the given name.
Else not return value.
"""
if name not in self.parameters:
return
if value is None:
value = self.parameters[name].value
self.parameters = {
k: v for k, v in self.parameters.items() if k != name
}
for template in self.templates:
template.substitute_parameter(name, value)
for observable in self.observables.values():
observable.substitute_parameter(name, value)
for initial in self.initials.values():
initial.substitute_parameter(name, value)
for param in self.parameters.values():
if param.distribution:
param.distribution.substitute_parameter(name, value)
[docs] def add_parameter(
self,
parameter_id: str,
name: str = None,
description: str = None,
value: float = None,
distribution=None,
units_mathml: str = None,
):
"""
Add a parameter to the template model.
Parameters
----------
parameter_id :
The id of the parameter.
name :
The name of the parameter.
description :
The description of the parameter.
value :
The value of the newly added parameter.
distribution : Dict[str,Any]
Dictionary of distribution attributes to their values to be
passed into the `Distribution` constructor.
units_mathml :
The unit of the parameter in mathml form.
"""
distribution = Distribution(**distribution) if distribution else None
if units_mathml:
units = Unit(mira.metamodel.io.mathml_to_expression(units_mathml))
else:
units = None
data = {
"name": parameter_id,
"display_name": name,
"description": description,
"value": value,
"distribution": distribution,
"units": units,
}
parameter = Parameter(**data)
self.parameters[parameter_id] = parameter
[docs] def eliminate_parameter(self, name):
"""
Eliminate a parameter from the model by substituting 0.
Parameters
----------
name : str
The name of the parameter to be eliminated.
"""
self.substitute_parameter(name, value=0)
[docs] def set_parameters(self, param_dict):
"""
Set the parameters of this model to the values in the given dict. If a
parameter in the given dict is not a part of the model, we create
a new parameter out of it.
Parameters
----------
param_dict : Dict[str,float]
Mapping of parameter name to its new value.
"""
for name, value in param_dict.items():
if self.parameters and name in self.parameters:
self.parameters[name].value = value
else:
self.add_parameter(parameter_id=name,value=value)
[docs] def set_initials(self, initial_dict):
"""
Set the initials of this model to the expression in the given dict.
Parameters
----------
initial_dict : dict
Mapping of initial name to its new expression.
"""
for name, expression in initial_dict.items():
if self.initials and name in self.initials:
self.initials[name].expression = expression
def _iter_concepts(template_model: TemplateModel):
for template in template_model.templates:
if isinstance(template, ControlledConversion):
yield from (template.subject, template.outcome, template.controller)
elif isinstance(template, NaturalConversion):
yield from (template.subject, template.outcome)
elif isinstance(template, GroupedControlledConversion):
yield from template.controllers
yield from (template.subject, template.outcome)
elif isinstance(template, NaturalDegradation):
yield template.subject
elif isinstance(template, NaturalProduction):
yield template.outcome
elif isinstance(template, ControlledDegradation):
yield from (template.subject, template.controller)
elif isinstance(template, ControlledProduction):
yield from (template.outcome, template.controller)
elif isinstance(template, GroupedControlledProduction):
yield from template.controllers
yield template.outcome
elif isinstance(template, GroupedControlledDegradation):
yield from template.controllers
yield template.subject
elif isinstance(template, NaturalReplication):
yield template.subject
elif isinstance(template, ControlledReplication):
yield from (template.subject, template.controller)
elif isinstance(template, StaticConcept):
yield template.subject
elif isinstance(template, MultiConversion):
yield from template.subjects
yield from template.outcomes
elif isinstance(template, ReversibleFlux):
yield from template.left
yield from template.right
else:
raise TypeError(f"could not handle template: {template}")
def get_concept_graph_key(concept: Concept) -> Tuple[str, ...]:
grounding_key = ("identity", concept.get_curie_str())
context_key = tuple(i for t in sorted(concept.context.items()) for i in t)
key = (concept.name,) + grounding_key + context_key
key = tuple(key) if len(key) > 1 else (key[0],)
return key
def get_template_graph_key(template: Template) -> Tuple[str, ...]:
name: str = template.type
key = [name]
for concept in template.get_concepts():
for key_part in get_concept_graph_key(concept):
key.append(key_part)
if len(key) > 1:
return tuple(key)
else:
return (key[0],)
[docs]def model_has_grounding(
template_model: TemplateModel, prefix: str, identifier: str
) -> bool:
"""
Returns true or false if a given search curie is present within the
TemplateModel.
Parameters
----------
template_model :
The TemplateModel to query.
prefix :
The prefix of the search curie.
identifier :
The identifier of the search curie.
Returns
-------
:
"""
search_curie = f"{prefix}:{identifier}"
for template in template_model.templates:
for concept in template.get_concepts():
for concept_prefix, concept_id in concept.identifiers.items():
if concept_prefix == prefix and concept_id == identifier:
return True
for key, curie in concept.context.items():
if curie == search_curie:
return True
for key, param in template_model.parameters.items():
for param_prefix, param_id in param.identifiers.items():
if param_prefix == prefix and param_id == identifier:
return True
for key, curie in param.context.items():
if curie == search_curie:
return True
return False