wc_lang/sbml/util.py
""" Utilities for writing/reading a `wc_lang` model to/from SBML
* Higher level functions for creating, getting, and setting SBML objects
* Utilities for wrapping libSBML calls
:Author: Jonathan Karr <karr@mssm.edu>
:Author: Arthur Goldberg <Arthur.Goldberg@mssm.edu>
:Date: 2019-03-21
:Copyright: 2017-2019, Karr Lab
:License: MIT
"""
from wc_utils.util.units import unit_registry, are_units_equivalent
import libsbml
import math
import obj_tables
import obj_tables.math.expression
import obj_tables.sci.units
import re
import scipy
import types
import warnings
import wc_lang.core
class SbmlModelMixin(object):
""" Methods for exporting/import models to/from SBML """
def gen_sbml_id(self):
""" Generate SBML id from id
Returns:
:obj:`str`: SBML id
"""
return self.__class__.__name__ + '__' \
+ self.id \
.replace('[', '__RB__') \
.replace(']', '__LB__') \
.replace('-', '__DS__')
@classmethod
def parse_sbml_id(cls, sbml_id):
""" Parse id from SBML id
Args:
sbml_id (:obj:`str`): SBML id
Returns:
:obj:`str`: id
"""
return sbml_id.partition(cls.__name__ + '__')[2] \
.replace('__RB__', '[') \
.replace('__LB__', ']') \
.replace('__DS__', '-')
def export_to_sbml(self, sbml_model):
""" Add object to SBML model.
Args:
sbml_model (:obj:`libsbml.Model`): SBML model
Returns:
:obj:`libsbml.SBase`: SBML object
"""
pass # pragma: no cover
def export_relations_to_sbml(self, sbml_model, sbml):
""" Add relationships to/from object to SBML object.
Args:
sbml_model (:obj:`libsbml.Model`): SBML model
sbml (:obj:`libsbml.SBase`): SBML object
"""
pass # pragma: no cover
def import_from_sbml(self, sbml):
""" Load object from SBML object
Args:
sbml (:obj:`libsbml.SBase`): SBML object
"""
pass # pragma: no cover
def import_relations_from_sbml(self, sbml, objs):
""" Load relations to/from object from SBML object
Args:
sbml (:obj:`libsbml.SBase`): SBML object
objs (:obj:`dict`): dictionary that maps WC-Lang types to dictionaries that
map the ids of WC-Lang objects to WC-Lang objects
"""
pass # pragma: no cover
class SbmlAssignmentRuleMixin(SbmlModelMixin):
""" Methods for exporting/import expression models to/from SBML """
def export_to_sbml(self, sbml_model):
""" Add expression to a SBML model.
Args:
sbml_model (:obj:`libsbml.Model`): SBML model
Returns:
:obj:`libsbml.AssignmentRule`: SBML assignment rule
"""
sbml = LibSbmlInterface.call_libsbml(sbml_model.createAssignmentRule)
LibSbmlInterface.call_libsbml(sbml.setIdAttribute, self.gen_sbml_id())
LibSbmlInterface.call_libsbml(sbml.setName, self.name)
param_id = '__param__' + self.gen_sbml_id()
LibSbmlInterface.create_parameter(sbml_model, param_id, None, self.units, constant=False)
LibSbmlInterface.call_libsbml(sbml.setVariable, param_id)
LibSbmlInterface.set_commments(self, sbml)
return sbml
def export_relations_to_sbml(self, sbml_model, sbml):
""" Add relationships to/from object to SBML object.
Args:
sbml_model (:obj:`libsbml.Model`): SBML model
sbml (:obj:`libsbml.SBase`): SBML object
"""
LibSbmlInterface.set_math(sbml.setMath, self.expression)
LibSbmlInterface.set_annotations(self, LibSbmlInterface.gen_nested_attr_paths(['identifiers']), sbml)
def import_from_sbml(self, sbml):
""" Load expression from SBML assignment rule
Args:
sbml (:obj:`libsbml.AssignmentRule`): SBML assignment rule
"""
self.id = self.parse_sbml_id(LibSbmlInterface.call_libsbml(sbml.getIdAttribute))
self.name = LibSbmlInterface.call_libsbml(sbml.getName)
param_id = LibSbmlInterface.call_libsbml(sbml.getVariable)
sbml_model = LibSbmlInterface.call_libsbml(sbml.getModel)
_, _, _, self.units = LibSbmlInterface.parse_parameter(
LibSbmlInterface.call_libsbml(sbml_model.getParameter, param_id))
LibSbmlInterface.get_commments(self, sbml)
def import_relations_from_sbml(self, sbml, objs):
""" Load relationships to/from expression from SBML assignment rule
Args:
sbml (:obj:`libsbml.AssignmentRule`): SBML assignment rule
objs (:obj:`dict`): dictionary that maps WC-Lang types to dictionaries that
map the ids of WC-Lang objects to WC-Lang objects
"""
self.expression = LibSbmlInterface.get_math(sbml.getMath, self.Meta.expression_term_model, objs)
LibSbmlInterface.get_annotations(self, LibSbmlInterface.gen_nested_attr_paths(['identifiers']), sbml, objs)
class LibSbmlError(Exception):
''' Exception raised when libSBML returns an error '''
class LibSbmlInterface(object):
''' Methods for compactly using libSBML to create SBML objects.
The libSBML method calls provide narrow interfaces, typically exchanging one
value per call, which creates verbose code. The methods below aggregate multiple
libSBML method calls to enable more compact usage.
'''
XML_NAMESPACE = 'https://www.wholecell.org/ns/wc_lang'
@classmethod
def create_doc(cls, level=3, version=2, packages=None):
""" Create an SBMLDocument that, optionally, uses package(s).
Args:
level (:obj:`int`, optional): SBML level number
version (:obj:`int`, optional): SBML version number
packages (:obj:`dict` that maps :obj:`str` to :obj:`int`, optional): dictionary of required packages
that maps package identifiers to package numbers
Returns:
:obj:`libsbml.SBMLDocument`: SBML document
"""
packages = packages or {}
# create name spaces for SBML document
namespaces = [level, version]
for package_id, package_version in packages.items():
namespaces.append(package_id)
namespaces.append(package_version)
sbml_ns = cls.call_libsbml(libsbml.SBMLNamespaces, *namespaces)
cls.call_libsbml(sbml_ns.addNamespace, cls.XML_NAMESPACE, 'wcLang')
# create SBML document
sbml_doc = cls.call_libsbml(libsbml.SBMLDocument, sbml_ns)
# set package requirements
for package in packages:
cls.call_libsbml(sbml_doc.setPackageRequired, package, False)
# return SBML document
return sbml_doc
@classmethod
def verify_doc(cls, sbml_doc, level=3, version=2, strict_units=True):
""" Verify that an SBML document is valid, and raise an exception if the document is not valid
* SBML-compatible
* Valid SBML
* Consistent
Args:
sbml_doc (:obj:`libsbml.SBMLDocument`): SBML document
level (:obj:`int`, optional): SBML level number
version (:obj:`int`, optional): SBML version number
strict_units (:obj:`bool`, optional): if :obj:`True`, strictly verify that the units are
consistent
"""
cls.verify_doc_is_compatible(sbml_doc, level=level, version=version)
cls.verify_doc_is_valid_sbml(sbml_doc)
cls.verify_doc_is_consistent(sbml_doc, strict_units=strict_units)
@classmethod
def verify_doc_is_compatible(cls, sbml_doc, level=3, version=2):
""" Check the compatibility of an SBML document with a specific level and version,
and raise an exception if the document is not verify_doc_is_compatible
Args:
sbml_doc (:obj:`libsbml.SBMLDocument`): SBML document
level (:obj:`int`, optional): SBML level number
version (:obj:`int`, optional): SBML version number
"""
# SBML compatibility method for the version being used
method = getattr(sbml_doc, 'checkL{}v{}Compatibility'.format(level, version))
cls.call_libsbml(method, returns_int=True)
cls.raise_if_error(sbml_doc, 'Document is incompatible')
@classmethod
def verify_doc_is_valid_sbml(cls, sbml_doc):
""" Check that an SBML document is valid,
and raise an exception if the document is not valid
Args:
sbml_doc (:obj:`libsbml.SBMLDocument`): SBML document
"""
cls.call_libsbml(sbml_doc.validateSBML, returns_int=True)
cls.raise_if_error(sbml_doc, 'Document is invalid SBML')
@classmethod
def verify_doc_is_consistent(cls, sbml_doc, strict_units=True):
""" Check that an SBML document is consistent,
and raise an exception if the document is not consistent
Args:
sbml_doc (:obj:`libsbml.SBMLDocument`): SBML document
strict_units (:obj:`bool`, optional): if :obj:`True`, strictly verify that the units are
consistent
"""
cls.call_libsbml(sbml_doc.checkInternalConsistency, returns_int=True)
cls.raise_if_error(sbml_doc, 'Document is internally inconsistent')
if strict_units:
method = sbml_doc.checkConsistencyWithStrictUnits
else:
method = sbml_doc.checkConsistency
cls.call_libsbml(method, returns_int=True)
cls.raise_if_error(sbml_doc, 'Document is inconsistent')
@classmethod
def create_model(cls, sbml_doc):
""" Create a SBML model
Args:
sbml_doc (:obj:`libsbml.SBMLDocument`): SBML document
Returns:
:obj:`libsbml.Model`: SBML model
"""
return cls.call_libsbml(sbml_doc.createModel)
@classmethod
def init_model(cls, model, sbml_doc, packages=None):
""" Create and initialize an SMBL model.
Args:
model (:obj:`wc_lang.core.Model`): model
sbml_doc (:obj:`libsbml.SBMLDocument`): a `libsbml` SBMLDocument
packages (:obj:`dict` that maps :obj:`str` to :obj:`int`, optional): dictionary of required packages
that maps package identifiers to package numbers
Returns:
:obj:`libsbml.Model`: the SBML model
"""
# create model
sbml_model = cls.create_model(sbml_doc)
# enable plugins for packages
packages = packages or {}
for package_id in packages.keys():
plugin = cls.call_libsbml(sbml_model.getPlugin, package_id)
cls.call_libsbml(plugin.setStrict, True)
# Set units
cls.create_units(model, sbml_model)
# return SBML model
return sbml_model
@classmethod
def create_units(cls, model, sbml_model):
""" Set time, extent, and substance units of SBML model
Args:
model (:obj:`wc_lang.core.Model`): model
sbml_model (:obj:`libsbml.Model`): SBML model that encodes the model
Returns:
:obj:`dict`: dictionary that maps units to ids of SBML unit definitions
"""
# Define units
units = obj_tables.sci.units.get_obj_units(model)
units_to_sbml = {}
for unit in units:
sbml_unit = cls.create_unit(unit, sbml_model)
if sbml_unit:
units_to_sbml[unit] = cls.call_libsbml(sbml_unit.getIdAttribute)
# set top-level units (time, substance, extent, volume)
cls.set_unit(sbml_model.setTimeUnits, model.time_units)
assert len(wc_lang.core.Species.units.choices) == 1
units = wc_lang.core.Species.units.choices[0]
cls.set_unit(sbml_model.setExtentUnits, units)
cls.set_unit(sbml_model.setSubstanceUnits, units)
assert len(wc_lang.core.InitVolume.units.choices) == 1
units = wc_lang.core.InitVolume.units.choices[0]
cls.set_unit(sbml_model.setVolumeUnits, units)
# return dictionary to SBML units
return units_to_sbml
@classmethod
def create_unit(cls, unit, sbml_model):
""" Add a unit definition to a SBML model
Args:
unit (:obj:`unit_registry.Unit`): unit
sbml_model (:obj:`libsbml.Model`): SBML model that encodes the model
Returns:
:obj:`libsbml.UnitDefinition`: unit definition
"""
id = cls.gen_unit_id(unit)
if not id.startswith('unit_'):
return None
unit_def = cls.call_libsbml(sbml_model.createUnitDefinition)
cls.call_libsbml(unit_def.setIdAttribute, id)
unit_registry = unit._REGISTRY
original_unit = unit
quant = unit_registry.parse_expression(str(unit))
mag, bases = quant.to_reduced_units().to_tuple()
mole_bases = []
has_molecule = False
for base, exp in bases:
if base == 'molecule':
has_molecule = True
base = 'mole'
mole_bases.append((base, exp))
mole_bases = tuple(mole_bases)
if has_molecule:
quant = unit_registry.Quantity.from_tuple((mag, mole_bases))
magnitude, root_units = quant.to_root_units().to_tuple()
scale = int(math.floor(math.log10(magnitude)))
multiplier = magnitude / pow(10, scale)
if not root_units:
root_units = [('dimensionless', 1.), ]
if are_units_equivalent(original_unit, unit_registry.parse_units('molecule / mole')):
scale = 0
multiplier = 1.
root_units = [('dimensionless', 1.), ]
elif are_units_equivalent(original_unit, unit_registry.parse_units('M')):
scale = 0
multiplier = 1.
root_units = [
('mole', 1.),
('liter', -1.),
]
for i_root_unit, (kind, exponent) in enumerate(root_units):
if i_root_unit == 0:
unit_scale = scale
unit_multiplier = multiplier
else:
unit_scale = 0
unit_multiplier = 1.
cls.create_base_unit(id, unit_def, kind, exponent=exponent, scale=unit_scale, multiplier=unit_multiplier)
return unit_def
@classmethod
def gen_unit_id(cls, unit):
""" Generate an SBML unit id for a unit
Args:
unit (:obj:`unit_registry.Unit`): unit
Returns:
:obj:`str`: SBML id for unit
Raises:
:obj:`ValueError`: if an SBML id cannot be generated for the unit
"""
if not isinstance(unit, unit_registry.Unit):
raise ValueError('Cannot generate SBML id for `None` unit')
id = 'unit_' + str(unit) \
.replace(' / ', '_per_') \
.replace(' * ', '_times_') \
.replace(' ** ', '_pow_')
if hasattr(libsbml, 'UNIT_KIND_' + cls.normalize_unit_kind(id[5:], to_sbml_base_units=False).upper()):
id = cls.normalize_unit_kind(id[5:], to_sbml_base_units=False)
return id
@classmethod
def create_base_unit(cls, unit_def_id, unit_def, kind, exponent=1, scale=0, multiplier=1.0):
""" Add a unit to a SBML unit definition
Each SBML unit has four attributes:
* `kind`
* `exponent`
* `scale`
* `multiplier`
Args:
unit_def_id (:obj:`str`): id of SBML unit definition
unit_def (:obj:`libsbml.UnitDefinition`): SBML unit definition
kind (:obj:`str`): unit kind
exponent (:obj:`int`, optional): exponent of the unit
scale (:obj:`int`, optional): scale of the unit
multiplier (:obj:`float`, optional): multiplier of the unit
Returns:
:obj:`libsbml.Unit`: SBML unit
"""
id = unit_def_id + '_' + kind
kind = cls.normalize_unit_kind(kind)
kind_val = getattr(libsbml, 'UNIT_KIND_' + kind.upper())
unit = cls.call_libsbml(unit_def.createUnit)
cls.call_libsbml(unit.setIdAttribute, id)
cls.call_libsbml(unit.setKind, kind_val)
cls.call_libsbml(unit.setExponent, exponent)
cls.call_libsbml(unit.setScale, scale)
cls.call_libsbml(unit.setMultiplier, multiplier)
return unit
@classmethod
def normalize_unit_kind(cls, kind, to_sbml_base_units=True):
""" Normalize unit kind for SBML
Args:
kind (:obj:`str`): unit kind
to_sbml_base_units (:obj:`bool`, optional): if :obj:`True`, map to fundamental SBML units
Returns:
:obj:`str`: normalized unit kind
"""
if kind == 'liter':
kind = 'litre'
elif kind == 'meter':
kind = 'metre'
elif kind == 'molecule':
kind = 'mole'
if to_sbml_base_units:
if kind == 'gDCW':
kind = 'gram'
return kind
@classmethod
def parse_units(cls, sbml_model):
""" Parse SBML units to Python
Args:
sbml_model (:obj:`libsbml.Model`): SBML model
Returns:
:obj:`dict`: dictionary that maps ids of unit definitions to instance of `unit_registry.Unit`
Raises:
:obj:`LibSbmlError`: if units are not set or are inconsistent
"""
units = {}
for type in ['Time', 'Substance', 'Volume']:
if not getattr(sbml_model, 'isSet' + type + 'Units')():
raise LibSbmlError('{} units must be set'.format(type))
sbml_unit_id = getattr(sbml_model, 'get' + type + 'Units')()
if sbml_unit_id == 'mole':
units[sbml_unit_id] = unit_registry.parse_expression('molecule')
else:
units[sbml_unit_id] = unit_registry.parse_expression(sbml_unit_id)
assert cls.call_libsbml(sbml_model.getExtentUnits) == cls.call_libsbml(sbml_model.getSubstanceUnits), \
LibSbmlError('Substance and extent units must be the same')
assert not cls.call_libsbml(sbml_model.isSetLengthUnits), \
LibSbmlError('Length units must be unset')
assert not cls.call_libsbml(sbml_model.isSetAreaUnits), \
LibSbmlError('Area units must be unset')
for i_unit_def in range(cls.call_libsbml(sbml_model.getNumUnitDefinitions, returns_int=True)):
sbml_unit_def = cls.call_libsbml(sbml_model.getUnitDefinition, i_unit_def)
sbml_unit_def_id = cls.call_libsbml(sbml_unit_def.getIdAttribute)
units[sbml_unit_def_id] = cls.parse_unit_id(sbml_unit_def_id)
return units
@classmethod
def parse_unit_id(cls, sbml_unit_def_id):
""" Parse a unit from an SBML unit definition.
Args:
sbml_unit_def_id (:obj:`str`): id of SBML unit definition
Returns:
:obj:`unit_registry.Unit`: units
"""
if sbml_unit_def_id.startswith('unit_'):
return unit_registry.parse_units(sbml_unit_def_id.partition('unit_')[2]
.replace('_per_', ' / ')
.replace('_times_', ' * ')
.replace('_pow_', ' ** '))
else:
if sbml_unit_def_id == 'mole':
return unit_registry.parse_units('molecule')
return unit_registry.parse_units(sbml_unit_def_id)
@classmethod
def set_unit(cls, sbml_set_unit_func, unit):
""" Set the units of an SBML object
Args:
sbml_set_unit_func (:obj:`callable`): function to set the units of an SBML object
unit (:obj:`unit_registry.Unit`): unit
"""
unit_id = cls.gen_unit_id(unit)
cls.call_libsbml(sbml_set_unit_func, unit_id)
@classmethod
def get_unit(cls, sbml_get_unit_func):
""" Get units from SBML unit definition id
Args:
sbml_get_unit_func (:obj:`callable`): function to get the units of an SBML object
Returns:
:obj:`unit_registry.Unit`: units
"""
return cls.parse_unit_id(cls.call_libsbml(sbml_get_unit_func))
@classmethod
def create_parameter(cls, sbml_model, id, value, units, name=None, constant=True):
""" Add a parameter to an SBML model.
Args:
sbml_model (:obj:`libsbml.Model`): SBML model
id (:obj:`str`): id
value (:obj:`obj`): value
units (:obj:`unit_registry.Unit`): units
name (:obj:`str`, optional): name
constant (:obj:`str`, optional): whether the parameter is a constant
Returns:
:obj:`libsbml.Parameter`: SBML parameter
"""
sbml_parameter = cls.call_libsbml(sbml_model.createParameter)
cls.call_libsbml(sbml_parameter.setIdAttribute, id)
if name is not None:
cls.call_libsbml(sbml_parameter.setName, name)
if value is not None:
cls.call_libsbml(sbml_parameter.setValue, value)
cls.set_unit(sbml_parameter.setUnits, units)
cls.call_libsbml(sbml_parameter.setConstant, constant)
return sbml_parameter
@classmethod
def parse_parameter(cls, sbml_parameter):
""" Parse a parameter from an SBML parameter.
Args:
sbml_parameter (:obj:`libsbml.Parameter`): SBML parameter
Returns:
:obj:`str`: id
:obj:`str`: name
:obj:`float`: value
:obj:`unit_registry.Unit`: units
"""
id = cls.call_libsbml(sbml_parameter.getIdAttribute)
name = cls.call_libsbml(sbml_parameter.getName)
value = cls.call_libsbml(sbml_parameter.getValue)
units = cls.get_unit(sbml_parameter.getUnits)
return (id, name, value, units)
@classmethod
def set_math(cls, set_math_func, expression, units_transform=None):
""" Set the math of an SBML object
Args:
set_math_func (:obj:`callable`): function to set the math of an SBML object
expression (:obj:`obj_tables.math.expression.Expression`): expression
"""
str_formula = expression._parsed_expression.get_str(cls._obj_tables_token_to_str, with_units=True, number_units=' dimensionless')
if units_transform:
str_formula = units_transform.format(str_formula)
sbml_formula = cls.call_libsbml(libsbml.parseL3Formula, str_formula)
cls.call_libsbml(set_math_func, sbml_formula)
@staticmethod
def _obj_tables_token_to_str(token):
""" Get a string representation of a token that represents an instance of :obj:`Model`.
Args:
token (:obj:`obj_tables.math.expression.ObjTablesToken`): token that represents an instance of :obj:`Model`
Returns:
:obj:`str`: string representation of a token that represents an instance of :obj:`Model`.
"""
if isinstance(token.model, wc_lang.core.Compartment):
return '__mass__' + token.model.gen_sbml_id()
elif isinstance(token.model, (wc_lang.core.Observable, wc_lang.core.Function)):
return '__param__' + token.model.gen_sbml_id()
else:
return token.model.gen_sbml_id()
@classmethod
def get_math(cls, get_math_func, Expression, model_objs, units_transform=None):
""" Get the math of an SBML object
Args:
get_math_func (:obj:`callable`): function to get the math of an SBML object
Expression (:obj:`type`): type of expression
model_objs (:obj:`dict`, optional): dictionary that maps classes of model objects to dictonaries
that map ids of model objects to model objects
Returns:
:obj:`obj_tables.math.expression.Expression`: expression
"""
sbml_formula = cls.call_libsbml(get_math_func)
str_formula = cls.call_libsbml(libsbml.formulaToL3String, sbml_formula)
if units_transform:
str_formula = units_transform(str_formula)
str_formula = str_formula \
.replace('__mass__Compartment__', 'Compartment__') \
.replace('__param__Observable__', 'Observable__') \
.replace('__param__Function__', 'Function__') \
.replace(' dimensionless', '')
str_formula = str_formula \
.replace('__RB__', '[') \
.replace('__LB__', ']') \
.replace('__DS__', '-')
str_formula = re.sub(r'[A-Za-z0-9]+__', '', str_formula)
expression, error = Expression.deserialize(str_formula, model_objs)
assert error is None, str(error)
return expression
@classmethod
def set_annotations(cls, model_obj, nested_attr_paths, sbml_obj):
""" Export annotations from a model object to an SBML object
Args:
model_obj (:obj:`obj_tables.Model`): model object
nested_attr_paths (:obj:`dict`): dictionary that maps names of attributes to paths to the attributes
sbml_obj (:obj:`libsbml.SBase`): SBML object
"""
cls.call_libsbml(sbml_obj.setAnnotation,
'<annotation><wcLang:annotation>'
+ cls.gen_annotations(model_obj, nested_attr_paths, sbml_obj)
+ '</wcLang:annotation></annotation>')
@classmethod
def gen_annotations(cls, model_obj, nested_attr_paths, sbml_obj):
""" Export annotations from a model object to an SBML object
Args:
model_obj (:obj:`obj_tables.Model`): model object
nested_attr_paths (:obj:`dict`): dictionary that maps names of attributes to paths to the attributes
sbml_obj (:obj:`libsbml.SBase`): SBML object
Returns:
:obj:`str`: string representation of the XML annotations of the model
"""
key_vals = []
for nested_attr_name, nested_attr_path in nested_attr_paths.items():
attr = model_obj.get_nested_attr(nested_attr_path)
val = model_obj.get_nested_attr_val(nested_attr_path)
serialized_val = attr.serialize(val)
if serialized_val is None:
serialized_val = ''
key_vals.append(('<wcLang:property>'
'<wcLang:key>{}</wcLang:key>'
'<wcLang:value>{}</wcLang:value>'
'</wcLang:property>').format(
nested_attr_name, serialized_val))
return ''.join(key_vals)
@classmethod
def get_annotations(cls, model_obj, nested_attr_paths, sbml_obj, model_objs=None):
""" Import annotations from a model object to an SBML object
Args:
model_obj (:obj:`obj_tables.Model`): model object
nested_attr_paths (:obj:`dict`): dictionary that maps names of attributes to paths to the attributes
sbml_obj (:obj:`libsbml.SBase`): SBML object
model_objs (:obj:`dict`, optional): dictionary that maps classes of model objects to dictonaries
that map ids of model objects to model objects
"""
for nested_attr_path in nested_attr_paths.values():
attr = model_obj.get_nested_attr(nested_attr_path)
val = attr.get_none_value()
model_obj.set_nested_attr_val(nested_attr_path, val)
for nested_attr_name, val in cls.parse_annotations(sbml_obj).items():
nested_attr_path = nested_attr_paths.get(nested_attr_name, None)
if nested_attr_path:
attr = model_obj.get_nested_attr(nested_attr_path)
if isinstance(attr, obj_tables.RelatedAttribute):
val, error = attr.deserialize(val, model_objs)
else:
val, error = attr.deserialize(val)
assert error is None, 'Error parsing {}.{} from {}: {}'.format(
model_obj.__class__.__name__, attr_name, str(val), str(error))
model_obj.set_nested_attr_val(nested_attr_path, val)
@classmethod
def parse_annotations(cls, sbml_obj):
""" Import annotations from a model object to an SBML object
Args:
sbml_obj (:obj:`libsbml.SBase`): SBML object
Returns:
:obj:`dict`: dictionary of names and values of annotated attributes
"""
sbml_annots = cls.call_libsbml(sbml_obj.getAnnotation)
key_vals = {}
for i_annot in range(cls.call_libsbml(sbml_annots.getNumChildren, returns_int=True)):
sbml_annot = cls.call_libsbml(sbml_annots.getChild, i_annot)
if cls.call_libsbml(sbml_annot.getName) == 'annotation' and cls.call_libsbml(sbml_annot.getURI) == cls.XML_NAMESPACE:
for i_child in range(cls.call_libsbml(sbml_annot.getNumChildren, returns_int=True)):
key_val = cls.call_libsbml(sbml_annot.getChild, i_child)
for i_g_child in range(cls.call_libsbml(key_val.getNumChildren, returns_int=True)):
g_child = cls.call_libsbml(key_val.getChild, i_g_child)
if cls.call_libsbml(g_child.getName) == 'key':
attr_name = cls.call_libsbml(g_child.toXMLString)[12:-13]
elif cls.call_libsbml(g_child.getName) == 'value':
val = cls.call_libsbml(g_child.toXMLString)[14:-15]
key_vals[attr_name] = val
return key_vals
@staticmethod
def gen_nested_attr_paths(dotted_attr_paths):
""" Generate structure representations of a list of nested attribute paths
Args:
dotted_attr_paths (:obj:`list` of :obj:`str`): List of paths to nested attributes. Each path is a
dot-separated string of the series of nested attributes.
Returns:
:obj:`dict`: dictionary that ma
"""
nested_attr_paths = {}
for dotted_attr_path in dotted_attr_paths:
nested_attr_paths[dotted_attr_path] = [(attr_name, ) for attr_name in dotted_attr_path.split('.')]
return nested_attr_paths
@classmethod
def gen_authors_annotation(cls, model):
""" Generate an annotation of the authors of a model
Args:
model (:obj:`wc_lang.core.Model`): model
Returns:
:obj:`str`: string representation of the XML annotation of the authors of a model
"""
authors_sbml = ''
for au in model.authors:
authors_sbml += ('<wcLang:author>'
'<wcLang:id>{}</wcLang:id>'
'<wcLang:name>{}</wcLang:name>'
'<wcLang:lastName>{}</wcLang:lastName>'
'<wcLang:firstName>{}</wcLang:firstName>'
'<wcLang:middleName>{}</wcLang:middleName>'
'<wcLang:title>{}</wcLang:title>'
'<wcLang:organization>{}</wcLang:organization>'
'<wcLang:email>{}</wcLang:email>'
'<wcLang:website>{}</wcLang:website>'
'<wcLang:address>{}</wcLang:address>'
'<wcLang:identifiers>{}</wcLang:identifiers>'
'<wcLang:comments>{}</wcLang:comments>'
'</wcLang:author>').format(
au.id, au.name, au.last_name, au.first_name, au.middle_name,
au.title, au.organization, au.email, au.website, au.address,
wc_lang.core.Author.Meta.attributes['identifiers'].serialize(au.identifiers),
au.comments)
return '<wcLang:authors>{}</wcLang:authors>'.format(authors_sbml)
@classmethod
def get_authors_annotation(cls, model, sbml_model, model_objs):
""" Import the SBML annotation of the authors of a model to a model
Args:
model (:obj:`wc_lang.core.Model`): model
sbml_model (:obj:`libsbml.Model`): SBML model
model_objs (:obj:`dict`, optional): dictionary that maps classes of model objects to dictonaries
that map ids of model objects to model objects
"""
sbml_annots = cls.call_libsbml(sbml_model.getAnnotation)
for i_annot in range(cls.call_libsbml(sbml_annots.getNumChildren, returns_int=True)):
sbml_annot = cls.call_libsbml(sbml_annots.getChild, i_annot)
if cls.call_libsbml(sbml_annot.getName) == 'annotation' and cls.call_libsbml(sbml_annot.getURI) == cls.XML_NAMESPACE:
for i_annot_prop in range(cls.call_libsbml(sbml_annot.getNumChildren, returns_int=True)):
sbml_authors = cls.call_libsbml(sbml_annot.getChild, i_annot_prop)
if cls.call_libsbml(sbml_authors.getName) == 'authors':
for i_author in range(cls.call_libsbml(sbml_authors.getNumChildren, returns_int=True)):
sbml_au = cls.call_libsbml(sbml_authors.getChild, i_author)
au = model.authors.create()
for i_prop in range(cls.call_libsbml(sbml_au.getNumChildren, returns_int=True)):
sbml_prop = cls.call_libsbml(sbml_au.getChild, i_prop)
key = cls.call_libsbml(sbml_prop.getName)
val = cls.call_libsbml(sbml_prop.toXMLString)
val = val.partition('>')[2]
val = val[0:val.rfind('<')]
if key == 'id':
au.id = val
elif key == 'name':
au.name = val
elif key == 'lastName':
au.last_name = val
elif key == 'firstName':
au.first_name = val
elif key == 'middleName':
au.middle_name = val
elif key == 'title':
au.title = val
elif key == 'organization':
au.organization = val
elif key == 'email':
au.email = val
elif key == 'website':
au.website = val
elif key == 'address':
au.address = val
elif key == 'identifiers':
attr = wc_lang.core.Author.Meta.attributes['identifiers']
au.identifiers, error = attr.deserialize(val, model_objs)
assert error is None, str(error)
elif key == 'comments':
au.comments = val
@classmethod
def set_commments(cls, model_obj, sbml_obj):
""" Export comments from a model object to an SBML object
Args:
model_obj (:obj:`obj_tables.Model`): model object
sbml_obj (:obj:`libsbml.SBase`): SBML object
"""
if model_obj.comments:
cls.call_libsbml(sbml_obj.setNotes, cls.str_to_xml_node(model_obj.comments))
@classmethod
def get_commments(cls, model_obj, sbml_obj):
""" Import comments from an SBML object to a model object
Args:
model_obj (:obj:`obj_tables.Model`): model object
sbml_obj (:obj:`libsbml.SBase`): SBML object
"""
if cls.call_libsbml(sbml_obj.isSetNotes):
sbml_notes = cls.call_libsbml(sbml_obj.getNotes)
model_obj.comments = cls.str_from_xml_node(cls.call_libsbml(sbml_notes.getChild, 0))
else:
model_obj.comments = ''
@classmethod
def call_libsbml(cls, method, *args, returns_int=False, debug=False):
""" Call a libSBML method and handle any errors.
Unfortunately, libSBML methods that do not return data usually report errors via return codes,
instead of exceptions, and the generic return codes contain virtually no information.
This function wraps these methods and raises useful exceptions when errors occur.
Set `returns_int` `True` to avoid raising false exceptions or warnings from methods that return
integer values.
Args:
method (:obj:`type`, :obj:`types.FunctionType`, or :obj:`types.MethodType`): `libsbml` method to execute
args (:obj:`list`): a `list` of arguments to the `libsbml` method
returns_int (:obj:`bool`, optional): whether the method returns an integer; if `returns_int`
is `True`, then an exception will not be raised if the method call returns an integer
debug (:obj:`bool`, optional): whether to print debug output
Returns:
:obj:`obj` or `int`: if the call does not return an error, return the `libsbml`
method's return value, either an object that has been created or retrieved, or an integer
value, or the `libsbml` success return code, :obj:`libsbml.LIBSBML_OPERATION_SUCCESS`
Raises:
:obj:`LibSbmlError`: if the `libsbml` call raises an exception, or returns None, or
returns a known integer error code != :obj:`libsbml.LIBSBML_OPERATION_SUCCESS`
"""
new_args = []
for arg in args:
new_args.append(arg)
if new_args:
new_args_str = ', '.join([str(a) for a in new_args])
call_str = "method: {}; args: {}".format(method, new_args_str)
else:
call_str = "method: {}".format(method)
if debug:
print('libSBML call:', call_str)
try:
rc = method(*tuple(new_args))
except BaseException as error:
raise LibSbmlError("Error '{}' in libSBML method call '{}'.".format(error, call_str))
if rc == None:
raise LibSbmlError("libSBML returned None when executing '{}'.".format(call_str))
elif type(rc) is int:
# if `method` returns an int value, do not interpret rc as an error code
if returns_int:
if debug:
print('libSBML returns an int:', rc)
return rc
if rc == libsbml.LIBSBML_OPERATION_SUCCESS:
if debug:
print('libSBML returns: LIBSBML_OPERATION_SUCCESS')
return rc
else:
error_code = libsbml.OperationReturnValue_toString(rc)
if error_code is None:
if debug:
print("libSBML returns:", rc)
warnings.warn("call_libsbml: unknown error code {} returned by '{}'."
"\nPerhaps an integer value is being returned; if so, to avoid this warning "
"pass 'returns_int=True' to call_libsbml().".format(error_code, call_str),
wc_lang.core.WcLangWarning)
return rc
else:
raise LibSbmlError("LibSBML returned error code '{}' when executing '{}'."
"\nWARNING: if this libSBML call returns an int value, then this error may be "
"incorrect; to avoid this error pass 'returns_int=True' to call_libsbml().".format(
error_code, call_str))
else:
# return data provided by libSBML method
if debug:
print('libSBML returns:', rc)
return rc
@classmethod
def str_to_xml_node(cls, str):
""" Convert a Python string to an XML string that can be stored as a Note in an SBML document.
Args:
str (:obj:`str`): a string
Returns:
:obj:`libsbml.XMLNode`: an XML string that can be stored as a `Note` in an SBML document
"""
return cls.call_libsbml(libsbml.XMLNode.convertStringToXMLNode,
'<p xmlns="http://www.w3.org/1999/xhtml">{}</p>'.format(str))
@classmethod
def str_from_xml_node(cls, xml_node):
""" Convert an XML string (e.g., from a Note in an SBML document) to a Python string.
Args:
xml_node (:obj:`libsbml.XMLNode`): an XML string that can be stored as a `Note` in an SBML document
Returns:
:obj:`str`: a string
"""
prefix = '<p xmlns="http://www.w3.org/1999/xhtml">'
suffix = '</p>'
text = cls.call_libsbml(xml_node.toXMLString)
text = text[len(prefix):-len(suffix)]
text = text.replace('\n ', '\n').strip()
return text
@classmethod
def raise_if_error(cls, sbml_doc, message):
""" Raise an error, if an SBML object has errors
Args:
sbml_doc (:obj:`libsbml.SBMLDocument`): SBML document
message (:obj:`str`): summary of error
Raises:
:obj:`LibSbmlError`: if the SBML object has errors
"""
errors, warns, log_errors, log_warns = cls.get_errors_warnings(sbml_doc)
if warns or log_warns:
warnings.warn('{}:{}{}'.format(message,
sbml_doc.__class__.__name__,
''.join(warns),
''.join(log_warns)),
wc_lang.core.WcLangWarning)
if errors or log_errors:
raise LibSbmlError('{}:{}{}'.format(message,
sbml_doc.__class__.__name__,
''.join(errors),
''.join(log_errors)))
@classmethod
def get_errors_warnings(cls, sbml_doc):
""" Get libSBML errors and warnings
Args:
sbml_doc (:obj:`libsbml.SBMLDocument`): SBML document
Returns:
:obj:`list` of :obj:`str`: error messages
:obj:`list` of :obj:`str`: warning messages
:obj:`list` of :obj:`str`: log error messages
:obj:`list` of :obj:`str`: log warning messages
"""
errors = []
warns = []
n_errors = cls.call_libsbml(sbml_doc.getNumErrors, returns_int=True)
for i_error in range(n_errors):
error = cls.call_libsbml(sbml_doc.getError, i_error)
msg = '\n {}: {}: {}'.format(cls.call_libsbml(error.getSeverityAsString),
cls.call_libsbml(error.getShortMessage),
cls.call_libsbml(error.getMessage))
if cls.call_libsbml(error.getSeverity, returns_int=True) in [libsbml.LIBSBML_SEV_INFO, libsbml.LIBSBML_SEV_WARNING]:
warns.append(msg)
else:
errors.append(msg)
error_log = cls.call_libsbml(sbml_doc.getErrorLog)
n_log_errors = cls.call_libsbml(error_log.getNumErrors, returns_int=True)
log_errors = []
log_warns = []
for i_log_error in range(n_log_errors):
log_error = cls.call_libsbml(error_log.getError, i_log_error)
msg = '\n {}: {}: {}'.format(cls.call_libsbml(log_error.getSeverityAsString),
cls.call_libsbml(log_error.getShortMessage),
cls.call_libsbml(log_error.getMessage))
if cls.call_libsbml(log_error.getSeverity, returns_int=True) in [libsbml.LIBSBML_SEV_INFO, libsbml.LIBSBML_SEV_WARNING]:
log_warns.append(msg)
else:
log_errors.append(msg)
return (errors, warns, log_errors, log_warns)