Source code for mira.metamodel.template_model

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