diff --git a/cobra/core/__init__.py b/cobra/core/__init__.py index 25d679371..affa4369f 100644 --- a/cobra/core/__init__.py +++ b/cobra/core/__init__.py @@ -9,5 +9,6 @@ from cobra.core.model import Model from cobra.core.object import Object from cobra.core.reaction import Reaction -from cobra.core.solution import LegacySolution, Solution, get_solution +from cobra.core.group import Group +from cobra.core.solution import Solution, LegacySolution, get_solution from cobra.core.species import Species diff --git a/cobra/core/gene.py b/cobra/core/gene.py index c5d0f9a7c..938780d67 100644 --- a/cobra/core/gene.py +++ b/cobra/core/gene.py @@ -249,6 +249,11 @@ def remove_from_model(self, model=None, the_gene_re = re.compile('(^|(?<=( |\()))%s(?=( |\)|$))' % re.escape(self.id)) + # remove reference to the gene in all groups + associated_groups = self._model.get_associated_groups(self) + for group in associated_groups: + group.remove_members(self) + self._model.genes.remove(self) self._model = None diff --git a/cobra/core/group.py b/cobra/core/group.py new file mode 100644 index 000000000..914e151b8 --- /dev/null +++ b/cobra/core/group.py @@ -0,0 +1,109 @@ +# -*- coding: utf-8 -*- + +"""Define the group class.""" + +from __future__ import absolute_import + +from warnings import warn + +from six import string_types + +from cobra.core.object import Object + + +class Group(Object): + """ + Manage groups via this implementation of the SBML group specification. + + `Group` is a class for holding information regarding a pathways, + subsystems, or other custom groupings of objects within a cobra.Model + object. + + Parameters + ---------- + id : str + The identifier to associate with this group + name : str, optional + A human readable name for the group + members : iterable, optional + A list object containing references to cobra.Model-associated objects + that belong to the group. + kind : {"collection", "classification", "partonomy"}, optional + The kind of group, as specified for the Groups feature in the SBML + level 3 package specification. Can be any of "classification", + "partonomy", or "collection". The default is "collection". + Please consult the SBML level 3 package specification to ensure you + are using the proper value for kind. In short, members of a + "classification" group should have an "is-a" relationship to the group + (e.g. member is-a polar compound, or member is-a transporter). + Members of a "partonomy" group should have a "part-of" relationship + (e.g. member is part-of glycolysis). Members of a "collection" group + do not have an implied relationship between the members, so use this + value for kind when in doubt (e.g. member is a gap-filled reaction, + or member is involved in a disease phenotype). + """ + KIND_TYPES = ("collection", "classification", "partonomy") + + def __init__(self, id, name='', members=None, kind=None): + Object.__init__(self, id, name) + + self._members = set() if members is None else set(members) + self._kind = None + self.kind = "collection" if kind is None else kind + # self.model is None or refers to the cobra.Model that + # contains self + self._model = None + + # read-only + @property + def members(self): + return self._members + + @property + def kind(self): + return self._kind + + @kind.setter + def kind(self, kind): + kind = kind.lower() + if kind in self.KIND_TYPES: + self._kind = kind + else: + raise ValueError( + "Kind can only by one of: {}.".format(", ".join( + self.KIND_TYPES))) + + def add_members(self, new_members): + """ + Add objects to the group. + + Parameters + ---------- + new_members : list + A list of cobrapy objects to add to the group. + + """ + + if isinstance(new_members, string_types) or \ + hasattr(new_members, "id"): + warn("need to pass in a list") + new_members = [new_members] + + self._members.update(new_members) + + def remove_members(self, to_remove): + """ + Remove objects from the group. + + Parameters + ---------- + to_remove : list + A list of cobra objects to remove from the group + """ + + if isinstance(to_remove, string_types) or \ + hasattr(to_remove, "id"): + warn("need to pass in a list") + to_remove = [to_remove] + + self._members.difference_update(to_remove) diff --git a/cobra/core/metabolite.py b/cobra/core/metabolite.py index ae6753db3..717f2d7f4 100644 --- a/cobra/core/metabolite.py +++ b/cobra/core/metabolite.py @@ -79,7 +79,7 @@ def elements(self): # necessary for some old pickles which use the deprecated # Formula class tmp_formula = str(self.formula) - # commonly occuring characters in incorrectly constructed formulas + # commonly occurring characters in incorrectly constructed formulas if "*" in tmp_formula: warn("invalid character '*' found in formula '%s'" % self.formula) tmp_formula = tmp_formula.replace("*", "") diff --git a/cobra/core/model.py b/cobra/core/model.py index 4e6ea1a40..0ffb5918f 100644 --- a/cobra/core/model.py +++ b/cobra/core/model.py @@ -15,6 +15,9 @@ from cobra.core.configuration import Configuration from cobra.core.dictlist import DictList +from cobra.core.gene import Gene +from cobra.core.group import Group +from cobra.core.metabolite import Metabolite from cobra.core.object import Object from cobra.core.reaction import Reaction from cobra.core.solution import get_solution @@ -55,12 +58,16 @@ class Model(Object): genes : DictList A DictList where the key is the gene identifier and the value a Gene + groups : DictList + A DictList where the key is the group identifier and the value a + Group solution : Solution The last obtained solution from optimizing the model. """ def __setstate__(self, state): - """Make sure all cobra.Objects in the model point to the model.""" + """Make sure all cobra.Objects in the model point to the model. + """ self.__dict__.update(state) for y in ['reactions', 'genes', 'metabolites']: for x in getattr(self, y): @@ -93,6 +100,7 @@ def __init__(self, id_or_model=None, name=None): self.genes = DictList() self.reactions = DictList() # A list of cobra.Reactions self.metabolites = DictList() # A list of cobra.Metabolites + self.groups = DictList() # A list of cobra.Groups # genes based on their ids {Gene.id: Gene} self._compartments = {} self._contexts = [] @@ -281,7 +289,7 @@ def copy(self): """ new = self.__class__() do_not_copy_by_ref = {"metabolites", "reactions", "genes", "notes", - "annotation"} + "annotation", "groups"} for attr in self.__dict__: if attr not in do_not_copy_by_ref: new.__dict__[attr] = self.__dict__[attr] @@ -327,6 +335,39 @@ def copy(self): new_gene = new.genes.get_by_id(gene.id) new_reaction._genes.add(new_gene) new_gene._reaction.add(new_reaction) + + new.groups = DictList() + do_not_copy_by_ref = {"_model", "_members"} + # Groups can be members of other groups. We initialize them first and + # then update their members. + for group in self.groups: + new_group = group.__class__() + for attr, value in iteritems(group.__dict__): + if attr not in do_not_copy_by_ref: + new_group.__dict__[attr] = copy(value) + new_group._model = new + new.groups.append(new_group) + for group in self.groups: + new_group = new.groups[group.id] + # update awareness, as in the reaction copies + new_objects = [] + for member in group.members: + if isinstance(member, Metabolite): + new_object = new.metabolites.get_by_id(member.id) + elif isinstance(member, Reaction): + new_object = new.reactions.get_by_id(member.id) + elif isinstance(member, Gene): + new_object = new.genes.get_by_id(member.id) + elif isinstance(member, Group): + new_object = new.genes.get_by_id(member.id) + else: + raise TypeError( + "The group member {!r} is unexpectedly not a " + "metabolite, reaction, gene, nor another " + "group.".format(member)) + new_objects.append(new_object) + new_group.add_members(new_objects) + try: new._solver = deepcopy(self.solver) # Cplex has an issue with deep copies @@ -409,6 +450,11 @@ def remove_metabolites(self, metabolite_list, destructive=False): for x in metabolite_list: x._model = None + # remove reference to the metabolite in all groups + associated_groups = self.get_associated_groups(x) + for group in associated_groups: + group.remove_members(x) + if not destructive: for the_reaction in list(x._reaction): the_coefficient = the_reaction._metabolites[x] @@ -505,6 +551,7 @@ def add_boundary(self, metabolite, type="exchange", reaction_id=None, (0, 1000.0) >>> demand.build_reaction_string() 'atp_c --> ' + """ ub = CONFIGURATION.upper_bound if ub is None else ub lb = CONFIGURATION.lower_bound if lb is None else lb @@ -684,6 +731,100 @@ def remove_reactions(self, reactions, remove_orphans=False): if context: context(partial(self.genes.add, gene)) + # remove reference to the reaction in all groups + associated_groups = self.get_associated_groups(reaction) + for group in associated_groups: + group.remove_members(reaction) + + def add_groups(self, group_list): + """Add groups to the model. + + Groups with identifiers identical to a group already in the model are + ignored. + + If any group contains members that are not in the model, these members + are added to the model as well. Only metabolites, reactions, and genes + can have groups. + + Parameters + ---------- + group_list : list + A list of `cobra.Group` objects to add to the model. + """ + + def existing_filter(group): + if group.id in self.groups: + LOGGER.warning( + "Ignoring group '%s' since it already exists.", group.id) + return False + return True + + if isinstance(group_list, string_types) or \ + hasattr(group_list, "id"): + warn("need to pass in a list") + group_list = [group_list] + + pruned = DictList(filter(existing_filter, group_list)) + + for group in pruned: + group._model = self + for member in group.members: + # If the member is not associated with the model, add it + if isinstance(member, Metabolite): + if member not in self.metabolites: + self.add_metabolites([member]) + if isinstance(member, Reaction): + if member not in self.reactions: + self.add_reactions([member]) + # TODO(midnighter): `add_genes` method does not exist. + # if isinstance(member, Gene): + # if member not in self.genes: + # self.add_genes([member]) + + self.groups += [group] + + def remove_groups(self, group_list): + """Remove groups from the model. + + Members of each group are not removed + from the model (i.e. metabolites, reactions, and genes in the group + stay in the model after any groups containing them are removed). + + Parameters + ---------- + group_list : list + A list of `cobra.Group` objects to remove from the model. + """ + + if isinstance(group_list, string_types) or \ + hasattr(group_list, "id"): + warn("need to pass in a list") + group_list = [group_list] + + for group in group_list: + # make sure the group is in the model + if group.id not in self.groups: + LOGGER.warning("%r not in %r. Ignored.", group, self) + else: + self.groups.remove(group) + group._model = None + + def get_associated_groups(self, element): + """Returns a list of groups that an element (reaction, metabolite, gene) + is associated with. + + Parameters + ---------- + element: `cobra.Reaction`, `cobra.Metabolite`, or `cobra.Gene` + + Returns + ------- + list of `cobra.Group` + All groups that the provided object is a member of + """ + # check whether the element is associated with the model + return [g for g in self.groups if element in g.members] + def add_cons_vars(self, what, **kwargs): """Add constraints and variables to the model's mathematical problem. @@ -922,6 +1063,7 @@ def repair(self, rebuild_index=True, rebuild_relationships=True): self.reactions._generate_index() self.metabolites._generate_index() self.genes._generate_index() + self.groups._generate_index() if rebuild_relationships: for met in self.metabolites: met._reaction.clear() @@ -932,8 +1074,9 @@ def repair(self, rebuild_index=True, rebuild_relationships=True): met._reaction.add(rxn) for gene in rxn._genes: gene._reaction.add(rxn) + # point _model to self - for l in (self.reactions, self.genes, self.metabolites): + for l in (self.reactions, self.genes, self.metabolites, self.groups): for e in l: e._model = self @@ -1108,6 +1251,9 @@ def _repr_html_(self): Number of reactions {num_reactions} + + Number of groups + {num_groups} Objective expression {objective} @@ -1120,6 +1266,7 @@ def _repr_html_(self): address='0x0%x' % id(self), num_metabolites=len(self.metabolites), num_reactions=len(self.reactions), + num_groups=len(self.groups), objective=format_long_string(str(self.objective.expression), 100), compartments=", ".join( v if v else k for k, v in iteritems(self.compartments) diff --git a/cobra/io/__init__.py b/cobra/io/__init__.py index 46d88a4b3..ca889ceda 100644 --- a/cobra/io/__init__.py +++ b/cobra/io/__init__.py @@ -5,7 +5,6 @@ from cobra.io.dict import model_from_dict, model_to_dict from cobra.io.json import from_json, load_json_model, save_json_model, to_json from cobra.io.mat import load_matlab_model, save_matlab_model -from cobra.io.sbml import read_legacy_sbml -from cobra.io.sbml import write_cobra_model_to_sbml_file as write_legacy_sbml -from cobra.io.sbml3 import read_sbml_model, write_sbml_model +from cobra.io.sbml import read_sbml_model, write_sbml_model, \ + validate_sbml_model from cobra.io.yaml import from_yaml, load_yaml_model, save_yaml_model, to_yaml diff --git a/cobra/io/sbml.py b/cobra/io/sbml.py index 9345904b0..885289b0d 100644 --- a/cobra/io/sbml.py +++ b/cobra/io/sbml.py @@ -1,745 +1,1516 @@ -# -*- coding: utf-8 -*- +""" +SBML import and export using python-libsbml(-experimental). + +- The SBML importer supports all versions of SBML and the fbc package. +- The SBML exporter writes SBML L3 models. +- Annotation information is stored on the cobrapy objects +- Information from the group package is read + +Parsing of fbc models was implemented as efficient as possible, whereas +(discouraged) fallback solutions are not optimized for efficiency. + +Notes are only supported in a minimal way relevant for constraint-based +models. I.e., structured information from notes in the form +

key: value

+is read into the Object.notes dictionary when reading SBML files. +On writing the Object.notes dictionary is serialized to the SBML +notes information. + +Annotations are read in the Object.annotation fields. + +Some SBML related issues are still open, please refer to the respective issue: +- update annotation format and support qualifiers (depends on decision + for new annotation format; https://github.com/opencobra/cobrapy/issues/684) +- write compartment annotations and notes (depends on updated first-class + compartments; see https://github.com/opencobra/cobrapy/issues/760) +- support compression on file handles (depends on solution for + https://github.com/opencobra/cobrapy/issues/812) +""" from __future__ import absolute_import +import datetime +import logging +import os import re -from math import isinf, isnan -from os.path import isfile -from warnings import warn +import traceback +from collections import defaultdict, namedtuple +from copy import deepcopy +from sys import platform +from warnings import catch_warnings, simplefilter -from six import iteritems +from six import iteritems, string_types -from cobra.core import Configuration, Metabolite, Model, Reaction -from cobra.util.solver import set_objective +import cobra +import libsbml +from cobra.core import Gene, Group, Metabolite, Model, Reaction +from cobra.manipulation.validate import check_metabolite_compartment_formula +from cobra.util.solver import linear_reaction_coefficients, set_objective -try: - import libsbml -except ImportError: - libsbml = None +class CobraSBMLError(Exception): + """ SBML error class. """ + pass -CONFIGURATION = Configuration() +LOGGER = logging.getLogger(__name__) +# ----------------------------------------------------------------------------- +# Defaults and constants for writing SBML +# ----------------------------------------------------------------------------- +config = cobra.Configuration() # for default bounds +LOWER_BOUND_ID = "cobra_default_lb" +UPPER_BOUND_ID = "cobra_default_ub" +ZERO_BOUND_ID = "cobra_0_bound" -def parse_legacy_id(the_id, the_compartment=None, the_type='metabolite', - use_hyphens=False): - """Deals with a bunch of problems due to bigg.ucsd.edu not following SBML - standards +BOUND_MINUS_INF = "minus_inf" +BOUND_PLUS_INF = "plus_inf" + + +SBO_FBA_FRAMEWORK = "SBO:0000624" +SBO_DEFAULT_FLUX_BOUND = "SBO:0000626" +SBO_FLUX_BOUND = "SBO:0000625" +SBO_EXCHANGE_REACTION = "SBO:0000627" + +LONG_SHORT_DIRECTION = {'maximize': 'max', 'minimize': 'min'} +SHORT_LONG_DIRECTION = {'min': 'minimize', 'max': 'maximize'} + +Unit = namedtuple('Unit', ['kind', 'scale', 'multiplier', 'exponent']) +UNITS_FLUX = ("mmol_per_gDW_per_hr", + [ + Unit(kind=libsbml.UNIT_KIND_MOLE, scale=-3, multiplier=1, + exponent=1), + Unit(kind=libsbml.UNIT_KIND_GRAM, scale=0, multiplier=1, + exponent=-1), + Unit(kind=libsbml.UNIT_KIND_SECOND, scale=0, multiplier=3600, + exponent=-1) + ]) + +# ----------------------------------------------------------------------------- +# Functions for id replacements (import/export) +# ----------------------------------------------------------------------------- +SBML_DOT = "__SBML_DOT__" + + +def _clip(sid, prefix): + """Clips a prefix from the beginning of a string if it exists.""" + return sid[len(prefix):] if sid.startswith(prefix) else sid + + +def _f_gene(sid, prefix="G_"): + """Clips gene prefix from id.""" + sid = sid.replace(SBML_DOT, ".") + return _clip(sid, prefix) + + +def _f_gene_rev(sid, prefix="G_"): + """Adds gene prefix to id.""" + return prefix + sid.replace(".", SBML_DOT) + + +def _f_specie(sid, prefix="M_"): + """Clips specie/metabolite prefix from id.""" + return _clip(sid, prefix) + + +def _f_specie_rev(sid, prefix="M_"): + """Adds specie/metabolite prefix to id.""" + return prefix + sid + + +def _f_reaction(sid, prefix="R_"): + """Clips reaction prefix from id.""" + return _clip(sid, prefix) + + +def _f_reaction_rev(sid, prefix="R_"): + """Adds reaction prefix to id.""" + return prefix + sid + + +F_GENE = "F_GENE" +F_GENE_REV = "F_GENE_REV" +F_SPECIE = "F_SPECIE" +F_SPECIE_REV = "F_SPECIE_REV" +F_REACTION = "F_REACTION" +F_REACTION_REV = "F_REACTION_REV" + +F_REPLACE = { + F_GENE: _f_gene, + F_GENE_REV: _f_gene_rev, + F_SPECIE: _f_specie, + F_SPECIE_REV: _f_specie_rev, + F_REACTION: _f_reaction, + F_REACTION_REV: _f_reaction_rev, +} + + +# ----------------------------------------------------------------------------- +# Read SBML +# ----------------------------------------------------------------------------- +def read_sbml_model(filename, number=float, f_replace=F_REPLACE, **kwargs): + """Reads SBML model from given filename. + + If the given filename ends with the suffix ''.gz'' (for example, + ''myfile.xml.gz'),' the file is assumed to be compressed in gzip + format and will be automatically decompressed upon reading. Similarly, + if the given filename ends with ''.zip'' or ''.bz2',' the file is + assumed to be compressed in zip or bzip2 format (respectively). Files + whose names lack these suffixes will be read uncompressed. Note that + if the file is in zip format but the archive contains more than one + file, only the first file in the archive will be read and the rest + ignored. + + To read a gzip/zip file, libSBML needs to be configured and linked + with the zlib library at compile time. It also needs to be linked + with the bzip2 library to read files in bzip2 format. (Both of these + are the default configurations for libSBML.) + + This function supports SBML with FBC-v1 and FBC-v2. FBC-v1 models + are converted to FBC-v2 models before reading. + + The parser tries to fall back to information in notes dictionaries + if information is not available in the FBC packages, e.g., + CHARGE, FORMULA on species, or GENE_ASSOCIATION, SUBSYSTEM on reactions. + + Parameters + ---------- + filename : path to SBML file, or SBML string, or SBML file handle + SBML which is read into cobra model + number: data type of stoichiometry: {float, int} + In which data type should the stoichiometry be parsed. + f_replace : dict of replacement functions for id replacement + Dictionary of replacement functions for gene, specie, and reaction. + By default the following id changes are performed on import: + clip G_ from genes, clip M_ from species, clip R_ from reactions + If no replacements should be performed, set f_replace={}, None + + Returns + ------- + cobra.core.Model + + Notes + ----- + Provided file handles cannot be opened in binary mode, i.e., use + with open(path, "r" as f): + read_sbml_model(f) + File handles to compressed files are not supported yet. + """ + try: + doc = _get_doc_from_filename(filename) + return _sbml_to_model(doc, number=number, + f_replace=f_replace, **kwargs) + except IOError as e: + raise e + + except Exception: + LOGGER.error(traceback.print_exc()) + raise CobraSBMLError( + "Something went wrong reading the SBML model. Most likely the SBML" + " model is not valid. Please check that your model is valid using " + "the `cobra.io.sbml.validate_sbml_model` function or via the " + "online validator at http://sbml.org/validator .\n" + "\t`(model, errors) = validate_sbml_model(filename)`" + "\nIf the model is valid and cannot be read please open an issue " + "at https://github.com/opencobra/cobrapy/issues .") + + +def _get_doc_from_filename(filename): + """Get SBMLDocument from given filename. Parameters ---------- - the_id: String. - the_compartment: String - the_type: String - Currently only 'metabolite' is supported - use_hyphens: Boolean - If True, double underscores (__) in an SBML ID will be converted to - hyphens + filename : path to SBML, or SBML string, or filehandle Returns ------- - string: the identifier + libsbml.SBMLDocument """ - if use_hyphens: - the_id = the_id.replace('__', '-') - if the_type == 'metabolite': - if the_id.split('_')[-1] == the_compartment: - # Reformat Ids to match convention in Palsson Lab. - the_id = the_id[:-len(the_compartment) - 1] - the_id += '[%s]' % the_compartment - return the_id + if isinstance(filename, string_types): + if ("win" in platform) and (len(filename) < 260) \ + and os.path.exists(filename): + # path (win) + doc = libsbml.readSBMLFromFile(filename) # noqa: E501 type: libsbml.SBMLDocument + elif ("win" not in platform) and os.path.exists(filename): + # path other + doc = libsbml.readSBMLFromFile(filename) # noqa: E501 type: libsbml.SBMLDocument + else: + # string representation + if " 0: + gpr = gpr.replace("(", ";") + gpr = gpr.replace(")", ";") + gpr = gpr.replace("or", ";") + gpr = gpr.replace("and", ";") + gids = [t.strip() for t in gpr.split(';')] + + # create missing genes + for gid in gids: + if f_replace and F_GENE in f_replace: + gid = f_replace[F_GENE](gid) + + if gid not in cobra_model.genes: + cobra_gene = Gene(gid) + cobra_gene.name = gid + cobra_model.genes.append(cobra_gene) + + # GPR rules + def process_association(ass): + """ Recursively convert gpr association to a gpr string. + Defined as inline functions to not pass the replacement dict around. + """ + if ass.isFbcOr(): + return " ".join( + ["(", ' or '.join(process_association(c) + for c in ass.getListOfAssociations()), ")"] + ) + elif ass.isFbcAnd(): + return " ".join( + ["(", ' and '.join(process_association(c) + for c in ass.getListOfAssociations()), ")"]) + elif ass.isGeneProductRef(): + gid = ass.getGeneProduct() + if f_replace and F_GENE in f_replace: + return f_replace[F_GENE](gid) + else: + return gid + + # Reactions + missing_bounds = False + reactions = [] + for reaction in model.getListOfReactions(): # type: libsbml.Reaction + rid = _check_required(reaction, reaction.getId(), "id") + if f_replace and F_REACTION in f_replace: + rid = f_replace[F_REACTION](rid) + cobra_reaction = Reaction(rid) + cobra_reaction.name = reaction.getName() + cobra_reaction.annotation = _parse_annotations(reaction) + cobra_reaction.notes = _parse_notes_dict(reaction) + + # set bounds + p_ub, p_lb = None, None + r_fbc = reaction.getPlugin("fbc") # type: libsbml.FbcReactionPlugin + if r_fbc: + # bounds in fbc + lb_id = r_fbc.getLowerFluxBound() + if lb_id: + p_lb = model.getParameter(lb_id) # type: libsbml.Parameter + if p_lb and p_lb.getConstant() and \ + (p_lb.getValue() is not None): + cobra_reaction.lower_bound = p_lb.getValue() else: - cobra_metabolites[ - tmp_metabolite] = sbml_metabolite.getStoichiometry() - # check for nan - for met, v in iteritems(cobra_metabolites): - if isnan(v) or isinf(v): - warn("invalid value %s for metabolite '%s' in reaction '%s'" % - (str(v), met.id, reaction.id)) - reaction.add_metabolites(cobra_metabolites) - # Parse the kinetic law info here. - parameter_dict = {} - # If lower and upper bounds are specified in the Kinetic Law then - # they override the sbml reversible attribute. If they are not - # specified then the bounds are determined by getReversible. - if not sbml_reaction.getKineticLaw(): - - if sbml_reaction.getReversible(): - parameter_dict['lower_bound'] = CONFIGURATION.lower_bound - parameter_dict['upper_bound'] = CONFIGURATION.upper_bound + raise CobraSBMLError("No constant bound '%s' for " + "reaction: %s" % (p_lb, reaction)) + + ub_id = r_fbc.getUpperFluxBound() + if ub_id: + p_ub = model.getParameter(ub_id) # type: libsbml.Parameter + if p_ub and p_ub.getConstant() and \ + (p_ub.getValue() is not None): + cobra_reaction.upper_bound = p_ub.getValue() + else: + raise CobraSBMLError("No constant bound '%s' for " + "reaction: %s" % (p_ub, reaction)) + + elif reaction.isSetKineticLaw(): + # some legacy models encode bounds in kinetic laws + klaw = reaction.getKineticLaw() # type: libsbml.KineticLaw + p_lb = klaw.getParameter("LOWER_BOUND") # noqa: E501 type: libsbml.LocalParameter + if p_lb: + cobra_reaction.lower_bound = p_lb.getValue() else: - # Assume that irreversible reactions only proceed from left to - # right. - parameter_dict['lower_bound'] = 0 - parameter_dict['upper_bound'] = CONFIGURATION.upper_bound - - parameter_dict[ - 'objective_coefficient'] = __default_objective_coefficient - else: - for sbml_parameter in \ - sbml_reaction.getKineticLaw().getListOfParameters(): - parameter_dict[ - sbml_parameter.getId().lower()] = sbml_parameter.getValue() - - if 'lower_bound' in parameter_dict: - reaction.lower_bound = parameter_dict['lower_bound'] - elif 'lower bound' in parameter_dict: - reaction.lower_bound = parameter_dict['lower bound'] - elif sbml_reaction.getReversible(): - reaction.lower_bound = CONFIGURATION.lower_bound + raise CobraSBMLError("Missing flux bounds on reaction: %s", + reaction) + p_ub = klaw.getParameter("UPPER_BOUND") # noqa: E501 type: libsbml.LocalParameter + if p_ub: + cobra_reaction.upper_bound = p_ub.getValue() + else: + raise CobraSBMLError("Missing flux bounds on reaction %s", + reaction) + + LOGGER.warning("Encoding LOWER_BOUND and UPPER_BOUND in " + "KineticLaw is discouraged, " + "use fbc:fluxBounds instead: %s", reaction) + + if p_lb is None: + LOGGER.error("Missing lower flux bound for reaction: " + "%s", reaction) + missing_bounds = True + if p_ub is None: + LOGGER.error("Missing upper flux bound for reaction: " + "%s", reaction) + missing_bounds = True + + # add reaction + reactions.append(cobra_reaction) + + # parse equation + stoichiometry = defaultdict(lambda: 0) + for sref in reaction.getListOfReactants(): # noqa: E501 type: libsbml.SpeciesReference + sid = sref.getSpecies() + if f_replace and F_SPECIE in f_replace: + sid = f_replace[F_SPECIE](sid) + stoichiometry[sid] -= number( + _check_required(sref, sref.getStoichiometry(), + "stoichiometry")) + + for sref in reaction.getListOfProducts(): # noqa: E501 type: libsbml.SpeciesReference + sid = sref.getSpecies() + if f_replace and F_SPECIE in f_replace: + sid = f_replace[F_SPECIE](sid) + stoichiometry[sid] += number( + _check_required(sref, sref.getStoichiometry(), + "stoichiometry")) + + # convert to metabolite objects + object_stoichiometry = {} + for met_id in stoichiometry: + metabolite = cobra_model.metabolites.get_by_id(met_id) + object_stoichiometry[metabolite] = stoichiometry[met_id] + cobra_reaction.add_metabolites(object_stoichiometry) + + # GPR + if r_fbc: + gpr = '' + gpa = r_fbc.getGeneProductAssociation() # noqa: E501 type: libsbml.GeneProductAssociation + if gpa is not None: + association = gpa.getAssociation() # noqa: E501 type: libsbml.FbcAssociation + gpr = process_association(association) else: - reaction.lower_bound = 0 + # fallback to notes information + notes = cobra_reaction.notes + if "GENE ASSOCIATION" in notes: + gpr = notes['GENE ASSOCIATION'] + elif "GENE_ASSOCIATION" in notes: + gpr = notes['GENE_ASSOCIATION'] + else: + gpr = '' - if 'upper_bound' in parameter_dict: - reaction.upper_bound = parameter_dict['upper_bound'] - elif 'upper bound' in parameter_dict: - reaction.upper_bound = parameter_dict['upper bound'] + if len(gpr) > 0: + LOGGER.warning("Use of GENE ASSOCIATION or GENE_ASSOCIATION " + "in the notes element is discouraged, use " + "fbc:gpr instead: %s", reaction) + if f_replace and F_GENE in f_replace: + gpr = " ".join( + f_replace[F_GENE](t) for t in gpr.split(' ') + ) + + # remove outside parenthesis, if any + if gpr.startswith("(") and gpr.endswith(")"): + gpr = gpr[1:-1].strip() + + cobra_reaction.gene_reaction_rule = gpr + + cobra_model.add_reactions(reactions) + if missing_bounds: + raise CobraSBMLError("Missing flux bounds on reactions.") + + # Objective + obj_direction = "max" + coefficients = {} + if model_fbc: + obj_list = model_fbc.getListOfObjectives() # noqa: E501 type: libsbml.ListOfObjectives + if obj_list is None: + LOGGER.warning("listOfObjectives element not found") + elif obj_list.size() == 0: + LOGGER.warning("No objective in listOfObjectives") + elif not obj_list.getActiveObjective(): + LOGGER.warning("No active objective in listOfObjectives") else: - reaction.upper_bound = CONFIGURATION.upper_bound - - objective_coefficient = parameter_dict.get( - 'objective_coefficient', parameter_dict.get( - 'objective_coefficient', __default_objective_coefficient)) - if objective_coefficient != 0: - coefficients[reaction] = objective_coefficient - - # ensure values are not set to nan or inf - if isnan(reaction.lower_bound) or isinf(reaction.lower_bound): - reaction.lower_bound = CONFIGURATION.lower_bound - if isnan(reaction.upper_bound) or isinf(reaction.upper_bound): - reaction.upper_bound = CONFIGURATION.upper_bound - - reaction_note_dict = parse_legacy_sbml_notes( - sbml_reaction.getNotesString()) - # Parse the reaction notes. - # POTENTIAL BUG: DEALING WITH LEGACY 'SBML' THAT IS NOT IN A - # STANDARD FORMAT - # TODO: READ IN OTHER NOTES AND GIVE THEM A reaction_ prefix. - # TODO: Make sure genes get added as objects - if 'GENE ASSOCIATION' in reaction_note_dict: - rule = reaction_note_dict['GENE ASSOCIATION'][0] - try: - rule.encode('ascii') - except (UnicodeEncodeError, UnicodeDecodeError): - warn("gene_reaction_rule '%s' is not ascii compliant" % rule) - if rule.startswith(""") and rule.endswith("""): - rule = rule[6:-6] - reaction.gene_reaction_rule = rule - if 'GENE LIST' in reaction_note_dict: - reaction.systematic_names = reaction_note_dict['GENE LIST'][0] - elif ('GENES' in reaction_note_dict and - reaction_note_dict['GENES'] != ['']): - reaction.systematic_names = reaction_note_dict['GENES'][0] - elif 'LOCUS' in reaction_note_dict: - gene_id_to_object = dict([(x.id, x) for x in reaction._genes]) - for the_row in reaction_note_dict['LOCUS']: - tmp_row_dict = {} - the_row = 'LOCUS:' + the_row.lstrip('_').rstrip('#') - for the_item in the_row.split('#'): - k, v = the_item.split(':') - tmp_row_dict[k] = v - tmp_locus_id = tmp_row_dict['LOCUS'] - if 'TRANSCRIPT' in tmp_row_dict: - tmp_locus_id = tmp_locus_id + \ - '.' + tmp_row_dict['TRANSCRIPT'] - - if 'ABBREVIATION' in tmp_row_dict: - gene_id_to_object[tmp_locus_id].name = tmp_row_dict[ - 'ABBREVIATION'] - - if 'SUBSYSTEM' in reaction_note_dict: - reaction.subsystem = reaction_note_dict.pop('SUBSYSTEM')[0] - - reaction.notes = reaction_note_dict - - # Now, add all of the reactions to the model. - cobra_model.id = sbml_model.getId() - # Populate the compartment list - This will be done based on - # cobra.Metabolites in cobra.Reactions in the future. - cobra_model.compartments = compartment_dict - - cobra_model.add_reactions(cobra_reaction_list) + obj_id = obj_list.getActiveObjective() + obj = model_fbc.getObjective(obj_id) # type: libsbml.Objective + obj_direction = LONG_SHORT_DIRECTION[obj.getType()] + + for flux_obj in obj.getListOfFluxObjectives(): # noqa: E501 type: libsbml.FluxObjective + rid = flux_obj.getReaction() + if f_replace and F_REACTION in f_replace: + rid = f_replace[F_REACTION](rid) + try: + objective_reaction = cobra_model.reactions.get_by_id(rid) + except KeyError: + raise CobraSBMLError("Objective reaction '%s' " + "not found" % rid) + try: + coefficients[objective_reaction] = number( + flux_obj.getCoefficient() + ) + except ValueError as e: + LOGGER.warning(str(e)) + set_objective(cobra_model, coefficients) + cobra_model.solver.objective.direction = obj_direction + else: + # some legacy models encode objective coefficients in kinetic laws + for cobra_reaction in model.getListOfReactions(): # noqa: E501 type: libsbml.Reaction + if cobra_reaction.isSetKineticLaw(): + + klaw = reaction.getKineticLaw() # type: libsbml.KineticLaw + p_oc = klaw.getParameter( + "OBJECTIVE_COEFFICIENT") # noqa: E501 type: libsbml.LocalParameter + if p_oc: + rid = cobra_reaction.getId() + if f_replace and F_REACTION in f_replace: + rid = f_replace[F_REACTION](rid) + try: + objective_reaction = cobra_model.reactions.get_by_id( + rid) + except KeyError: + raise CobraSBMLError("Objective reaction '%s' " + "not found", rid) + try: + coefficients[objective_reaction] = number( + p_oc.getValue()) + except ValueError as e: + LOGGER.warning(str(e)) + + LOGGER.warning("Encoding OBJECTIVE_COEFFICIENT in " + "KineticLaw is discouraged, " + "use fbc:fluxObjective " + "instead: %s", cobra_reaction) + set_objective(cobra_model, coefficients) + cobra_model.solver.objective.direction = obj_direction + + # parse groups + model_groups = model.getPlugin("groups") # type: libsbml.GroupsModelPlugin + groups = [] + if model_groups: + # calculate hashmaps to lookup objects in O(1) + sid_map = {} + metaid_map = {} + for obj_list in [model.getListOfCompartments(), + model.getListOfSpecies(), + model.getListOfReactions(), + model_groups.getListOfGroups()]: + + for sbase in obj_list: # type: libsbml.SBase + if sbase.isSetId(): + sid_map[sbase.getId()] = sbase + if sbase.isSetMetaId(): + metaid_map[sbase.getMetaId()] = sbase + + # create groups + for group in model_groups.getListOfGroups(): # type: libsbml.Group + cobra_group = Group(group.getId()) + cobra_group.name = group.getName() + if group.isSetKind(): + cobra_group.kind = group.getKindAsString() + cobra_group.annotation = _parse_annotations(group) + cobra_group.notes = _parse_notes_dict(group) + + cobra_members = [] + for member in group.getListOfMembers(): # type: libsbml.Member + if member.isSetIdRef(): + obj = sid_map[member.getIdRef()] + # obj = doc.getElementBySId(member.getIdRef()) + elif member.isSetMetaIdRef(): + obj = metaid_map[member.getMetaIdRef()] + # obj = doc.getElementByMetaId(member.getMetaIdRef()) + + typecode = obj.getTypeCode() + obj_id = obj.getId() + + # id replacements + cobra_member = None + if typecode == libsbml.SBML_SPECIES: + if f_replace and F_SPECIE in f_replace: + obj_id = f_replace[F_SPECIE](obj_id) + cobra_member = cobra_model.metabolites.get_by_id(obj_id) + elif typecode == libsbml.SBML_REACTION: + if f_replace and F_REACTION in f_replace: + obj_id = f_replace[F_REACTION](obj_id) + cobra_member = cobra_model.reactions.get_by_id(obj_id) + elif typecode == libsbml.SBML_FBC_GENEPRODUCT: + if f_replace and F_GENE in f_replace: + obj_id = f_replace[F_GENE](obj_id) + cobra_member = cobra_model.genes.get_by_id(obj_id) + else: + LOGGER.warning("Member %s could not be added to group %s." + "unsupported type code: " + "%s" % (member, group, typecode)) + + if cobra_member: + cobra_members.append(cobra_member) + + cobra_group.add_members(cobra_members) + groups.append(cobra_group) + else: + # parse deprecated subsystems on reactions + groups_dict = {} + for cobra_reaction in cobra_model.reactions: + if "SUBSYSTEM" in cobra_reaction.notes: + g_name = cobra_reaction.notes["SUBSYSTEM"] + if g_name in groups_dict: + groups_dict[g_name].append(cobra_reaction) + else: + groups_dict[g_name] = [cobra_reaction] + + for gid, cobra_members in groups_dict.items(): + cobra_group = Group(gid, name=gid, kind="collection") + cobra_group.add_members(cobra_members) + groups.append(cobra_group) + + cobra_model.add_groups(groups) + return cobra_model -def parse_legacy_sbml_notes(note_string, note_delimiter=':'): - """Deal with various legacy SBML format issues. - """ - note_dict = {} - start_tag = '

' - end_tag = '

' - if '' in note_string: - start_tag = '' - end_tag = '' - while start_tag in note_string and end_tag in note_string: - note_start = note_string.index(start_tag) - note_end = note_string.index(end_tag) - the_note = note_string[ - (note_start + len(start_tag)):note_end].lstrip(' ').rstrip( - ' ') - if note_delimiter in the_note: - note_delimiter_index = the_note.index(note_delimiter) - note_field = the_note[:note_delimiter_index].lstrip( - ' ').rstrip(' ').replace('_', ' ').upper() - note_value = the_note[ - (note_delimiter_index + 1):].lstrip(' ').rstrip(' ') - if note_field in note_dict: - note_dict[note_field].append(note_value) - else: - note_dict[note_field] = [note_value] - note_string = note_string[(note_end + len(end_tag)):] +# ----------------------------------------------------------------------------- +# Write SBML +# ----------------------------------------------------------------------------- +def write_sbml_model(cobra_model, filename, f_replace=F_REPLACE, **kwargs): + """Writes cobra model to filename. + + The created model is SBML level 3 version 1 (L1V3) with + fbc package v2 (fbc-v2). + + If the given filename ends with the suffix ".gz" (for example, + "myfile.xml.gz"), libSBML assumes the caller wants the file to be + written compressed in gzip format. Similarly, if the given filename + ends with ".zip" or ".bz2", libSBML assumes the caller wants the + file to be compressed in zip or bzip2 format (respectively). Files + whose names lack these suffixes will be written uncompressed. Special + considerations for the zip format: If the given filename ends with + ".zip", the file placed in the zip archive will have the suffix + ".xml" or ".sbml". For example, the file in the zip archive will + be named "test.xml" if the given filename is "test.xml.zip" or + "test.zip". Similarly, the filename in the archive will be + "test.sbml" if the given filename is "test.sbml.zip". - if ('CHARGE' in note_dict and - note_dict['CHARGE'][0].lower() in ['none', 'na', 'nan']): - note_dict.pop('CHARGE') # Remove non-numeric charges + Parameters + ---------- + cobra_model : cobra.core.Model + Model instance which is written to SBML + filename : string + path to which the model is written + use_fbc_package : boolean {True, False} + should the fbc package be used + f_replace: dict of replacement functions for id replacement + """ + doc = _model_to_sbml(cobra_model, f_replace=f_replace, **kwargs) - if 'CHARGE' in note_dict and note_dict['CHARGE'][0].lower() in ['none', - 'na', - 'nan']: - note_dict.pop('CHARGE') # Remove non-numeric charges + if isinstance(filename, string_types): + # write to path + libsbml.writeSBMLToFile(doc, filename) - return note_dict + elif hasattr(filename, "write"): + # write to file handle + sbml_str = libsbml.writeSBMLToString(doc) + filename.write(sbml_str) -def write_cobra_model_to_sbml_file(cobra_model, sbml_filename, - sbml_level=2, sbml_version=1, - print_time=False, - use_fbc_package=True): - """Write a cobra.Model object to an SBML XML file. +def _model_to_sbml(cobra_model, f_replace=None, units=True): + """Convert Cobra model to SBMLDocument. Parameters ---------- - cobra_model : cobra.core.Model.Model - The model object to write - sbml_filename : string - The file to write the SBML XML to. - sbml_level : int - 2 is the only supported level. - sbml_version : int - 1 is the only supported version. - print_time : bool - deprecated - use_fbc_package : bool - Convert the model to the FBC package format to improve portability. - http://sbml.org/Documents/Specifications/SBML_Level_3/Packages/Flux_Balance_Constraints_(flux) + cobra_model : cobra.core.Model + Cobra model instance + f_replace : dict of replacement functions + Replacement to apply on identifiers. + units : boolean + Should the FLUX_UNITS be written in the SBMLDocument. - Notes - ----- - TODO: Update the NOTES to match the SBML standard and provide support for - Level 2 Version 4 + Returns + ------- + libsbml.SBMLDocument """ - if not libsbml: - raise ImportError('write_cobra_model_to_sbml_file ' - 'requires python-libsbml') - sbml_doc = get_libsbml_document(cobra_model, - sbml_level=sbml_level, - sbml_version=sbml_version, - print_time=print_time, - use_fbc_package=use_fbc_package) - - libsbml.writeSBML(sbml_doc, sbml_filename) - - -def get_libsbml_document(cobra_model, - sbml_level=2, sbml_version=1, - print_time=False, - use_fbc_package=True): - """ Return a libsbml document object for writing to a file. This function - is used by write_cobra_model_to_sbml_file(). """ - note_start_tag, note_end_tag = '

', '

' - if sbml_level > 2 or (sbml_level == 2 and sbml_version == 4): - note_start_tag, note_end_tag = '', '' - - sbml_doc = libsbml.SBMLDocument(sbml_level, sbml_version) - sbml_model = sbml_doc.createModel(cobra_model.id.split('.')[0]) - # Note need to set units - reaction_units = 'mmol_per_gDW_per_hr' - model_units = sbml_model.createUnitDefinition() - model_units.setId(reaction_units) - sbml_unit = model_units.createUnit() - sbml_unit.setKind(libsbml.UNIT_KIND_MOLE) - sbml_unit.setScale(-3) - sbml_unit = model_units.createUnit() - sbml_unit.setKind(libsbml.UNIT_KIND_GRAM) - sbml_unit.setExponent(-1) - sbml_unit = model_units.createUnit() - sbml_unit.setKind(libsbml.UNIT_KIND_SECOND) - sbml_unit.setMultiplier(1.0 / 60 / 60) - sbml_unit.setExponent(-1) - - # Add in the common compartment abbreviations. If there are additional - # compartments they also need to be added. - if not cobra_model.compartments: - cobra_model.compartments = {'c': 'cytosol', - 'p': 'periplasm', - 'e': 'extracellular'} - for the_key in cobra_model.compartments.keys(): - sbml_comp = sbml_model.createCompartment() - sbml_comp.setId(the_key) - sbml_comp.setName(cobra_model.compartments[the_key]) - sbml_comp.setSize(1) # Just to get rid of warnings - - if print_time: - warn("print_time is deprecated", DeprecationWarning) - # Use this dict to allow for fast look up of species id - # for references created in the reaction section. - metabolite_dict = {} - - for cobra_metabolite in cobra_model.metabolites: - metabolite_dict[cobra_metabolite.id] = add_sbml_species( - sbml_model, cobra_metabolite, note_start_tag=note_start_tag, - note_end_tag=note_end_tag) - - for the_reaction in cobra_model.reactions: - # This is probably the culprit. Including cobra.Reaction - # objects explicitly in cobra.Model will speed this up. - sbml_reaction = sbml_model.createReaction() - # Need to remove - for proper SBML. Replace with __ - the_reaction_id = 'R_' + the_reaction.id.replace('-', '__') - sbml_reaction.setId(the_reaction_id) - # The reason we are not using the Reaction.reversibility property - # is because the SBML definition of reversibility does not quite - # match with the cobra definition. In cobra, reversibility implies - # that both positive and negative flux values are feasible. However, - # SBML requires negative-flux-only reactions to still be classified - # as reversible. To quote from the SBML Level 3 Version 1 Spec: - # > However, labeling a reaction as irreversible is interpreted as - # > an assertion that the rate expression will not have negative - # > values during a simulation. - # (Page 60 lines 44-45) - sbml_reaction.setReversible(the_reaction.lower_bound < 0) - if the_reaction.name: - sbml_reaction.setName(the_reaction.name) + if f_replace is None: + f_replace = {} + + sbml_ns = libsbml.SBMLNamespaces(3, 1) # SBML L3V1 + sbml_ns.addPackageNamespace("fbc", 2) # fbc-v2 + + doc = libsbml.SBMLDocument(sbml_ns) # noqa: E501 type: libsbml.SBMLDocument + doc.setPackageRequired("fbc", False) + doc.setSBOTerm(SBO_FBA_FRAMEWORK) + + model = doc.createModel() # type: libsbml.Model + model_fbc = model.getPlugin("fbc") # type: libsbml.FbcModelPlugin + model_fbc.setStrict(True) + + if cobra_model.id is not None: + model.setId(cobra_model.id) + model.setMetaId("meta_" + cobra_model.id) + else: + model.setMetaId("meta_model") + if cobra_model.name is not None: + model.setName(cobra_model.name) + + # Meta information (ModelHistory) + if hasattr(cobra_model, "_sbml"): + meta = cobra_model._sbml + if "annotation" in meta: + _sbase_annotations(doc, meta["annotation"]) + if "notes" in meta: + _sbase_notes_dict(doc, meta["notes"]) + + history = libsbml.ModelHistory() # type: libsbml.ModelHistory + if "created" in meta and meta["created"]: + history.setCreatedDate(meta["created"]) else: - sbml_reaction.setName(the_reaction.id) - # Add in the reactant/product references - for the_metabolite, the_coefficient in \ - iteritems(the_reaction._metabolites): - sbml_stoichiometry = the_coefficient - metabolite_id = str(metabolite_dict[the_metabolite.id]) - # Each SpeciesReference must have a unique id - if sbml_stoichiometry < 0: - species_reference = sbml_reaction.createReactant() - else: - species_reference = sbml_reaction.createProduct() - species_reference.setId(metabolite_id + '_' + the_reaction_id) - species_reference.setSpecies(metabolite_id) - species_reference.setStoichiometry(abs(sbml_stoichiometry)) - # Deal with the case where the reaction is a boundary reaction - if len(the_reaction._metabolites) == 1: - the_metabolite, the_coefficient = list( - the_reaction._metabolites.items())[0] - metabolite_id = add_sbml_species(sbml_model, the_metabolite, - note_start_tag=note_start_tag, - note_end_tag=note_end_tag, - boundary_metabolite=True) - sbml_stoichiometry = -the_coefficient - # Each SpeciesReference must have a unique id - if sbml_stoichiometry < 0: - species_reference = sbml_reaction.createReactant() - else: - species_reference = sbml_reaction.createProduct() - species_reference.setId(metabolite_id + '_' + the_reaction_id) - species_reference.setSpecies(metabolite_id) - species_reference.setStoichiometry(abs(sbml_stoichiometry)) - - # Add in the kineticLaw - sbml_law = libsbml.KineticLaw(sbml_level, sbml_version) - if hasattr(sbml_law, 'setId'): - sbml_law.setId('FLUX_VALUE') - sbml_law.setFormula('FLUX_VALUE') - reaction_parameter_dict = { - 'LOWER_BOUND': [the_reaction.lower_bound, reaction_units], - 'UPPER_BOUND': [the_reaction.upper_bound, reaction_units], - 'FLUX_VALUE': [0, reaction_units], - 'OBJECTIVE_COEFFICIENT': [the_reaction.objective_coefficient, - 'dimensionless']} - for k, v in reaction_parameter_dict.items(): - sbml_parameter = libsbml.Parameter(sbml_level, sbml_version) - sbml_parameter.setId(k) - if hasattr(v, '__iter__'): - sbml_parameter.setValue(v[0]) - sbml_parameter.setUnits(v[1]) + time = datetime.datetime.now() + timestr = time.strftime('%Y-%m-%dT%H:%M:%S') + date = libsbml.Date(timestr) + _check(history.setCreatedDate(date), 'set creation date') + _check(history.setModifiedDate(date), 'set modified date') + + if "creators" in meta: + for cobra_creator in meta["creators"]: + creator = libsbml.ModelCreator() # noqa: E501 type: libsbml.ModelCreator + if cobra_creator.get("familyName", None): + creator.setFamilyName(cobra_creator["familyName"]) + if cobra_creator.get("givenName", None): + creator.setGivenName(cobra_creator["givenName"]) + if cobra_creator.get("organisation", None): + creator.setOrganisation(cobra_creator["organisation"]) + if cobra_creator.get("email", None): + creator.setEmail(cobra_creator["email"]) + + _check(history.addCreator(creator), + "adding creator to ModelHistory.") + _check(model.setModelHistory(history), 'set model history') + + # Units + if units: + flux_udef = model.createUnitDefinition() # noqa: E501 type: libsbml.UnitDefinition + flux_udef.setId(UNITS_FLUX[0]) + for u in UNITS_FLUX[1]: + unit = flux_udef.createUnit() # type: libsbml.Unit + unit.setKind(u.kind) + unit.setExponent(u.exponent) + unit.setScale(u.scale) + unit.setMultiplier(u.multiplier) + + # minimum and maximum value from model + if len(cobra_model.reactions) > 0: + min_value = min(cobra_model.reactions.list_attr("lower_bound")) + max_value = max(cobra_model.reactions.list_attr("upper_bound")) + else: + min_value = config.lower_bound + max_value = config.upper_bound + + _create_parameter(model, pid=LOWER_BOUND_ID, + value=min_value, sbo=SBO_DEFAULT_FLUX_BOUND) + _create_parameter(model, pid=UPPER_BOUND_ID, + value=max_value, sbo=SBO_DEFAULT_FLUX_BOUND) + _create_parameter(model, pid=ZERO_BOUND_ID, + value=0, sbo=SBO_DEFAULT_FLUX_BOUND) + _create_parameter(model, pid=BOUND_MINUS_INF, + value=-float("Inf"), sbo=SBO_FLUX_BOUND) + _create_parameter(model, pid=BOUND_PLUS_INF, + value=float("Inf"), sbo=SBO_FLUX_BOUND) + + # Compartments + # FIXME: use first class compartment model (and write notes & annotations) + # (https://github.com/opencobra/cobrapy/issues/811) + for cid, name in iteritems(cobra_model.compartments): + compartment = model.createCompartment() # type: libsbml.Compartment + compartment.setId(cid) + compartment.setName(name) + compartment.setConstant(True) + + # FIXME: write annotations and notes + # _sbase_notes(c, com.notes) + # _sbase_annotations(c, com.annotation) + + # Species + for metabolite in cobra_model.metabolites: + specie = model.createSpecies() # type: libsbml.Species + mid = metabolite.id + if f_replace and F_SPECIE_REV in f_replace: + mid = f_replace[F_SPECIE_REV](mid) + specie.setId(mid) + specie.setConstant(False) + specie.setBoundaryCondition(False) + specie.setHasOnlySubstanceUnits(False) + specie.setName(metabolite.name) + specie.setCompartment(metabolite.compartment) + s_fbc = specie.getPlugin("fbc") # type: libsbml.FbcSpeciesPlugin + if metabolite.charge is not None: + s_fbc.setCharge(metabolite.charge) + if metabolite.formula is not None: + s_fbc.setChemicalFormula(metabolite.formula) + + _sbase_annotations(specie, metabolite.annotation) + _sbase_notes_dict(specie, metabolite.notes) + + # Genes + for cobra_gene in cobra_model.genes: + gp = model_fbc.createGeneProduct() # type: libsbml.GeneProduct + gid = cobra_gene.id + if f_replace and F_GENE_REV in f_replace: + gid = f_replace[F_GENE_REV](gid) + gp.setId(gid) + gname = cobra_gene.name + if gname is None or len(gname) == 0: + gname = gid + gp.setName(gname) + gp.setLabel(gid) + + _sbase_annotations(gp, cobra_gene.annotation) + _sbase_notes_dict(gp, cobra_gene.notes) + + # Objective + objective = model_fbc.createObjective() # type: libsbml.Objective + objective.setId("obj") + objective.setType(SHORT_LONG_DIRECTION[cobra_model.objective.direction]) + model_fbc.setActiveObjectiveId("obj") + + # Reactions + reaction_coefficients = linear_reaction_coefficients(cobra_model) + for cobra_reaction in cobra_model.reactions: + rid = cobra_reaction.id + if f_replace and F_REACTION_REV in f_replace: + rid = f_replace[F_REACTION_REV](rid) + reaction = model.createReaction() # type: libsbml.Reaction + reaction.setId(rid) + reaction.setName(cobra_reaction.name) + reaction.setFast(False) + reaction.setReversible((cobra_reaction.lower_bound < 0)) + _sbase_annotations(reaction, cobra_reaction.annotation) + _sbase_notes_dict(reaction, cobra_reaction.notes) + + # stoichiometry + for metabolite, stoichiometry in iteritems(cobra_reaction._metabolites): # noqa: E501 + sid = metabolite.id + if f_replace and F_SPECIE_REV in f_replace: + sid = f_replace[F_SPECIE_REV](sid) + if stoichiometry < 0: + sref = reaction.createReactant() # noqa: E501 type: libsbml.SpeciesReference + sref.setSpecies(sid) + sref.setStoichiometry(-stoichiometry) + sref.setConstant(True) else: - sbml_parameter.setValue(v) - sbml_law.addParameter(sbml_parameter) - sbml_reaction.setKineticLaw(sbml_law) - - # Checks if GPR and Subsystem annotations are present in the notes - # section and if they are the same as those in the reaction's - # gene_reaction_rule/ subsystem attribute. If they are not identical, - # they are set to be identical - note_dict = the_reaction.notes.copy() - if the_reaction.gene_reaction_rule: - note_dict['GENE ASSOCIATION'] = [ - str(the_reaction.gene_reaction_rule)] - if the_reaction.subsystem: - note_dict['SUBSYSTEM'] = [str(the_reaction.subsystem)] - - # In a cobrapy model the notes section is stored as a dictionary. The - # following section turns the key-value-pairs of the dictionary into a - # string and replaces recurring symbols so that the string has the i - # required syntax for an SBML doc. - note_str = str(list(iteritems(note_dict))) - note_start_tag, note_end_tag, note_delimiter = '

', '

', ':' - note_str = note_str.replace('(\'', note_start_tag) - note_str = note_str.replace('\']),', note_end_tag) - note_str = note_str.replace('\',', note_delimiter) - note_str = note_str.replace('\']', '') - note_str = note_str.replace('[\'', '') - note_str = note_str.replace( - '[', '') - note_str = note_str.replace(')]', note_end_tag + '') - sbml_reaction.setNotes(note_str) - - if use_fbc_package: - try: - from libsbml import ConversionProperties, LIBSBML_OPERATION_SUCCESS - conversion_properties = ConversionProperties() - conversion_properties.addOption( - "convert cobra", True, "Convert Cobra model") - result = sbml_doc.convert(conversion_properties) - if result != LIBSBML_OPERATION_SUCCESS: - raise Exception("Conversion of COBRA to SBML+fbc failed") - except Exception as e: - error_string = 'Error saving as SBML+fbc. %s' - try: - # Check whether the FbcExtension is there - from libsbml import FbcExtension - error_string = error_string % e - except ImportError: - error_string = error_string % "FbcExtension not available in " - "libsbml. If use_fbc_package == True then libsbml must be " - "compiled with the fbc extension." - from libsbml import getLibSBMLDottedVersion - _sbml_version = getLibSBMLDottedVersion() - _major, _minor, _patch = map(int, _sbml_version.split('.')) - if _major < 5 or (_major == 5 and _minor < 8): - error_string += "You've got libsbml %s installed. " - "You need 5.8.0 or later with the fbc package" - - raise Exception(error_string) - return sbml_doc - - -def add_sbml_species(sbml_model, cobra_metabolite, note_start_tag, - note_end_tag, boundary_metabolite=False): - """A helper function for adding cobra metabolites to an sbml model. + sref = reaction.createProduct() # noqa: E501 type: libsbml.SpeciesReference + sref.setSpecies(sid) + sref.setStoichiometry(stoichiometry) + sref.setConstant(True) + + # bounds + r_fbc = reaction.getPlugin("fbc") # type: libsbml.FbcReactionPlugin + r_fbc.setLowerFluxBound(_create_bound(model, cobra_reaction, + "lower_bound", + f_replace=f_replace, units=units, + flux_udef=flux_udef)) + r_fbc.setUpperFluxBound(_create_bound(model, cobra_reaction, + "upper_bound", + f_replace=f_replace, units=units, + flux_udef=flux_udef)) + + # GPR + gpr = cobra_reaction.gene_reaction_rule + if gpr is not None and len(gpr) > 0: + gpa = r_fbc.createGeneProductAssociation() # noqa: E501 type: libsbml.GeneProductAssociation + # replace ids + if f_replace and F_GENE_REV in f_replace: + tokens = gpr.split(' ') + for k in range(len(tokens)): + if tokens[k] not in ['and', 'or', '(', ')']: + tokens[k] = f_replace[F_GENE_REV](tokens[k]) + gpr = " ".join(tokens) + + gpa.setAssociation(gpr) + + # objective coefficients + if reaction_coefficients.get(cobra_reaction, 0) != 0: + flux_obj = objective.createFluxObjective() # noqa: E501 type: libsbml.FluxObjective + flux_obj.setReaction(rid) + flux_obj.setCoefficient(cobra_reaction.objective_coefficient) + + # write groups + if len(cobra_model.groups) > 0: + doc.enablePackage( + "http://www.sbml.org/sbml/level3/version1/groups/version1", + "groups", True) + doc.setPackageRequired("groups", False) + model_group = model.getPlugin("groups") # noqa: E501 type: libsbml.GroupsModelPlugin + for cobra_group in cobra_model.groups: + group = model_group.createGroup() # type: libsbml.Group + group.setId(cobra_group.id) + group.setName(cobra_group.name) + group.setKind(cobra_group.kind) + + _sbase_notes_dict(group, cobra_group.notes) + _sbase_annotations(group, cobra_group.annotation) + + for cobra_member in cobra_group.members: + member = group.createMember() # type: libsbml.Member + mid = cobra_member.id + m_type = str(type(cobra_member)) + + # id replacements + if "Reaction" in m_type: + if f_replace and F_REACTION_REV in f_replace: + mid = f_replace[F_REACTION_REV](mid) + if "Metabolite" in m_type: + if f_replace and F_SPECIE_REV in f_replace: + mid = f_replace[F_SPECIE_REV](mid) + if "Gene" in m_type: + if f_replace and F_GENE_REV in f_replace: + mid = f_replace[F_GENE_REV](mid) + + member.setIdRef(mid) + if cobra_member.name and len(cobra_member.name) > 0: + member.setName(cobra_member.name) + + return doc + + +def _create_bound(model, reaction, bound_type, f_replace, units=None, + flux_udef=None): + """Creates bound in model for given reaction. + + Adds the parameters for the bounds to the SBML model. Parameters ---------- - sbml_model: sbml_model object + model : libsbml.Model + SBML model instance + reaction : cobra.core.Reaction + Cobra reaction instance from which the bounds are read. + bound_type : {LOWER_BOUND, UPPER_BOUND} + Type of bound + f_replace : dict of id replacement functions + units : flux units - cobra_metabolite: a cobra.Metabolite object - - note_start_tag: string - the start tag for parsing cobra notes. this will eventually - be supplanted when COBRA is worked into sbml. + Returns + ------- + Id of bound parameter. + """ + value = getattr(reaction, bound_type) + if value == config.lower_bound: + return LOWER_BOUND_ID + elif value == 0: + return ZERO_BOUND_ID + elif value == config.upper_bound: + return UPPER_BOUND_ID + elif value == -float("Inf"): + return BOUND_MINUS_INF + elif value == float("Inf"): + return BOUND_PLUS_INF + else: + # new parameter + rid = reaction.id + if f_replace and F_REACTION_REV in f_replace: + rid = f_replace[F_REACTION_REV](rid) + pid = rid + "_" + bound_type + _create_parameter(model, pid=pid, value=value, sbo=SBO_FLUX_BOUND, + units=units, flux_udef=flux_udef) + return pid + + +def _create_parameter(model, pid, value, sbo=None, constant=True, units=None, + flux_udef=None): + """Create parameter in SBML model.""" + parameter = model.createParameter() # type: libsbml.Parameter + parameter.setId(pid) + parameter.setValue(value) + parameter.setConstant(constant) + if sbo: + parameter.setSBOTerm(sbo) + if units: + parameter.setUnits(flux_udef.getId()) + + +def _check_required(sbase, value, attribute): + """Get required attribute from the SBase. - note_end_tag: string - the end tag for parsing cobra notes. this will eventually - be supplanted when COBRA is worked into sbml. - boundary_metabolite: bool - if metabolite boundary condition should be set or not + Parameters + ---------- + sbase : libsbml.SBase + value : existing value + attribute: name of attribute Returns ------- - string: the created metabolite identifier + attribute value (or value if already set) """ - sbml_species = sbml_model.createSpecies() - the_id = 'M_' + cobra_metabolite.id.replace('-', '__') - # Deal with legacy naming issues - the_compartment = cobra_metabolite.compartment - if the_id.endswith('[%s]' % the_compartment): - the_id = the_id[:-len('[%s]' % the_compartment)] - elif not the_id.endswith('_%s' % the_compartment): - the_id += '_%s' % the_compartment - if boundary_metabolite: - the_id += '_boundary' - sbml_species.setId(the_id) - metabolite_id = the_id - if boundary_metabolite: - sbml_species.setBoundaryCondition(True) - if cobra_metabolite.name: - sbml_species.setName(cobra_metabolite.name) - else: - sbml_species.setName(cobra_metabolite.id) - if the_compartment is not None: - try: - sbml_species.setCompartment(the_compartment) - except: - warn('metabolite failed: ' + the_id) - return cobra_metabolite - if cobra_metabolite.charge is not None: - sbml_species.setCharge(cobra_metabolite.charge) - # Deal with cases where the formula in the model is not an object but as a - # string - if cobra_metabolite.formula or cobra_metabolite.notes: - tmp_note = '' - if hasattr(cobra_metabolite.formula, 'id'): - tmp_note += '%sFORMULA: %s%s' % (note_start_tag, - cobra_metabolite.formula.id, - note_end_tag) + + if not value: + msg = "required attribute '%s' not found in '%s'" % \ + (attribute, sbase) + if hasattr(sbase, "getId") and sbase.getId(): + msg += " with id '%s'" % sbase.getId() + elif hasattr(sbase, "getName") and sbase.getName(): + msg += " with name '%s'" % sbase.getName() + raise CobraSBMLError(msg) + return value + + +def _check(value, message): + """ + Checks the libsbml return value and logs error messages. + + If 'value' is None, logs an error message constructed using + 'message' and then exits with status code 1. If 'value' is an integer, + it assumes it is a libSBML return status code. If the code value is + LIBSBML_OPERATION_SUCCESS, returns without further action; if it is not, + prints an error message constructed using 'message' along with text from + libSBML explaining the meaning of the code, and exits with status code 1. + + """ + if value is None: + LOGGER.error('Error: LibSBML returned a null value trying ' + 'to <' + message + '>.') + elif type(value) is int: + if value == libsbml.LIBSBML_OPERATION_SUCCESS: + return else: - tmp_note += '%sFORMULA: %s%s' % (note_start_tag, - cobra_metabolite.formula, - note_end_tag) - if hasattr(cobra_metabolite.notes, 'items'): - for the_id_type, the_id in cobra_metabolite.notes.items(): - if the_id_type.lower() == 'charge': - # Use of notes['CHARGE'] has been deprecated in favor of - # metabolite.charge - continue - if not isinstance(the_id_type, str): - the_id_type = str(the_id_type) - if hasattr(the_id, '__iter__') and len(the_id) == 1: - the_id = the_id[0] - if not isinstance(the_id, str): - the_id = str(the_id) - tmp_note += '%s%s: %s%s' % (note_start_tag, - the_id_type, - the_id, note_end_tag) - sbml_species.setNotes(tmp_note + '') - return metabolite_id - - -def fix_legacy_id(id, use_hyphens=False, fix_compartments=False): - id = id.replace('_DASH_', '__') - id = id.replace('_FSLASH_', '/') - id = id.replace('_BSLASH_', "\\") - id = id.replace('_LPAREN_', '(') - id = id.replace('_LSQBKT_', '[') - id = id.replace('_RSQBKT_', ']') - id = id.replace('_RPAREN_', ')') - id = id.replace('_COMMA_', ',') - id = id.replace('_PERIOD_', '.') - id = id.replace('_APOS_', "'") - id = id.replace('&', '&') - id = id.replace('<', '<') - id = id.replace('>', '>') - id = id.replace('"', '"') - if use_hyphens: - id = id.replace('__', '-') + LOGGER.error('Error encountered trying to <' + message + '>.') + LOGGER.error('LibSBML error code {}: {}'.format(str(value), + libsbml.OperationReturnValue_toString(value).strip())) + else: + return + + +# ----------------------------------------------------------------------------- +# Notes +# ----------------------------------------------------------------------------- +def _parse_notes_dict(sbase): + """ Creates dictionary of COBRA notes. + + Parameters + ---------- + sbase : libsbml.SBase + + Returns + ------- + dict of notes + """ + notes = sbase.getNotesString() + if notes and len(notes) > 0: + pattern = r"

\s*(\w+\s*\w*)\s*:\s*([\w|\s]+)<" + matches = re.findall(pattern, notes) + d = {k.strip(): v.strip() for (k, v) in matches} + return {k: v for k, v in d.items() if len(v) > 0} else: - id = id.replace("-", "__") - if fix_compartments: - if len(id) > 2: - if (id[-3] == "(" and id[-1] == ")") or \ - (id[-3] == "[" and id[-1] == "]"): - id = id[:-3] + "_" + id[-2] - return id - - -def read_legacy_sbml(filename, use_hyphens=False): - """read in an sbml file and fix the sbml id's""" - model = create_cobra_model_from_sbml_file(filename, old_sbml=True) - for metabolite in model.metabolites: - metabolite.id = fix_legacy_id(metabolite.id) - model.metabolites._generate_index() - for reaction in model.reactions: - reaction.id = fix_legacy_id(reaction.id) - if reaction.id.startswith("EX_") and reaction.id.endswith("(e)"): - reaction.id = reaction.id[:-3] + "_e" - model.reactions._generate_index() - # remove boundary metabolites (end in _b and only present in exchanges) - for metabolite in list(model.metabolites): - if not metabolite.id.endswith("_b"): + return {} + + +def _sbase_notes_dict(sbase, notes): + """Set SBase notes based on dictionary. + + Parameters + ---------- + sbase : libsbml.SBase + SBML object to set notes on + notes : notes object + notes information from cobra object + """ + if notes and len(notes) > 0: + tokens = [''] + \ + ["

{}: {}

".format(k, v) for (k, v) in notes.items()] + \ + [""] + _check( + sbase.setNotes("\n".join(tokens)), + "Setting notes on sbase: {}".format(sbase) + ) + + +# ----------------------------------------------------------------------------- +# Annotations +# ----------------------------------------------------------------------------- +""" +cobra annotations will be dictionaries of the form: + object.annotation = { + 'provider' : [(qualifier, entity), ...] + } +A concrete example for a metabolite would look like the following + metabolite.annotation = { + 'chebi': [(isVersionOf, "CHEBI:17234), (is, "CHEBI:4167),], + 'kegg.compound': [(is, "C00031")] + } +The providers are hereby MIRIAM registry keys for collections +https://www.ebi.ac.uk/miriam/main/collections +The qualifiers are biomodel qualifiers +https://co.mbine.org/standards/qualifiers + +In the current stage the new annotation format is not completely supported yet. +""" +URL_IDENTIFIERS_PATTERN = r"^http[s]{0,1}://identifiers.org/(.+)/(.+)" +URL_IDENTIFIERS_PREFIX = r"https://identifiers.org" +QUALIFIER_TYPES = { + "is": libsbml.BQB_IS, + "hasPart": libsbml.BQB_HAS_PART, + "isPartOf": libsbml.BQB_IS_PART_OF, + "isVersionOf": libsbml.BQB_IS_VERSION_OF, + "hasVersion": libsbml.BQB_HAS_VERSION, + "isHomologTo": libsbml.BQB_IS_HOMOLOG_TO, + "isDescribedBy": libsbml.BQB_IS_DESCRIBED_BY, + "isEncodedBy": libsbml.BQB_IS_ENCODED_BY, + "encodes": libsbml.BQB_ENCODES, + "occursIn": libsbml.BQB_OCCURS_IN, + "hasProperty": libsbml.BQB_HAS_PROPERTY, + "isPropertyOf": libsbml.BQB_IS_PROPERTY_OF, + "hasTaxon": libsbml.BQB_HAS_TAXON, + "unknown": libsbml.BQB_UNKNOWN, + "bqm_is": libsbml.BQM_IS, + "bqm_isDescribedBy": libsbml.BQM_IS_DESCRIBED_BY, + "bqm_isDerivedFrom": libsbml.BQM_IS_DERIVED_FROM, + "bqm_isInstanceOf": libsbml.BQM_IS_INSTANCE_OF, + "bqm_hasInstance": libsbml.BQM_HAS_INSTANCE, + "bqm_unknown": libsbml.BQM_UNKNOWN, +} + + +def _parse_annotations(sbase): + """Parses cobra annotations from a given SBase object. + + Annotations are dictionaries with the providers as keys. + + Parameters + ---------- + sbase : libsbml.SBase + SBase from which the SBML annotations are read + + Returns + ------- + dict (annotation dictionary) + + FIXME: annotation format must be updated + (https://github.com/opencobra/cobrapy/issues/684) + """ + annotation = {} + + # SBO term + if sbase.isSetSBOTerm(): + # FIXME: correct handling of annotations + annotation["sbo"] = sbase.getSBOTermID() + + # RDF annotation + cvterms = sbase.getCVTerms() + if cvterms is None: + return annotation + + for cvterm in cvterms: # type: libsbml.CVTerm + for k in range(cvterm.getNumResources()): + uri = cvterm.getResourceURI(k) + + # FIXME: read and store the qualifier + tokens = uri.split('/') + if len(tokens) != 5 or not tokens[2] == "identifiers.org": + LOGGER.warning("%s does not conform to " + "http(s)://identifiers.org/collection/id", uri) + continue + + provider, identifier = tokens[3], tokens[4] + if provider in annotation: + if isinstance(annotation[provider], string_types): + annotation[provider] = [annotation[provider]] + annotation[provider].append(identifier) + else: + # FIXME: always in list + annotation[provider] = identifier + + return annotation + + +def _sbase_annotations(sbase, annotation): + """Set SBase annotations based on cobra annotations. + + Parameters + ---------- + sbase : libsbml.SBase + SBML object to annotate + annotation : cobra annotation structure + cobra object with annotation information + + FIXME: annotation format must be updated + (https://github.com/opencobra/cobrapy/issues/684) + """ + + if not annotation or len(annotation) == 0: + return + + # standardize annotations + annotation_data = deepcopy(annotation) + for key, value in annotation_data.items(): + if isinstance(value, string_types): + annotation_data[key] = [("is", value)] + + for key, value in annotation_data.items(): + for idx, item in enumerate(value): + if isinstance(item, string_types): + value[idx] = ("is", item) + + # set metaId + meta_id = "meta_{}".format(sbase.getId()) + sbase.setMetaId(meta_id) + + # rdf_items = [] + for provider, data in iteritems(annotation_data): + + # set SBOTerm + if provider in ["SBO", "sbo"]: + if provider == "SBO": + logging.warning("'SBO' provider is deprecated, " + "use 'sbo' provider instead") + sbo_term = data[0][1] + _check(sbase.setSBOTerm(sbo_term), + "Setting SBOTerm: {}".format(sbo_term)) + + # FIXME: sbo should also be written as CVTerm continue - if len(metabolite._reaction) == 1: - if list(metabolite._reaction)[0].id.startswith("EX_"): - metabolite.remove_from_model() - model.metabolites._generate_index() - return model + + for item in data: + qualifier_str, entity = item[0], item[1] + qualifier = QUALIFIER_TYPES.get(qualifier_str, None) + if qualifier is None: + qualifier = libsbml.BQB_IS + LOGGER.error("Qualifier type is not supported on " + "annotation: '{}'".format(qualifier_str)) + + qualifier_type = libsbml.BIOLOGICAL_QUALIFIER + if qualifier_str.startswith("bqm_"): + qualifier_type = libsbml.MODEL_QUALIFIER + + cv = libsbml.CVTerm() # type: libsbml.CVTerm + cv.setQualifierType(qualifier_type) + if qualifier_type == libsbml.BIOLOGICAL_QUALIFIER: + cv.setBiologicalQualifierType(qualifier) + elif qualifier_type == libsbml.MODEL_QUALIFIER: + cv.setModelQualifierType(qualifier) + else: + raise CobraSBMLError('Unsupported qualifier: ' + '%s' % qualifier) + resource = "%s/%s/%s" % (URL_IDENTIFIERS_PREFIX, provider, entity) + cv.addResource(resource) + _check(sbase.addCVTerm(cv), + "Setting cvterm: {}, resource: {}".format(cv, resource)) + + +# ----------------------------------------------------------------------------- +# Validation +# ----------------------------------------------------------------------------- +def validate_sbml_model(filename, + check_model=True, + internal_consistency=True, + check_units_consistency=False, + check_modeling_practice=False): + """Validate SBML model and returns the model along with a list of errors. + + Parameters + ---------- + filename : str + The filename (or SBML string) of the SBML model to be validated. + internal_consistency: boolean {True, False} + Check internal consistency. + check_units_consistency: boolean {True, False} + Check consistency of units. + check_modeling_practice: boolean {True, False} + Check modeling practise. + check_model: boolean {True, False} + Whether to also check some basic model properties such as reaction + boundaries and compartment formulas. + + Returns + ------- + (model, errors) + model : :class:`~cobra.core.Model.Model` object + The cobra model if the file could be read successfully or None + otherwise. + errors : dict + Warnings and errors grouped by their respective types. + + Raises + ------ + CobraSBMLError + """ + # store errors + errors = {key: [] for key in ("validator", "warnings", "other", + "SBML errors")} + + for key in ["SBML_FATAL", "SBML ERROR", "SBML_SCHEMA_ERROR", + "SBML_WARNING"]: + errors[key] = [] + + # make sure there is exactly one model + doc = _get_doc_from_filename(filename) + model = doc.getModel() # type: libsbml.Model + if model is None: + raise CobraSBMLError("No SBML model detected in file.") + + # set checking of units & modeling practise + doc.setConsistencyChecks(libsbml.LIBSBML_CAT_UNITS_CONSISTENCY, + check_units_consistency) + doc.setConsistencyChecks(libsbml.LIBSBML_CAT_MODELING_PRACTICE, + check_modeling_practice) + + # check internal consistency + if internal_consistency: + doc.checkInternalConsistency() + doc.checkConsistency() + + for k in range(doc.getNumErrors()): + e = doc.getError(k) + msg = _error_string(e, k=k) + sev = e.getSeverity() + if sev == libsbml.LIBSBML_SEV_FATAL: + errors["SBML_FATAL"].append(msg) + elif sev == libsbml.LIBSBML_SEV_ERROR: + errors["SBML_ERROR"].append(msg) + elif sev == libsbml.LIBSBML_SEV_SCHEMA_ERROR: + errors["SBML_SCHEMA_ERROR"].append(msg) + elif sev == libsbml.LIBSBML_SEV_WARNING: + errors["SBML_WARNING"].append(msg) + + # ensure can be made into model + # all warnings generated while loading will be logged as errors + with catch_warnings(record=True) as warning_list: + simplefilter("always") + try: + model = _sbml_to_model(doc) + except CobraSBMLError as e: + errors["SBML errors"].append(str(e)) + return None, errors + except Exception as e: + errors["other"].append(str(e)) + return None, errors + + errors["warnings"].extend(str(i.message) for i in warning_list) + + if check_model: + errors["validator"].extend(check_metabolite_compartment_formula(model)) + + return model, errors + + +def _error_string(error, k=None): + """String representation of SBMLError. + + Parameters + ---------- + error : libsbml.SBMLError + k : index of error + + Returns + ------- + string representation of error + """ + package = error.getPackage() + if package == '': + package = 'core' + + error_str = '{}\n' \ + 'E{}: {} ({}, L{}, {})\n' \ + '{}\n' \ + '[{}] {}\n' \ + '{}'.format( + '-' * 60, + k, error.getCategoryAsString(), package, error.getLine(), + 'code', + '-' * 60, + error.getSeverityAsString(), error.getShortMessage(), + error.getMessage()) + return error_str diff --git a/cobra/io/sbml3.py b/cobra/io/sbml3.py deleted file mode 100644 index 3433048ed..000000000 --- a/cobra/io/sbml3.py +++ /dev/null @@ -1,759 +0,0 @@ -# -*- coding: utf-8 -*- - -from __future__ import absolute_import - -import re -from ast import And, BoolOp, Name, Or -from bz2 import BZ2File -from collections import defaultdict -from decimal import Decimal -from gzip import GzipFile -from tempfile import NamedTemporaryFile -from warnings import catch_warnings, simplefilter, warn - -from six import iteritems, string_types - -from cobra.core import Configuration, Gene, Metabolite, Model, Reaction -from cobra.core.gene import parse_gpr -from cobra.manipulation.modify import _renames -from cobra.manipulation.validate import check_metabolite_compartment_formula -from cobra.util.solver import set_objective - - -try: - from lxml.etree import ( - parse, Element, SubElement, ElementTree, register_namespace, - ParseError, XPath) - - _with_lxml = True -except ImportError: - _with_lxml = False - try: - from xml.etree.cElementTree import ( - parse, Element, SubElement, ElementTree, register_namespace, - ParseError) - except ImportError: - XPath = None - from xml.etree.ElementTree import ( - parse, Element, SubElement, ElementTree, register_namespace, - ParseError) - -# use sbml level 2 from sbml.py (which uses libsbml). Eventually, it would -# be nice to use the libSBML converters directly instead. -try: - import libsbml -except ImportError: - libsbml = None -else: - from cobra.io.sbml import create_cobra_model_from_sbml_file as read_sbml2 - from cobra.io.sbml import write_cobra_model_to_sbml_file as write_sbml2 - -try: - from optlang.symbolics import Basic -except ImportError: - class Basic: - pass - - -CONFIGURATION = Configuration() - -# deal with namespaces -namespaces = {"fbc": "http://www.sbml.org/sbml/level3/version1/fbc/version2", - "sbml": "http://www.sbml.org/sbml/level3/version1/core", - "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#", - "bqbiol": "http://biomodels.net/biology-qualifiers/"} -for key in namespaces: - register_namespace(key, namespaces[key]) - - -def ns(query): - """replace prefixes with namespace""" - for prefix, uri in iteritems(namespaces): - query = query.replace(prefix + ":", "{" + uri + "}") - return query - - -# XPATH query wrappers -fbc_prefix = "{" + namespaces["fbc"] + "}" -sbml_prefix = "{" + namespaces["sbml"] + "}" - -SBML_DOT = "__SBML_DOT__" -# FBC TAGS -OR_TAG = ns("fbc:or") -AND_TAG = ns("fbc:and") -GENEREF_TAG = ns("fbc:geneProductRef") -GPR_TAG = ns("fbc:geneProductAssociation") -GENELIST_TAG = ns("fbc:listOfGeneProducts") -GENE_TAG = ns("fbc:geneProduct") -# XPATHS -BOUND_XPATH = ns("sbml:listOfParameters/sbml:parameter[@value]") -COMPARTMENT_XPATH = ns("sbml:listOfCompartments/sbml:compartment") -GENES_XPATH = GENELIST_TAG + "/" + GENE_TAG -SPECIES_XPATH = ns("sbml:listOfSpecies/sbml:species[@boundaryCondition='%s']") -OBJECTIVES_XPATH = ns("fbc:objective[@fbc:id='%s']/" - "fbc:listOfFluxObjectives/" - "fbc:fluxObjective") -LONG_SHORT_DIRECTION = {'maximize': 'max', 'minimize': 'min'} -SHORT_LONG_DIRECTION = {'min': 'minimize', 'max': 'maximize'} - -if _with_lxml: - RDF_ANNOTATION_XPATH = ("sbml:annotation/rdf:RDF/" - "rdf:Description[@rdf:about=$metaid]/" - "*[self::bqbiol:isEncodedBy or self::bqbiol:is]/" - "rdf:Bag/rdf:li/@rdf:resource") - extract_rdf_annotation = XPath(RDF_ANNOTATION_XPATH, namespaces=namespaces, - smart_strings=False) -else: - RDF_ANNOTATION_XPATH = ns("sbml:annotation/rdf:RDF/" - "rdf:Description[@rdf:about='%s']/" - "bqbiol:isEncodedBy/" - "rdf:Bag/rdf:li[@rdf:resource]") - - def extract_rdf_annotation(sbml_element, metaid): - search_xpath = RDF_ANNOTATION_XPATH % metaid - for i in sbml_element.iterfind(search_xpath): - yield get_attrib(i, "rdf:resource") - for i in sbml_element.iterfind(search_xpath.replace( - "isEncodedBy", "is")): - yield get_attrib(i, "rdf:resource") - - -class CobraSBMLError(Exception): - pass - - -def get_attrib(tag, attribute, type=lambda x: x, require=False): - value = tag.get(ns(attribute)) - if require and value is None: - msg = "required attribute '%s' not found in tag '%s'" % \ - (attribute, tag.tag) - if tag.get("id") is not None: - msg += " with id '%s'" % tag.get("id") - elif tag.get("name") is not None: - msg += " with name '%s'" % tag.get("name") - raise CobraSBMLError(msg) - return type(value) if value is not None else None - - -def set_attrib(xml, attribute_name, value): - if value is None or value == "": - return - xml.set(ns(attribute_name), str(value)) - - -def parse_stream(filename): - """parses filename or compressed stream to xml""" - try: - if hasattr(filename, "read"): - return parse(filename) - elif filename.endswith(".gz"): - with GzipFile(filename) as infile: - return parse(infile) - elif filename.endswith(".bz2"): - with BZ2File(filename) as infile: - return parse(infile) - else: - return parse(filename) - except ParseError as e: - raise CobraSBMLError("Malformed XML file: " + str(e)) - - -# string utility functions -def clip(string, prefix): - """clips a prefix from the beginning of a string if it exists - - >>> clip("R_pgi", "R_") - "pgi" - - """ - return string[len(prefix):] if string.startswith(prefix) else string - - -def strnum(number): - """Utility function to convert a number to a string""" - if isinstance(number, (Decimal, Basic, str)): - return str(number) - s = "%.15g" % number - return s.rstrip(".") - - -def construct_gpr_xml(parent, expression): - """create gpr xml under parent node""" - if isinstance(expression, BoolOp): - op = expression.op - if isinstance(op, And): - new_parent = SubElement(parent, AND_TAG) - elif isinstance(op, Or): - new_parent = SubElement(parent, OR_TAG) - else: - raise Exception("unsupported operation " + op.__class__) - for arg in expression.values: - construct_gpr_xml(new_parent, arg) - elif isinstance(expression, Name): - gene_elem = SubElement(parent, GENEREF_TAG) - set_attrib(gene_elem, "fbc:geneProduct", "G_" + expression.id) - else: - raise Exception("unsupported operation " + repr(expression)) - - -def annotate_cobra_from_sbml(cobra_element, sbml_element): - sbo_term = sbml_element.get("sboTerm") - if sbo_term is not None: - cobra_element.annotation["sbo"] = sbo_term - meta_id = get_attrib(sbml_element, "metaid") - if meta_id is None: - return - annotation = cobra_element.annotation - for uri in extract_rdf_annotation(sbml_element, metaid="#" + meta_id): - if not uri.startswith("http://identifiers.org/"): - warn("%s does not start with http://identifiers.org/" % uri) - continue - try: - provider, identifier = uri[23:].split("/", 1) - except ValueError: - warn("%s does not conform to http://identifiers.org/provider/id" - % uri) - continue - # handle multiple id's in the same database - if provider in annotation: - # make into a list if necessary - if isinstance(annotation[provider], string_types): - annotation[provider] = [annotation[provider]] - annotation[provider].append(identifier) - else: - cobra_element.annotation[provider] = identifier - - -def annotate_sbml_from_cobra(sbml_element, cobra_element): - if len(cobra_element.annotation) == 0: - return - # get the id so we can set the metaid - tag = sbml_element.tag - if tag.startswith(sbml_prefix) or tag[0] != "{": - prefix = "" - elif tag.startswith(fbc_prefix): - prefix = fbc_prefix - else: - raise ValueError("Can not annotate " + repr(sbml_element)) - id = sbml_element.get(prefix + "id") - if len(id) == 0: - raise ValueError("%s does not have id set" % repr(sbml_element)) - set_attrib(sbml_element, "metaid", id) - annotation = SubElement(sbml_element, ns("sbml:annotation")) - rdf_desc = SubElement(SubElement(annotation, ns("rdf:RDF")), - ns("rdf:Description")) - set_attrib(rdf_desc, "rdf:about", "#" + id) - bag = SubElement(SubElement(rdf_desc, ns("bqbiol:is")), - ns("rdf:Bag")) - for provider, identifiers in sorted(iteritems(cobra_element.annotation)): - if provider == "sbo": - set_attrib(sbml_element, "sboTerm", identifiers) - continue - if isinstance(identifiers, string_types): - identifiers = (identifiers,) - for identifier in identifiers: - li = SubElement(bag, ns("rdf:li")) - set_attrib(li, "rdf:resource", "http://identifiers.org/%s/%s" % - (provider, identifier)) - - -def parse_xml_into_model(xml, number=float): - xml_model = xml.find(ns("sbml:model")) - if get_attrib(xml_model, "fbc:strict") != "true": - warn('loading SBML model without fbc:strict="true"') - - model_id = get_attrib(xml_model, "id") - model = Model(model_id) - model.name = xml_model.get("name") - - model.compartments = {c.get("id"): c.get("name") for c in - xml_model.findall(COMPARTMENT_XPATH)} - # add metabolites - for species in xml_model.findall(SPECIES_XPATH % 'false'): - met = get_attrib(species, "id", require=True) - met = Metabolite(clip(met, "M_")) - met.name = species.get("name") - annotate_cobra_from_sbml(met, species) - met.compartment = species.get("compartment") - met.charge = get_attrib(species, "fbc:charge", int) - met.formula = get_attrib(species, "fbc:chemicalFormula") - model.add_metabolites([met]) - # Detect boundary metabolites - In case they have been mistakenly - # added. They should not actually appear in a model - boundary_metabolites = {clip(i.get("id"), "M_") - for i in xml_model.findall(SPECIES_XPATH % 'true')} - - # add genes - for sbml_gene in xml_model.iterfind(GENES_XPATH): - gene_id = get_attrib(sbml_gene, "fbc:id").replace(SBML_DOT, ".") - gene = Gene(clip(gene_id, "G_")) - gene.name = get_attrib(sbml_gene, "fbc:name") - if gene.name is None: - gene.name = get_attrib(sbml_gene, "fbc:label") - annotate_cobra_from_sbml(gene, sbml_gene) - model.genes.append(gene) - - def process_gpr(sub_xml): - """recursively convert gpr xml to a gpr string""" - if sub_xml.tag == OR_TAG: - return "( " + ' or '.join(process_gpr(i) for i in sub_xml) + " )" - elif sub_xml.tag == AND_TAG: - return "( " + ' and '.join(process_gpr(i) for i in sub_xml) + " )" - elif sub_xml.tag == GENEREF_TAG: - gene_id = get_attrib(sub_xml, "fbc:geneProduct", require=True) - return clip(gene_id, "G_") - else: - raise Exception("unsupported tag " + sub_xml.tag) - - bounds = {bound.get("id"): get_attrib(bound, "value", type=number) - for bound in xml_model.iterfind(BOUND_XPATH)} - # add reactions - reactions = [] - for sbml_reaction in xml_model.iterfind( - ns("sbml:listOfReactions/sbml:reaction")): - reaction = get_attrib(sbml_reaction, "id", require=True) - reaction = Reaction(clip(reaction, "R_")) - reaction.name = sbml_reaction.get("name") - annotate_cobra_from_sbml(reaction, sbml_reaction) - lb_id = get_attrib(sbml_reaction, "fbc:lowerFluxBound", require=True) - ub_id = get_attrib(sbml_reaction, "fbc:upperFluxBound", require=True) - try: - reaction.upper_bound = bounds[ub_id] - reaction.lower_bound = bounds[lb_id] - except KeyError as e: - raise CobraSBMLError("No constant bound with id '%s'" % str(e)) - reactions.append(reaction) - - stoichiometry = defaultdict(lambda: 0) - for species_reference in sbml_reaction.findall( - ns("sbml:listOfReactants/sbml:speciesReference")): - met_name = clip(species_reference.get("species"), "M_") - stoichiometry[met_name] -= \ - number(species_reference.get("stoichiometry")) - for species_reference in sbml_reaction.findall( - ns("sbml:listOfProducts/sbml:speciesReference")): - met_name = clip(species_reference.get("species"), "M_") - stoichiometry[met_name] += \ - get_attrib(species_reference, "stoichiometry", - type=number, require=True) - # needs to have keys of metabolite objects, not ids - object_stoichiometry = {} - for met_id in stoichiometry: - if met_id in boundary_metabolites: - warn("Boundary metabolite '%s' used in reaction '%s'" % - (met_id, reaction.id)) - continue - try: - metabolite = model.metabolites.get_by_id(met_id) - except KeyError: - warn("ignoring unknown metabolite '%s' in reaction %s" % - (met_id, reaction.id)) - continue - object_stoichiometry[metabolite] = stoichiometry[met_id] - reaction.add_metabolites(object_stoichiometry) - # set gene reaction rule - gpr_xml = sbml_reaction.find(GPR_TAG) - if gpr_xml is not None and len(gpr_xml) != 1: - warn("ignoring invalid geneAssociation for " + repr(reaction)) - gpr_xml = None - gpr = process_gpr(gpr_xml[0]) if gpr_xml is not None else '' - # remove outside parenthesis, if any - if gpr.startswith("(") and gpr.endswith(")"): - gpr = gpr[1:-1].strip() - gpr = gpr.replace(SBML_DOT, ".") - reaction.gene_reaction_rule = gpr - try: - model.add_reactions(reactions) - except ValueError as e: - warn(str(e)) - - # objective coefficients are handled after all reactions are added - obj_list = xml_model.find(ns("fbc:listOfObjectives")) - if obj_list is None: - warn("listOfObjectives element not found") - return model - target_objective_id = get_attrib(obj_list, "fbc:activeObjective") - target_objective = obj_list.find( - ns("fbc:objective[@fbc:id='{}']".format(target_objective_id))) - obj_direction_long = get_attrib(target_objective, "fbc:type") - obj_direction = LONG_SHORT_DIRECTION[obj_direction_long] - - obj_query = OBJECTIVES_XPATH % target_objective_id - coefficients = {} - for sbml_objective in obj_list.findall(obj_query): - rxn_id = clip(get_attrib(sbml_objective, "fbc:reaction"), "R_") - try: - objective_reaction = model.reactions.get_by_id(rxn_id) - except KeyError: - raise CobraSBMLError("Objective reaction '%s' not found" % rxn_id) - try: - coefficients[objective_reaction] = get_attrib( - sbml_objective, "fbc:coefficient", type=number) - except ValueError as e: - warn(str(e)) - set_objective(model, coefficients) - model.solver.objective.direction = obj_direction - return model - - -def model_to_xml(cobra_model, units=True): - xml = Element("sbml", xmlns=namespaces["sbml"], level="3", version="1", - sboTerm="SBO:0000624") - set_attrib(xml, "fbc:required", "false") - xml_model = SubElement(xml, "model") - set_attrib(xml_model, "fbc:strict", "true") - if cobra_model.id is not None: - xml_model.set("id", cobra_model.id) - if cobra_model.name is not None: - xml_model.set("name", cobra_model.name) - - # if using units, add in mmol/gdw/hr - if units: - unit_def = SubElement( - SubElement(xml_model, "listOfUnitDefinitions"), - "unitDefinition", id="mmol_per_gDW_per_hr") - list_of_units = SubElement(unit_def, "listOfUnits") - SubElement(list_of_units, "unit", kind="mole", scale="-3", - multiplier="1", exponent="1") - SubElement(list_of_units, "unit", kind="gram", scale="0", - multiplier="1", exponent="-1") - SubElement(list_of_units, "unit", kind="second", scale="0", - multiplier="3600", exponent="-1") - - # create the element for the flux objective - obj_list_tmp = SubElement(xml_model, ns("fbc:listOfObjectives")) - set_attrib(obj_list_tmp, "fbc:activeObjective", "obj") - obj_list_tmp = SubElement(obj_list_tmp, ns("fbc:objective")) - set_attrib(obj_list_tmp, "fbc:id", "obj") - set_attrib(obj_list_tmp, "fbc:type", - SHORT_LONG_DIRECTION[cobra_model.objective.direction]) - flux_objectives_list = SubElement(obj_list_tmp, - ns("fbc:listOfFluxObjectives")) - - # create the element for the flux bound parameters - parameter_list = SubElement(xml_model, "listOfParameters") - param_attr = {"constant": "true"} - if units: - param_attr["units"] = "mmol_per_gDW_per_hr" - # the most common bounds are the minimum, maximum, and 0 - if len(cobra_model.reactions) > 0: - min_value = min(cobra_model.reactions.list_attr("lower_bound")) - max_value = max(cobra_model.reactions.list_attr("upper_bound")) - else: - min_value = CONFIGURATION.lower_bound - max_value = CONFIGURATION.upper_bound - - SubElement(parameter_list, "parameter", value=strnum(min_value), - id="cobra_default_lb", sboTerm="SBO:0000626", **param_attr) - SubElement(parameter_list, "parameter", value=strnum(max_value), - id="cobra_default_ub", sboTerm="SBO:0000626", **param_attr) - SubElement(parameter_list, "parameter", value="0", - id="cobra_0_bound", sboTerm="SBO:0000626", **param_attr) - - def create_bound(reaction, bound_type): - """returns the str id of the appropriate bound for the reaction - - The bound will also be created if necessary""" - value = getattr(reaction, bound_type) - if value == min_value: - return "cobra_default_lb" - elif value == 0: - return "cobra_0_bound" - elif value == max_value: - return "cobra_default_ub" - else: - param_id = "R_" + reaction.id + "_" + bound_type - SubElement(parameter_list, "parameter", id=param_id, - value=strnum(value), sboTerm="SBO:0000625", - **param_attr) - return param_id - - # add in compartments - compartments_list = SubElement(xml_model, "listOfCompartments") - compartments = cobra_model.compartments - for compartment, name in iteritems(compartments): - SubElement(compartments_list, "compartment", id=compartment, name=name, - constant="true") - - # add in metabolites - species_list = SubElement(xml_model, "listOfSpecies") - for met in cobra_model.metabolites: - species = SubElement(species_list, "species", - id="M_" + met.id, - # Useless required SBML parameters - constant="false", - boundaryCondition="false", - hasOnlySubstanceUnits="false") - set_attrib(species, "name", met.name) - annotate_sbml_from_cobra(species, met) - set_attrib(species, "compartment", met.compartment) - set_attrib(species, "fbc:charge", met.charge) - set_attrib(species, "fbc:chemicalFormula", met.formula) - - # add in genes - if len(cobra_model.genes) > 0: - genes_list = SubElement(xml_model, GENELIST_TAG) - for gene in cobra_model.genes: - gene_id = gene.id.replace(".", SBML_DOT) - sbml_gene = SubElement(genes_list, GENE_TAG) - set_attrib(sbml_gene, "fbc:id", "G_" + gene_id) - name = gene.name - if name is None or len(name) == 0: - name = gene.id - set_attrib(sbml_gene, "fbc:label", gene_id) - set_attrib(sbml_gene, "fbc:name", name) - annotate_sbml_from_cobra(sbml_gene, gene) - - # add in reactions - reactions_list = SubElement(xml_model, "listOfReactions") - for reaction in cobra_model.reactions: - id = "R_" + reaction.id - sbml_reaction = SubElement( - reactions_list, "reaction", - id=id, - # Useless required SBML parameters - fast="false", - reversible=str(reaction.lower_bound < 0).lower()) - set_attrib(sbml_reaction, "name", reaction.name) - annotate_sbml_from_cobra(sbml_reaction, reaction) - # add in bounds - set_attrib(sbml_reaction, "fbc:upperFluxBound", - create_bound(reaction, "upper_bound")) - set_attrib(sbml_reaction, "fbc:lowerFluxBound", - create_bound(reaction, "lower_bound")) - - # objective coefficient - if reaction.objective_coefficient != 0: - objective = SubElement(flux_objectives_list, - ns("fbc:fluxObjective")) - set_attrib(objective, "fbc:reaction", id) - set_attrib(objective, "fbc:coefficient", - strnum(reaction.objective_coefficient)) - - # stoichiometry - reactants = {} - products = {} - for metabolite, stoichiomety in iteritems(reaction._metabolites): - met_id = "M_" + metabolite.id - if stoichiomety > 0: - products[met_id] = strnum(stoichiomety) - else: - reactants[met_id] = strnum(-stoichiomety) - if len(reactants) > 0: - reactant_list = SubElement(sbml_reaction, "listOfReactants") - for met_id, stoichiomety in sorted(iteritems(reactants)): - SubElement(reactant_list, "speciesReference", species=met_id, - stoichiometry=stoichiomety, constant="true") - if len(products) > 0: - product_list = SubElement(sbml_reaction, "listOfProducts") - for met_id, stoichiomety in sorted(iteritems(products)): - SubElement(product_list, "speciesReference", species=met_id, - stoichiometry=stoichiomety, constant="true") - - # gene reaction rule - gpr = reaction.gene_reaction_rule - if gpr is not None and len(gpr) > 0: - gpr = gpr.replace(".", SBML_DOT) - gpr_xml = SubElement(sbml_reaction, GPR_TAG) - try: - parsed, _ = parse_gpr(gpr) - construct_gpr_xml(gpr_xml, parsed.body) - except Exception as e: - print("failed on '%s' in %s" % - (reaction.gene_reaction_rule, repr(reaction))) - raise e - - return xml - - -def read_sbml_model(filename, number=float, **kwargs): - if not _with_lxml: - warn("Install lxml for faster SBML I/O", ImportWarning) - xmlfile = parse_stream(filename) - xml = xmlfile.getroot() - # use libsbml if not l3v1 with fbc v2 - use_libsbml = (xml.get("level") != "3" or xml.get("version") != "1" or - get_attrib(xml, "fbc:required") is None) - if use_libsbml: - if libsbml is None: - raise ImportError("libSBML required for fbc < 2") - # libsbml needs a file string, so write to temp file if a file handle - if hasattr(filename, "read"): - with NamedTemporaryFile(suffix=".xml", delete=False) as outfile: - xmlfile.write(outfile, encoding="UTF-8", xml_declaration=True) - filename = outfile.name - return read_sbml2(filename, **kwargs) - try: - return parse_xml_into_model(xml, number=number) - except Exception: - raise CobraSBMLError( - "Something went wrong reading the model. You can get a detailed " - "report using the `cobra.io.sbml3.validate_sbml_model` function " - "or using the online validator at http://sbml.org/validator") - - -id_required = {ns(i) for i in ("sbml:model", "sbml:reaction:", "sbml:species", - "fbc:geneProduct", "sbml:compartment", - "sbml:parameter", "sbml:UnitDefinition", - "fbc:objective")} -invalid_id_detector = re.compile("|".join(re.escape(i[0]) for i in _renames)) - - -def validate_sbml_model(filename, check_model=True): - """Returns the model along with a list of errors. - - Parameters - ---------- - filename : str - The filename of the SBML model to be validated. - check_model: bool, optional - Whether to also check some basic model properties such as reaction - boundaries and compartment formulas. - - Returns - ------- - model : :class:`~cobra.core.Model.Model` object - The cobra model if the file could be read succesfully or None - otherwise. - errors : dict - Warnings and errors grouped by their respective types. - - Raises - ------ - CobraSBMLError - If the file is not a valid SBML Level 3 file with FBC. - """ - xmlfile = parse_stream(filename) - xml = xmlfile.getroot() - # use libsbml if not l3v1 with fbc v2 - use_libsbml = (xml.get("level") != "3" or xml.get("version") != "1" or - get_attrib(xml, "fbc:required") is None) - if use_libsbml: - raise CobraSBMLError("XML is not SBML level 3 v1 with fbc v2") - - errors = {k: [] for k in ("validator", "warnings", "SBML errors", "other")} - - def err(err_msg, group="validator"): - errors[group].append(err_msg) - - # make sure there is exactly one model - xml_models = xml.findall(ns("sbml:model")) - if len(xml_models) == 0: - raise CobraSBMLError("No SBML model detected in file") - if len(xml_models) > 1: - err("More than 1 SBML model detected in file") - xml_model = xml_models[0] - - # make sure all sbml id's are valid - all_ids = set() - for element in xmlfile.iter(): - if element.tag.startswith(fbc_prefix): - prefix = fbc_prefix - elif element.tag.startswith(sbml_prefix): - prefix = "" - else: - continue - str_id = element.get(prefix + "id", None) - element_name = element.tag.split("}")[-1] - id_repr = "%s id '%s' " % (element_name, str_id) - if str_id is None or len(str_id) == 0: - if element.tag in id_required: - err(element_name + " missing id") - else: - if str_id in all_ids: - err("duplicate id for " + id_repr) - all_ids.add(str_id) - try: - str_id.encode("ascii") - except UnicodeEncodeError as e: - err("invalid character '%s' found in %s" % - (str_id[e.start:e.end], id_repr)) - if invalid_id_detector.search(str_id): - bad_chars = "".join(invalid_id_detector.findall(str_id)) - err("invalid character%s '%s' found in %s" % - ("s" if len(bad_chars) > 1 else "", bad_chars, id_repr)) - if not str_id[0].isalpha(): - err("%s does not start with alphabet character" % id_repr) - - # check SBO terms - for element in xml.findall(".//*[@sboTerm]"): - sbo_term = element.get("sboTerm") - if not sbo_term.startswith("SBO:"): - err("sboTerm '%s' does not begin with 'SBO:'" % sbo_term) - - # ensure can be made into model - # all warnings generated while loading will be logged as errors - with catch_warnings(record=True) as warning_list: - simplefilter("always") - try: - model = parse_xml_into_model(xml) - except CobraSBMLError as e: - err(str(e), "SBML errors") - return None, errors - except Exception as e: - err(str(e), "other") - return None, errors - errors["warnings"].extend(str(i.message) for i in warning_list) - - # check genes - xml_genes = { - get_attrib(i, "fbc:id").replace(SBML_DOT, ".") - for i in xml_model.iterfind(GENES_XPATH)} - for gene in model.genes: - if "G_" + gene.id not in xml_genes and gene.id not in xml_genes: - err("No gene specfied with id 'G_%s'" % gene.id) - - if check_model: - errors["validator"].extend(check_metabolite_compartment_formula(model)) - - return model, errors - - -def write_sbml_model(cobra_model, filename, use_fbc_package=True, **kwargs): - if not _with_lxml: - warn("Install lxml for faster SBML I/O", ImportWarning) - if not use_fbc_package: - if libsbml is None: - raise ImportError("libSBML required to write non-fbc models") - write_sbml2(cobra_model, filename, use_fbc_package=False, **kwargs) - return - # create xml - xml = model_to_xml(cobra_model, **kwargs) - write_args = {"encoding": "UTF-8", "xml_declaration": True} - if _with_lxml: - write_args["pretty_print"] = True - write_args["pretty_print"] = True - else: - indent_xml(xml) - # write xml to file - should_close = True - if hasattr(filename, "write"): - xmlfile = filename - should_close = False - elif filename.endswith(".gz"): - xmlfile = GzipFile(filename, "wb") - elif filename.endswith(".bz2"): - xmlfile = BZ2File(filename, "wb") - else: - xmlfile = open(filename, "wb") - ElementTree(xml).write(xmlfile, **write_args) - if should_close: - xmlfile.close() - - -# inspired by http://effbot.org/zone/element-lib.htm#prettyprint -def indent_xml(elem, level=0): - """indent xml for pretty printing""" - i = "\n" + level * " " - if len(elem): - if not elem.text or not elem.text.strip(): - elem.text = i + " " - if not elem.tail or not elem.tail.strip(): - elem.tail = i - for elem in elem: - indent_xml(elem, level + 1) - if not elem.tail or not elem.tail.strip(): - elem.tail = i - else: - if level and (not elem.tail or not elem.tail.strip()): - elem.tail = i diff --git a/cobra/manipulation/delete.py b/cobra/manipulation/delete.py index 6650aa3d7..de7d05453 100644 --- a/cobra/manipulation/delete.py +++ b/cobra/manipulation/delete.py @@ -230,4 +230,8 @@ def remove_genes(cobra_model, gene_list, remove_reactions=True): reaction.gene_reaction_rule = new_rule for gene in gene_set: cobra_model.genes.remove(gene) + # remove reference to the gene in all groups + associated_groups = cobra_model.get_associated_groups(gene) + for group in associated_groups: + group.remove_members(gene) cobra_model.remove_reactions(target_reactions) diff --git a/cobra/medium/boundary_types.py b/cobra/medium/boundary_types.py index 2cc3249bb..3535c1219 100644 --- a/cobra/medium/boundary_types.py +++ b/cobra/medium/boundary_types.py @@ -102,7 +102,11 @@ def is_boundary_type(reaction, boundary_type, external_compartment): on a heuristic. """ # Check if the reaction has an annotation. Annotations dominate everything. - sbo_term = reaction.annotation.get("sbo", "").upper() + sbo_term = reaction.annotation.get("sbo", "") + if isinstance(sbo_term, list): + sbo_term = sbo_term[0] + sbo_term = sbo_term.upper() + if sbo_term == sbo_terms[boundary_type]: return True if sbo_term in [sbo_terms[k] for k in sbo_terms if k != boundary_type]: diff --git a/cobra/test/data/e_coli_core.xml b/cobra/test/data/e_coli_core.xml new file mode 100644 index 000000000..a50651aae --- /dev/null +++ b/cobra/test/data/e_coli_core.xml @@ -0,0 +1,8293 @@ + + + + + +
+
+

+
e_coli_core - Escherichia coli str. K-12 substr. MG1655
+

+
+
+ +
+ + + +

Description

+
+

This is a metabolism model of Escherichia coli str. K-12 substr. MG1655 in + SBML format.

+
+
The content of this model has been carefully created in a manual research effort. This file has been exported from the software + COBRApy and further processed with the + JSBML-based + ModelPolisher application.
+
This file has been produced by the + Systems Biology Research Group using + BiGG Models knowledge-base version of Feb 24, 2018, where it is currently hosted and + identified by: + e_coli_core.
+

Terms of use

+
Copyright © 2017 The Regents of the University of California.
+
+

Redistribution and use of any part of this model from BiGG Models knowledge-base, with or without modification, are permitted provided that the following conditions are met: +

    +
  1. Redistributions of this SBML file must retain the above copyright notice, this list of conditions and the following disclaimer.
  2. +
  3. Redistributions in a different form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided + with the distribution.
  4. +
This model is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

+

For specific licensing terms about this particular model and regulations of commercial use, see + this model in BiGG Models knowledge-base.

+
+

References

When using content from BiGG Models knowledge-base in your research works, please cite +
+
King ZA, Lu JS, Dräger A, Miller PC, Federowicz S, Lerman JA, Ebrahim A, Palsson BO, and Lewis NE. (2015). +
BiGG Models: A platform for integrating, standardizing, and sharing genome-scale models. + Nucl Acids Res. + doi:10.1093/nar/gkv1049
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
diff --git a/cobra/test/data/fbc_ex1.xml b/cobra/test/data/fbc_ex1.xml new file mode 100644 index 000000000..539e3bf94 --- /dev/null +++ b/cobra/test/data/fbc_ex1.xml @@ -0,0 +1,197 @@ + + + + + + + + + + + Koenig + Matthias + + koenigmx@hu-berlin.de + + Humboldt-University Berlin, Institute for Theoretical Biology + + + + + + 2019-03-06T14:40:55Z + + + 2019-03-06T14:40:55Z + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/cobra/test/data/fbc_ex2.xml b/cobra/test/data/fbc_ex2.xml new file mode 100644 index 000000000..36f0461c4 --- /dev/null +++ b/cobra/test/data/fbc_ex2.xml @@ -0,0 +1,187 @@ + + + + + + + + + + + Koenig + Matthias + + koenigmx@hu-berlin.de + + Humboldt-University Berlin, Institute for Theoretical Biology + + + + + + 2019-03-06T14:40:55Z + + + 2019-03-06T14:40:55Z + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/cobra/test/data/iJO1366.pickle b/cobra/test/data/iJO1366.pickle index 2e89067fa..c08a8b3e8 100644 Binary files a/cobra/test/data/iJO1366.pickle and b/cobra/test/data/iJO1366.pickle differ diff --git a/cobra/test/data/mini.json b/cobra/test/data/mini.json index c75f18ace..c0266f904 100644 --- a/cobra/test/data/mini.json +++ b/cobra/test/data/mini.json @@ -1140,8 +1140,8 @@ }, { "annotation": { - "sbo": "SBO:0000627", - "bigg.reaction": "glc" + "bigg.reaction": "glc", + "sbo": "SBO:0000627" }, "gene_reaction_rule": "", "id": "EX_glc__D_e", @@ -1154,8 +1154,8 @@ }, { "annotation": { - "sbo": "SBO:0000627", - "bigg.reaction": "h" + "bigg.reaction": "h", + "sbo": "SBO:0000627" }, "gene_reaction_rule": "", "id": "EX_h_e", @@ -1364,4 +1364,4 @@ } ], "version": "1" -} +} \ No newline at end of file diff --git a/cobra/test/data/mini.mat b/cobra/test/data/mini.mat index 6726342d3..667f0d1a2 100644 Binary files a/cobra/test/data/mini.mat and b/cobra/test/data/mini.mat differ diff --git a/cobra/test/data/mini.pickle b/cobra/test/data/mini.pickle index af9ef9ccc..e13bb9318 100644 Binary files a/cobra/test/data/mini.pickle and b/cobra/test/data/mini.pickle differ diff --git a/cobra/test/data/mini_cobra.xml b/cobra/test/data/mini_cobra.xml index 48b02294b..6270767d7 100644 --- a/cobra/test/data/mini_cobra.xml +++ b/cobra/test/data/mini_cobra.xml @@ -176,21 +176,21 @@ - +

FORMULA: C6H12O6

- +

FORMULA: H

- +

FORMULA: C3H5O3

@@ -206,8 +206,8 @@ - + @@ -215,8 +215,8 @@ - + @@ -224,7 +224,7 @@ -

GENE_ASSOCIATION: b3603 or b2975

+

GENE ASSOCIATION: b3603 or b2975

@@ -232,8 +232,8 @@ - + @@ -241,8 +241,8 @@ - + @@ -250,15 +250,15 @@ -

GENE_ASSOCIATION: b2779

+

GENE ASSOCIATION: b2779

- + @@ -266,8 +266,8 @@ - + @@ -285,8 +285,8 @@ - + @@ -304,8 +304,8 @@ - + @@ -323,8 +323,8 @@ - + @@ -332,15 +332,15 @@ -

GENE_ASSOCIATION: b1773 or b2097 or b2925

+

GENE ASSOCIATION: b1773 or b2097 or b2925

- + @@ -348,8 +348,8 @@ - + @@ -357,18 +357,18 @@ -

GENE_ASSOCIATION: b1779

+

GENE ASSOCIATION: b1779

- + - + @@ -376,8 +376,8 @@ - + @@ -385,12 +385,12 @@ -

GENE_ASSOCIATION: ( b2417 and b1621 and b2415 and b2416 ) or ( b2417 and b1101 and b2415 and b2416 ) or ( b1817 and b1818 and b1819 and b2415 and b2416 )

+

GENE ASSOCIATION: ( b2417 and b1621 and b2415 and b2416 ) or ( b2417 and b1101 and b2415 and b2416 ) or ( b1817 and b1818 and b1819 and b2415 and b2416 )

- + @@ -402,8 +402,8 @@ - + @@ -411,7 +411,7 @@ -

GENE_ASSOCIATION: b0875 or s0001

+

GENE ASSOCIATION: b0875 or s0001

@@ -426,8 +426,8 @@ - + @@ -435,17 +435,17 @@ -

GENE_ASSOCIATION: b2133 or b1380

+

GENE ASSOCIATION: b2133 or b1380

- + - + @@ -453,8 +453,8 @@ - + @@ -462,12 +462,12 @@ -

GENE_ASSOCIATION: b3916 or b1723

+

GENE ASSOCIATION: b3916 or b1723

- + @@ -480,8 +480,8 @@ - + @@ -489,7 +489,7 @@ -

GENE_ASSOCIATION: b4025

+

GENE ASSOCIATION: b4025

@@ -504,8 +504,8 @@ - + @@ -513,12 +513,12 @@ -

GENE_ASSOCIATION: b2926

+

GENE ASSOCIATION: b2926

- + @@ -530,8 +530,8 @@ - + @@ -539,7 +539,7 @@ -

GENE_ASSOCIATION: b4395 or b3612 or b0755

+

GENE ASSOCIATION: b4395 or b3612 or b0755

@@ -554,8 +554,8 @@ - + @@ -563,7 +563,7 @@ -

GENE_ASSOCIATION: b2987 or b3493

+

GENE ASSOCIATION: b2987 or b3493

@@ -571,8 +571,8 @@ - + @@ -580,8 +580,8 @@ - + @@ -589,13 +589,13 @@ -

GENE_ASSOCIATION: b1854 or b1676

+

GENE ASSOCIATION: b1854 or b1676

- + @@ -607,8 +607,8 @@ - + @@ -616,7 +616,7 @@ -

GENE_ASSOCIATION: b3919

+

GENE ASSOCIATION: b3919

@@ -631,8 +631,8 @@ - + diff --git a/cobra/test/data/mini_fbc1.xml b/cobra/test/data/mini_fbc1.xml index 58fb4bc9c..02ae563bf 100644 --- a/cobra/test/data/mini_fbc1.xml +++ b/cobra/test/data/mini_fbc1.xml @@ -313,14 +313,14 @@ - +
-

GENE_ASSOCIATION: b3603 or b2975

+

GENE ASSOCIATION: b3603 or b2975

@@ -328,22 +328,22 @@ - +
-

GENE_ASSOCIATION: b2779

+

GENE ASSOCIATION: b2779

- +
@@ -373,43 +373,43 @@ -

GENE_ASSOCIATION: b1773 or b2097 or b2925

+

GENE ASSOCIATION: b1773 or b2097 or b2925

- +
-

GENE_ASSOCIATION: b1779

+

GENE ASSOCIATION: b1779

- + - +
-

GENE_ASSOCIATION: ( b2417 and b1621 and b2415 and b2416 ) or ( b2417 and b1101 and b2415 and b2416 ) or ( b1817 and b1818 and b1819 and b2415 and b2416 )

+

GENE ASSOCIATION: ( b2417 and b1621 and b2415 and b2416 ) or ( b2417 and b1101 and b2415 and b2416 ) or ( b1817 and b1818 and b1819 and b2415 and b2416 )

- + @@ -419,7 +419,7 @@ -

GENE_ASSOCIATION: b0875 or s0001

+

GENE ASSOCIATION: b0875 or s0001

@@ -432,28 +432,28 @@ -

GENE_ASSOCIATION: b2133 or b1380

+

GENE ASSOCIATION: b2133 or b1380

- + - +
-

GENE_ASSOCIATION: b3916 or b1723

+

GENE ASSOCIATION: b3916 or b1723

- + @@ -464,7 +464,7 @@ -

GENE_ASSOCIATION: b4025

+

GENE ASSOCIATION: b4025

@@ -477,12 +477,12 @@ -

GENE_ASSOCIATION: b2926

+

GENE ASSOCIATION: b2926

- + @@ -492,7 +492,7 @@ -

GENE_ASSOCIATION: b4395 or b3612 or b0755

+

GENE ASSOCIATION: b4395 or b3612 or b0755

@@ -505,7 +505,7 @@ -

GENE_ASSOCIATION: b2987 or b3493

+

GENE ASSOCIATION: b2987 or b3493

@@ -513,20 +513,20 @@ - +
-

GENE_ASSOCIATION: b1854 or b1676

+

GENE ASSOCIATION: b1854 or b1676

- + @@ -536,7 +536,7 @@ -

GENE_ASSOCIATION: b3919

+

GENE ASSOCIATION: b3919

diff --git a/cobra/test/data/mini_fbc2.xml.gz b/cobra/test/data/mini_fbc2.xml.gz index 16d383c00..3590bd001 100644 Binary files a/cobra/test/data/mini_fbc2.xml.gz and b/cobra/test/data/mini_fbc2.xml.gz differ diff --git a/cobra/test/data/raven.pickle b/cobra/test/data/raven.pickle index adfaf9b65..f9d55bdf3 100644 Binary files a/cobra/test/data/raven.pickle and b/cobra/test/data/raven.pickle differ diff --git a/cobra/test/data/salmonella.pickle b/cobra/test/data/salmonella.pickle index ba0568c82..a3eb5947e 100644 Binary files a/cobra/test/data/salmonella.pickle and b/cobra/test/data/salmonella.pickle differ diff --git a/cobra/test/data/textbook_fva.json b/cobra/test/data/textbook_fva.json index f31092a84..0784592ae 100644 --- a/cobra/test/data/textbook_fva.json +++ b/cobra/test/data/textbook_fva.json @@ -1 +1 @@ -{"maximum": {"EX_fum_e": 0.0, "ACALDt": 0.0, "EX_glc__D_e": -10.0, "EX_mal__L_e": -0.0, "ADK1": -0.0, "ICL": -0.0, "TALA": 1.49698, "EX_ac_e": -0.0, "PGI": 4.86086, "ACKr": 0.0, "NADTRHD": -0.0, "SUCCt2_2": -0.0, "O2t": 21.79949, "EX_co2_e": 22.80983, "PTAr": -0.0, "EX_h2o_e": 29.17583, "GLUDy": -4.54186, "ACONTa": 6.00725, "GLCpts": 10.0, "GAPD": 16.02353, "TKT1": 1.49698, "TKT2": 1.1815, "NADH16": 38.53461, "EX_etoh_e": -0.0, "ME1": -0.0, "FBP": -0.0, "GLUt2r": 0.0, "SUCDi": 1000.0, "EX_h_e": 17.53087, "ACt2r": 0.0, "GLUSy": -0.0, "TPI": 7.47738, "PYRt2": 0.0, "PGM": -14.71614, "Biomass_Ecoli_core": 0.87392, "PFL": -0.0, "RPE": 2.67848, "RPI": -2.2815, "EX_succ_e": -0.0, "ACONTb": 6.00725, "EX_lac__D_e": -0.0, "PPC": 2.50431, "ALCD2x": 0.0, "AKGDH": 5.06438, "EX_acald_e": -0.0, "EX_nh4_e": -4.76532, "GLUN": -0.0, "EX_gln__L_e": 0.0, "EX_glu__L_e": -0.0, "GND": 4.95998, "PGL": 4.95998, "PPCK": -0.0, "ENO": 14.71614, "EX_fru_e": -0.0, "AKGt2r": 0.0, "SUCCt3": -0.0, "PDH": 9.28253, "EX_pyr_e": -0.0, "EX_o2_e": -21.79949, "PPS": -0.0, "H2Ot": -29.17583, "GLNabc": 0.0, "MDH": 5.06438, "EX_akg_e": -0.0, "ME2": -0.0, "FORt2": -0.0, "EX_for_e": -0.0, "SUCOAS": -5.06438, "PIt2r": 3.2149, "CS": 6.00725, "MALS": -0.0, "FBA": 7.47738, "FRUpts2": 0.0, "PYK": 1.75818, "ATPM": 8.39, "LDH_D": 0.0, "CYTBD": 43.59899, "NH4t": 4.76532, "CO2t": -22.80983, "THD2": -0.0, "ATPS4r": 45.51401, "D_LACt2": 0.0, "FRD7": 994.93562, "GLNS": 0.22346, "G6PDH2r": 4.95998, "MALt2_2": 0.0, "FORti": -0.0, "PFK": 7.47738, "ETOHt2r": 0.0, "ICDHyr": 6.00725, "PGK": -16.02353, "ACALD": 0.0, "FUMt2_2": 0.0, "FUM": 5.06438, "EX_pi_e": -3.2149}, "minimum": {"EX_fum_e": 0.0, "ACALDt": 0.0, "EX_glc__D_e": -10.0, "EX_mal__L_e": 0.0, "ADK1": 0.0, "ICL": 0.0, "TALA": 1.49698, "EX_ac_e": 0.0, "PGI": 4.86086, "ACKr": 0.0, "NADTRHD": 0.0, "SUCCt2_2": 0.0, "O2t": 21.79949, "EX_co2_e": 22.80983, "PTAr": 0.0, "EX_h2o_e": 29.17583, "GLUDy": -4.54186, "ACONTa": 6.00725, "GLCpts": 10.0, "GAPD": 16.02353, "TKT1": 1.49698, "TKT2": 1.1815, "NADH16": 38.53461, "EX_etoh_e": 0.0, "ME1": 0.0, "FBP": 0.0, "GLUt2r": 0.0, "SUCDi": 5.06438, "EX_h_e": 17.53087, "ACt2r": 0.0, "GLUSy": 0.0, "TPI": 7.47738, "PYRt2": 0.0, "PGM": -14.71614, "Biomass_Ecoli_core": 0.87392, "PFL": 0.0, "RPE": 2.67848, "RPI": -2.2815, "EX_succ_e": 0.0, "ACONTb": 6.00725, "EX_lac__D_e": 0.0, "PPC": 2.50431, "ALCD2x": 0.0, "AKGDH": 5.06438, "EX_acald_e": 0.0, "EX_nh4_e": -4.76532, "GLUN": 0.0, "EX_gln__L_e": 0.0, "EX_glu__L_e": 0.0, "GND": 4.95998, "PGL": 4.95998, "PPCK": 0.0, "ENO": 14.71614, "EX_fru_e": 0.0, "AKGt2r": 0.0, "SUCCt3": 0.0, "PDH": 9.28253, "EX_pyr_e": 0.0, "EX_o2_e": -21.79949, "PPS": 0.0, "H2Ot": -29.17583, "GLNabc": 0.0, "MDH": 5.06438, "EX_akg_e": 0.0, "ME2": 0.0, "FORt2": 0.0, "EX_for_e": 0.0, "SUCOAS": -5.06438, "PIt2r": 3.2149, "CS": 6.00725, "MALS": 0.0, "FBA": 7.47738, "FRUpts2": 0.0, "PYK": 1.75818, "ATPM": 8.39, "LDH_D": 0.0, "CYTBD": 43.59899, "NH4t": 4.76532, "CO2t": -22.80983, "THD2": 0.0, "ATPS4r": 45.51401, "D_LACt2": 0.0, "FRD7": 0.0, "GLNS": 0.22346, "G6PDH2r": 4.95998, "MALt2_2": 0.0, "FORti": -0.0, "PFK": 7.47738, "ETOHt2r": 0.0, "ICDHyr": 6.00725, "PGK": -16.02353, "ACALD": 0.0, "FUMt2_2": 0.0, "FUM": 5.06438, "EX_pi_e": -3.2149}} \ No newline at end of file +{"maximum": {"ACALD": 0.0, "ACALDt": 0.0, "ACKr": 0.0, "ACONTa": 6.00725, "ACONTb": 6.00725, "ACt2r": 0.0, "ADK1": -0.0, "AKGDH": 5.06438, "AKGt2r": 0.0, "ALCD2x": 0.0, "ATPM": 8.39, "ATPS4r": 45.51401, "Biomass_Ecoli_core": 0.87392, "CO2t": -22.80983, "CS": 6.00725, "CYTBD": 43.59899, "D_LACt2": 0.0, "ENO": 14.71614, "ETOHt2r": 0.0, "EX_ac_e": -0.0, "EX_acald_e": -0.0, "EX_akg_e": -0.0, "EX_co2_e": 22.80983, "EX_etoh_e": -0.0, "EX_for_e": -0.0, "EX_fru_e": -0.0, "EX_fum_e": 0.0, "EX_glc__D_e": -10.0, "EX_gln__L_e": 0.0, "EX_glu__L_e": -0.0, "EX_h_e": 17.53087, "EX_h2o_e": 29.17583, "EX_lac__D_e": -0.0, "EX_mal__L_e": 0.0, "EX_nh4_e": -4.76532, "EX_o2_e": -21.79949, "EX_pi_e": -3.2149, "EX_pyr_e": -0.0, "EX_succ_e": -0.0, "FBA": 7.47738, "FBP": -0.0, "FORt2": -0.0, "FORti": -0.0, "FRD7": 994.93562, "FRUpts2": 0.0, "FUM": 5.06438, "FUMt2_2": 0.0, "G6PDH2r": 4.95998, "GAPD": 16.02353, "GLCpts": 10.0, "GLNS": 0.22346, "GLNabc": 0.0, "GLUDy": -4.54186, "GLUN": -0.0, "GLUSy": -0.0, "GLUt2r": 0.0, "GND": 4.95998, "H2Ot": -29.17583, "ICDHyr": 6.00725, "ICL": -0.0, "LDH_D": 0.0, "MALS": -0.0, "MALt2_2": 0.0, "MDH": 5.06438, "ME1": -0.0, "ME2": -0.0, "NADH16": 38.53461, "NADTRHD": -0.0, "NH4t": 4.76532, "O2t": 21.79949, "PDH": 9.28253, "PFK": 7.47738, "PFL": -0.0, "PGI": 4.86086, "PGK": -16.02353, "PGL": 4.95998, "PGM": -14.71614, "PIt2r": 3.2149, "PPC": 2.50431, "PPCK": -0.0, "PPS": -0.0, "PTAr": -0.0, "PYK": 1.75818, "PYRt2": 0.0, "RPE": 2.67848, "RPI": -2.2815, "SUCCt2_2": -0.0, "SUCCt3": -0.0, "SUCDi": 1000.0, "SUCOAS": -5.06438, "TALA": 1.49698, "THD2": -0.0, "TKT1": 1.49698, "TKT2": 1.1815, "TPI": 7.47738}, "minimum": {"ACALD": 0.0, "ACALDt": 0.0, "ACKr": 0.0, "ACONTa": 6.00725, "ACONTb": 6.00725, "ACt2r": 0.0, "ADK1": 0.0, "AKGDH": 5.06438, "AKGt2r": 0.0, "ALCD2x": 0.0, "ATPM": 8.39, "ATPS4r": 45.51401, "Biomass_Ecoli_core": 0.87392, "CO2t": -22.80983, "CS": 6.00725, "CYTBD": 43.59899, "D_LACt2": 0.0, "ENO": 14.71614, "ETOHt2r": 0.0, "EX_ac_e": 0.0, "EX_acald_e": 0.0, "EX_akg_e": 0.0, "EX_co2_e": 22.80983, "EX_etoh_e": 0.0, "EX_for_e": 0.0, "EX_fru_e": 0.0, "EX_fum_e": 0.0, "EX_glc__D_e": -10.0, "EX_gln__L_e": 0.0, "EX_glu__L_e": 0.0, "EX_h_e": 17.53087, "EX_h2o_e": 29.17583, "EX_lac__D_e": 0.0, "EX_mal__L_e": 0.0, "EX_nh4_e": -4.76532, "EX_o2_e": -21.79949, "EX_pi_e": -3.2149, "EX_pyr_e": 0.0, "EX_succ_e": 0.0, "FBA": 7.47738, "FBP": 0.0, "FORt2": 0.0, "FORti": 0.0, "FRD7": 0.0, "FRUpts2": 0.0, "FUM": 5.06438, "FUMt2_2": 0.0, "G6PDH2r": 4.95998, "GAPD": 16.02353, "GLCpts": 10.0, "GLNS": 0.22346, "GLNabc": 0.0, "GLUDy": -4.54186, "GLUN": 0.0, "GLUSy": 0.0, "GLUt2r": 0.0, "GND": 4.95998, "H2Ot": -29.17583, "ICDHyr": 6.00725, "ICL": 0.0, "LDH_D": 0.0, "MALS": 0.0, "MALt2_2": 0.0, "MDH": 5.06438, "ME1": 0.0, "ME2": 0.0, "NADH16": 38.53461, "NADTRHD": 0.0, "NH4t": 4.76532, "O2t": 21.79949, "PDH": 9.28253, "PFK": 7.47738, "PFL": 0.0, "PGI": 4.86086, "PGK": -16.02353, "PGL": 4.95998, "PGM": -14.71614, "PIt2r": 3.2149, "PPC": 2.50431, "PPCK": 0.0, "PPS": 0.0, "PTAr": 0.0, "PYK": 1.75818, "PYRt2": 0.0, "RPE": 2.67848, "RPI": -2.2815, "SUCCt2_2": 0.0, "SUCCt3": 0.0, "SUCDi": 5.06438, "SUCOAS": -5.06438, "TALA": 1.49698, "THD2": 0.0, "TKT1": 1.49698, "TKT2": 1.1815, "TPI": 7.47738}} \ No newline at end of file diff --git a/cobra/test/data/textbook_pfba_fva.json b/cobra/test/data/textbook_pfba_fva.json index c8acb0281..09633dd87 100644 --- a/cobra/test/data/textbook_pfba_fva.json +++ b/cobra/test/data/textbook_pfba_fva.json @@ -1 +1 @@ -{"maximum": {"ACALD": 0.0, "ACALDt": 0.0, "ACKr": 0.0, "ACONTa": 6.00725, "ACONTb": 6.00725, "ACt2r": 0.0, "ADK1": -0.0, "AKGDH": 5.06438, "AKGt2r": 0.0, "ALCD2x": -0.0, "ATPM": 8.39, "ATPS4r": 45.51401, "Biomass_Ecoli_core": 0.87392, "CO2t": -22.80983, "CS": 6.00725, "CYTBD": 43.59899, "D_LACt2": 0.0, "ENO": 14.71614, "ETOHt2r": -0.0, "EX_ac_e": -0.0, "EX_acald_e": -0.0, "EX_akg_e": -0.0, "EX_co2_e": 22.80983, "EX_etoh_e": -0.0, "EX_for_e": -0.0, "EX_fru_e": -0.0, "EX_fum_e": 0.0, "EX_glc__D_e": -10.0, "EX_gln__L_e": 0.0, "EX_glu__L_e": -0.0, "EX_h2o_e": 29.17583, "EX_h_e": 17.53087, "EX_lac__D_e": -0.0, "EX_mal__L_e": -0.0, "EX_nh4_e": -4.76532, "EX_o2_e": -21.79949, "EX_pi_e": -3.2149, "EX_pyr_e": -0.0, "EX_succ_e": -0.0, "FBA": 7.47738, "FBP": -0.0, "FORt2": -0.0, "FORti": -0.0, "FRD7": 25.9211, "FRUpts2": 0.0, "FUM": 5.06438, "FUMt2_2": 0.0, "G6PDH2r": 4.95998, "GAPD": 16.02353, "GLCpts": 10.0, "GLNS": 0.22346, "GLNabc": 0.0, "GLUDy": -4.54186, "GLUN": -0.0, "GLUSy": -0.0, "GLUt2r": 0.0, "GND": 4.95998, "H2Ot": -29.17583, "ICDHyr": 6.00725, "ICL": -0.0, "LDH_D": 0.0, "MALS": -0.0, "MALt2_2": 0.0, "MDH": 5.06438, "ME1": -0.0, "ME2": -0.0, "NADH16": 38.53461, "NADTRHD": -0.0, "NH4t": 4.76532, "O2t": 21.79949, "PDH": 9.28253, "PFK": 7.47738, "PFL": -0.0, "PGI": 4.86086, "PGK": -16.02353, "PGL": 4.95998, "PGM": -14.71614, "PIt2r": 3.2149, "PPC": 2.50431, "PPCK": -0.0, "PPS": -0.0, "PTAr": -0.0, "PYK": 1.75818, "PYRt2": 0.0, "RPE": 2.67848, "RPI": -2.2815, "SUCCt2_2": -0.0, "SUCCt3": -0.0, "SUCDi": 30.98548, "SUCOAS": -5.06438, "TALA": 1.49698, "THD2": -0.0, "TKT1": 1.49698, "TKT2": 1.1815, "TPI": 7.47738}, "minimum": {"ACALD": 0.0, "ACALDt": 0.0, "ACKr": 0.0, "ACONTa": 6.00725, "ACONTb": 6.00725, "ACt2r": 0.0, "ADK1": 0.0, "AKGDH": 5.06438, "AKGt2r": 0.0, "ALCD2x": 0.0, "ATPM": 8.39, "ATPS4r": 45.51401, "Biomass_Ecoli_core": 0.87392, "CO2t": -22.80983, "CS": 6.00725, "CYTBD": 43.59899, "D_LACt2": 0.0, "ENO": 14.71614, "ETOHt2r": 0.0, "EX_ac_e": 0.0, "EX_acald_e": 0.0, "EX_akg_e": 0.0, "EX_co2_e": 22.80983, "EX_etoh_e": 0.0, "EX_for_e": 0.0, "EX_fru_e": 0.0, "EX_fum_e": 0.0, "EX_glc__D_e": -10.0, "EX_gln__L_e": 0.0, "EX_glu__L_e": 0.0, "EX_h2o_e": 29.17583, "EX_h_e": 17.53087, "EX_lac__D_e": 0.0, "EX_mal__L_e": 0.0, "EX_nh4_e": -4.76532, "EX_o2_e": -21.79949, "EX_pi_e": -3.2149, "EX_pyr_e": 0.0, "EX_succ_e": 0.0, "FBA": 7.47738, "FBP": 0.0, "FORt2": 0.0, "FORti": 0.0, "FRD7": 0.0, "FRUpts2": 0.0, "FUM": 5.06438, "FUMt2_2": 0.0, "G6PDH2r": 4.95998, "GAPD": 16.02353, "GLCpts": 10.0, "GLNS": 0.22346, "GLNabc": 0.0, "GLUDy": -4.54186, "GLUN": 0.0, "GLUSy": 0.0, "GLUt2r": 0.0, "GND": 4.95998, "H2Ot": -29.17583, "ICDHyr": 6.00725, "ICL": 0.0, "LDH_D": 0.0, "MALS": 0.0, "MALt2_2": 0.0, "MDH": 5.06438, "ME1": 0.0, "ME2": 0.0, "NADH16": 38.53461, "NADTRHD": 0.0, "NH4t": 4.76532, "O2t": 21.79949, "PDH": 9.28253, "PFK": 7.47738, "PFL": 0.0, "PGI": 4.86086, "PGK": -16.02353, "PGL": 4.95998, "PGM": -14.71614, "PIt2r": 3.2149, "PPC": 2.50431, "PPCK": 0.0, "PPS": 0.0, "PTAr": 0.0, "PYK": 1.75818, "PYRt2": 0.0, "RPE": 2.67848, "RPI": -2.2815, "SUCCt2_2": 0.0, "SUCCt3": 0.0, "SUCDi": 5.06438, "SUCOAS": -5.06438, "TALA": 1.49698, "THD2": 0.0, "TKT1": 1.49698, "TKT2": 1.1815, "TPI": 7.47738}} \ No newline at end of file +{"maximum": {"ACALD": 0.0, "ACALDt": 0.0, "ACKr": 0.0, "ACONTa": 6.00725, "ACONTb": 6.00725, "ACt2r": 0.0, "ADK1": 0.0, "AKGDH": 5.06438, "AKGt2r": 0.0, "ALCD2x": 0.0, "ATPM": 8.39, "ATPS4r": 45.51401, "Biomass_Ecoli_core": 0.87392, "CO2t": -22.80983, "CS": 6.00725, "CYTBD": 43.59899, "D_LACt2": 0.0, "ENO": 14.71614, "ETOHt2r": 0.0, "EX_ac_e": 0.0, "EX_acald_e": 0.0, "EX_akg_e": 0.0, "EX_co2_e": 22.80983, "EX_etoh_e": 0.0, "EX_for_e": 0.0, "EX_fru_e": 0.0, "EX_fum_e": 0.0, "EX_glc__D_e": -10.0, "EX_gln__L_e": 0.0, "EX_glu__L_e": 0.0, "EX_h_e": 17.53087, "EX_h2o_e": 29.17583, "EX_lac__D_e": 0.0, "EX_mal__L_e": 0.0, "EX_nh4_e": -4.76532, "EX_o2_e": -21.79949, "EX_pi_e": -3.2149, "EX_pyr_e": 0.0, "EX_succ_e": 0.0, "FBA": 7.47738, "FBP": 0.0, "FORt2": 0.0, "FORti": 0.0, "FRD7": 25.9211, "FRUpts2": 0.0, "FUM": 5.06438, "FUMt2_2": 0.0, "G6PDH2r": 4.95998, "GAPD": 16.02353, "GLCpts": 10.0, "GLNS": 0.22346, "GLNabc": 0.0, "GLUDy": -4.54186, "GLUN": 0.0, "GLUSy": 0.0, "GLUt2r": 0.0, "GND": 4.95998, "H2Ot": -29.17583, "ICDHyr": 6.00725, "ICL": 0.0, "LDH_D": 0.0, "MALS": 0.0, "MALt2_2": 0.0, "MDH": 5.06438, "ME1": 0.0, "ME2": 0.0, "NADH16": 38.53461, "NADTRHD": 0.0, "NH4t": 4.76532, "O2t": 21.79949, "PDH": 9.28253, "PFK": 7.47738, "PFL": 0.0, "PGI": 4.86086, "PGK": -16.02353, "PGL": 4.95998, "PGM": -14.71614, "PIt2r": 3.2149, "PPC": 2.50431, "PPCK": 0.0, "PPS": 0.0, "PTAr": 0.0, "PYK": 1.75818, "PYRt2": 0.0, "RPE": 2.67848, "RPI": -2.2815, "SUCCt2_2": 0.0, "SUCCt3": 0.0, "SUCDi": 30.98548, "SUCOAS": -5.06438, "TALA": 1.49698, "THD2": 0.0, "TKT1": 1.49698, "TKT2": 1.1815, "TPI": 7.47738}, "minimum": {"ACALD": 0.0, "ACALDt": 0.0, "ACKr": 0.0, "ACONTa": 6.00725, "ACONTb": 6.00725, "ACt2r": 0.0, "ADK1": 0.0, "AKGDH": 5.06438, "AKGt2r": 0.0, "ALCD2x": 0.0, "ATPM": 8.39, "ATPS4r": 45.51401, "Biomass_Ecoli_core": 0.87392, "CO2t": -22.80983, "CS": 6.00725, "CYTBD": 43.59899, "D_LACt2": 0.0, "ENO": 14.71614, "ETOHt2r": 0.0, "EX_ac_e": 0.0, "EX_acald_e": 0.0, "EX_akg_e": 0.0, "EX_co2_e": 22.80983, "EX_etoh_e": 0.0, "EX_for_e": 0.0, "EX_fru_e": 0.0, "EX_fum_e": 0.0, "EX_glc__D_e": -10.0, "EX_gln__L_e": 0.0, "EX_glu__L_e": 0.0, "EX_h_e": 17.53087, "EX_h2o_e": 29.17583, "EX_lac__D_e": 0.0, "EX_mal__L_e": 0.0, "EX_nh4_e": -4.76532, "EX_o2_e": -21.79949, "EX_pi_e": -3.2149, "EX_pyr_e": 0.0, "EX_succ_e": 0.0, "FBA": 7.47738, "FBP": 0.0, "FORt2": 0.0, "FORti": 0.0, "FRD7": 0.0, "FRUpts2": 0.0, "FUM": 5.06438, "FUMt2_2": 0.0, "G6PDH2r": 4.95998, "GAPD": 16.02353, "GLCpts": 10.0, "GLNS": 0.22346, "GLNabc": 0.0, "GLUDy": -4.54186, "GLUN": 0.0, "GLUSy": 0.0, "GLUt2r": 0.0, "GND": 4.95998, "H2Ot": -29.17583, "ICDHyr": 6.00725, "ICL": 0.0, "LDH_D": 0.0, "MALS": 0.0, "MALt2_2": 0.0, "MDH": 5.06438, "ME1": 0.0, "ME2": 0.0, "NADH16": 38.53461, "NADTRHD": 0.0, "NH4t": 4.76532, "O2t": 21.79949, "PDH": 9.28253, "PFK": 7.47738, "PFL": 0.0, "PGI": 4.86086, "PGK": -16.02353, "PGL": 4.95998, "PGM": -14.71614, "PIt2r": 3.2149, "PPC": 2.50431, "PPCK": 0.0, "PPS": 0.0, "PTAr": 0.0, "PYK": 1.75818, "PYRt2": 0.0, "RPE": 2.67848, "RPI": -2.2815, "SUCCt2_2": 0.0, "SUCCt3": 0.0, "SUCDi": 5.06438, "SUCOAS": -5.06438, "TALA": 1.49698, "THD2": 0.0, "TKT1": 1.49698, "TKT2": 1.1815, "TPI": 7.47738}} \ No newline at end of file diff --git a/cobra/test/data/update_pickles.py b/cobra/test/data/update_pickles.py index d37977f52..dc3a5ed75 100755 --- a/cobra/test/data/update_pickles.py +++ b/cobra/test/data/update_pickles.py @@ -21,6 +21,10 @@ from pickle import load, dump +config = cobra.Configuration() +config.solver = "glpk" + + # ecoli ecoli_model = read_sbml_model("iJO1366.xml") with open("iJO1366.pickle", "wb") as outfile: diff --git a/cobra/test/test_core/test_core_reaction.py b/cobra/test/test_core/test_core_reaction.py index 18dc2381c..892eaf985 100644 --- a/cobra/test/test_core/test_core_reaction.py +++ b/cobra/test/test_core/test_core_reaction.py @@ -289,6 +289,7 @@ def test_copy(model): for gene in copied.genes: assert gene is not model.genes.get_by_id(gene.id) assert gene.model is not model + assert len(model.get_associated_groups(copied.id)) == 0 def test_iadd(model): diff --git a/cobra/test/test_core/test_group.py b/cobra/test/test_core/test_group.py new file mode 100644 index 000000000..18d50644b --- /dev/null +++ b/cobra/test/test_core/test_group.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- + +"""Test functions of model.py""" + + +import pytest + +from cobra.core import Group + + +def test_group_add_elements(model): + num_members = 5 + reactions_for_group = model.reactions[0:num_members] + group = Group("arbitrary_group1") + group.add_members(reactions_for_group) + group.kind = "collection" + # number of member sin group should equal the number of reactions + # assigned to the group + assert len(group.members) == num_members + + # Choose an overlapping, larger subset of reactions for the group + num_total_members = 12 + reactions_for_larger_group = model.reactions[0:num_total_members] + group.add_members(reactions_for_larger_group) + assert len(group.members) == num_total_members + + +def test_group_kind(): + group = Group("arbitrary_group1") + with pytest.raises(ValueError) as excinfo: + group.kind = "non-SBML compliant group kind" + assert "Kind can only by one of:" in str(excinfo.value) + + group.kind = "collection" + assert group.kind == "collection" diff --git a/cobra/test/test_core/test_model.py b/cobra/test/test_core/test_model.py index b5b21e4c1..9d95f425a 100644 --- a/cobra/test/test_core/test_model.py +++ b/cobra/test/test_core/test_model.py @@ -15,7 +15,7 @@ from optlang.symbolics import Zero import cobra.util.solver as su -from cobra.core import Metabolite, Model, Reaction +from cobra.core import Group, Metabolite, Model, Reaction from cobra.exceptions import OptimizationError from cobra.util.solver import SolverNotFound, set_objective, solvers @@ -324,6 +324,73 @@ def test_remove_gene(model): assert target_gene not in reaction.genes +def test_group_model_reaction_association(model): + num_members = 5 + reactions_for_group = model.reactions[0:num_members] + group = Group("arbitrary_group1") + group.add_members(reactions_for_group) + group.kind = "collection" + model.add_groups([group]) + # group should point to and be associated with the model + assert group._model is model + assert group in model.groups + + # model.get_associated_groups should find the group for each reaction + # we added to the group + for reaction in reactions_for_group: + assert group in model.get_associated_groups(reaction) + + # remove the group from the model and check that reactions are no + # longer associated with the group + model.remove_groups([group]) + assert group not in model.groups + assert group._model is not model + for reaction in reactions_for_group: + assert group not in model.get_associated_groups(reaction) + + +def test_group_members_add_to_model(model): + # remove a few reactions from the model and add them to a new group + num_members = 5 + reactions_for_group = model.reactions[0:num_members] + model.remove_reactions(reactions_for_group, remove_orphans=False) + group = Group("arbitrary_group1") + group.add_members(reactions_for_group) + group.kind = "collection" + # the old reactions should not be in the model + for reaction in reactions_for_group: + assert reaction not in model.reactions + + # add the group to the model and check that the reactions were added + model.add_groups([group]) + assert group in model.groups + for reaction in reactions_for_group: + assert reaction in model.reactions + + +def test_group_loss_of_elements(model): + # when a metabolite, reaction, or gene is removed from a model, it + # should no longer be a member of any groups + num_members_each = 5 + elements_for_group = model.reactions[0:num_members_each] + elements_for_group.extend(model.metabolites[0:num_members_each]) + elements_for_group.extend(model.genes[0:num_members_each]) + group = Group("arbitrary_group1") + group.add_members(elements_for_group) + group.kind = "collection" + model.add_groups([group]) + + remove_met = model.metabolites[0] + model.remove_metabolites([remove_met]) + remove_rxn = model.reactions[0] + model.remove_reactions([remove_rxn]) + remove_gene = model.genes[0] + remove_gene.remove_from_model() + assert remove_met not in group.members + assert remove_rxn not in group.members + assert remove_gene not in group.members + + def test_exchange_reactions(model): assert set(model.exchanges) == set([rxn for rxn in model.reactions if rxn.id.startswith("EX")]) diff --git a/cobra/test/test_io/test_sbml.py b/cobra/test/test_io/test_sbml.py index fabf1fd7e..a8e6a2010 100644 --- a/cobra/test/test_io/test_sbml.py +++ b/cobra/test/test_io/test_sbml.py @@ -1,67 +1,299 @@ # -*- coding: utf-8 -*- - -"""Test functionalities provided by sbml.py""" +""" +Testing SBML functionality based on libsbml. +""" from __future__ import absolute_import -from io import BytesIO -from os.path import getsize, join +from collections import namedtuple +from os import unlink +from os.path import join, split +from pickle import load +from tempfile import gettempdir import pytest -from cobra import io -from cobra.test.test_io.conftest import compare_models +from cobra import Model +from cobra.io import read_sbml_model, validate_sbml_model, write_sbml_model try: - import libsbml + import jsonschema except ImportError: - libsbml = None + jsonschema = None + +# ---------------------------------- +# Definition of SBML files to test +# ---------------------------------- +IOTrial = namedtuple('IOTrial', + ['name', 'reference_file', 'test_file', 'read_function', + 'write_function', 'validation_function']) +trials = [IOTrial('fbc2', 'mini.pickle', 'mini_fbc2.xml', + read_sbml_model, write_sbml_model, + validate_sbml_model), + IOTrial('fbc2Gz', 'mini.pickle', 'mini_fbc2.xml.gz', + read_sbml_model, write_sbml_model, None), + IOTrial('fbc2Bz2', 'mini.pickle', 'mini_fbc2.xml.bz2', + read_sbml_model, write_sbml_model, None), + IOTrial('fbc1', 'mini.pickle', 'mini_fbc1.xml', + read_sbml_model, write_sbml_model, None), + IOTrial('cobra', None, 'mini_cobra.xml', + read_sbml_model, write_sbml_model, None), + ] +trial_names = [node.name for node in trials] + + +@pytest.mark.parametrize("trial", trials) +def test_validate(trial, data_directory): + """ Test validation function. """ + if trial.validation_function is None: + pytest.skip('not implemented') + test_file = join(data_directory, trial.test_file) + trial.validation_function(test_file) + + +class TestCobraIO: + """ Tests the read and write functions. """ + + @classmethod + def compare_models(cls, name, model1, model2): + assert len(model1.reactions) == len(model2.reactions) + assert len(model1.metabolites) == len(model2.metabolites) + assert model1.objective.direction == model2.objective.direction + for attr in ("id", "name", "lower_bound", "upper_bound", + "objective_coefficient", "gene_reaction_rule"): + assert getattr(model1.reactions[0], attr) == getattr( + model2.reactions[0], attr) + assert getattr(model1.reactions[5], attr) == getattr( + model2.reactions[5], attr) + assert getattr(model1.reactions[-1], attr) == getattr( + model2.reactions[-1], attr) + for attr in ("id", "name", "compartment", "formula", "charge"): + assert getattr(model1.metabolites[0], attr) == getattr( + model2.metabolites[0], attr) + assert getattr(model1.metabolites[5], attr) == getattr( + model2.metabolites[5], attr) + assert getattr(model1.metabolites[-1], attr) == getattr( + model2.metabolites[-1], attr) + assert len(model1.reactions[0].metabolites) == len( + model2.reactions[0].metabolites) + assert len(model1.reactions[8].metabolites) == len( + model2.reactions[8].metabolites) + assert len(model1.reactions[-1].metabolites) == len( + model2.reactions[-1].metabolites) + assert len(model1.genes) == len(model2.genes) + + # ensure they have the same solution max + solution1 = model1.optimize() + solution2 = model2.optimize() + assert abs(solution1.objective_value - + solution2.objective_value) < 0.001 + # ensure the references are correct + assert model2.metabolites[0]._model is model2 + assert model2.reactions[0]._model is model2 + assert model2.genes[0]._model is model2 + + @classmethod + def extra_comparisons(cls, name, model1, model2): + assert model1.compartments == model2.compartments + + # FIXME: problems of duplicate annotations in test data + # ('cas': ['56-65-5', '56-65-5']) + # assert dict(model1.metabolites[4].annotation) == dict( + # model2.metabolites[4].annotation) + d1 = model1.reactions[4].annotation + d2 = model2.reactions[4].annotation + assert list(d1.keys()) == list(d2.keys()) + for k in d1: + assert set(d1[k]) == set(d2[k]) + assert dict(model1.reactions[4].annotation) == dict( + model2.reactions[4].annotation) + assert dict(model1.genes[5].annotation) == dict( + model2.genes[5].annotation) + + for attr in ("id", "name"): + assert getattr(model1.genes[0], attr) == getattr(model2.genes[0], + attr) + assert getattr(model1.genes[10], attr) == getattr(model2.genes[10], + attr) + assert getattr(model1.genes[-1], attr) == getattr(model2.genes[-1], + attr) + + def test_read_1(self, io_trial): + name, reference_model, test_model, _ = io_trial + if name in ['fbc1']: + pytest.xfail('not supported') + if reference_model: + self.compare_models(name, reference_model, test_model) + + def test_read_2(self, io_trial): + name, reference_model, test_model, _ = io_trial + if name in ['fbc1', 'mat', 'cobra', 'raven-mat']: + pytest.xfail('not supported') + if reference_model: + self.extra_comparisons(name, reference_model, test_model) + + def test_write_1(self, io_trial): + name, _, test_model, reread_model = io_trial + if name in ['fbc1', 'raven-mat']: + pytest.xfail('not supported') + + self.compare_models(name, test_model, reread_model) + + def test_write_2(self, io_trial): + name, _, test_model, reread_model = io_trial + if name in ['fbc1', 'mat', 'cobra', 'raven-mat']: + pytest.xfail('not supported') + self.extra_comparisons(name, test_model, reread_model) + + +@pytest.fixture(scope="module", params=trials, ids=trial_names) +def io_trial(request, data_directory): + reference_model = None + if request.param.reference_file: + with open(join(data_directory, request.param.reference_file), + "rb") as infile: + reference_model = load(infile) + test_model = request.param.read_function(join(data_directory, + request.param.test_file)) + test_output_filename = join(gettempdir(), + split(request.param.test_file)[-1]) + # test writing the model within a context with a non-empty stack + with test_model: + test_model.objective = test_model.objective + request.param.write_function(test_model, test_output_filename) + reread_model = request.param.read_function(test_output_filename) + unlink(test_output_filename) + return request.param.name, reference_model, test_model, reread_model + + +def test_filehandle(data_directory, tmp_path): + """Test reading and writing to file handle.""" + with open(join(data_directory, "mini_fbc2.xml"), "r") as f_in: + model1 = read_sbml_model(f_in) + assert model1 is not None + + sbml_path = join(str(tmp_path), "test.xml") + with open(sbml_path, "w") as f_out: + write_sbml_model(model1, f_out) + + with open(sbml_path, "r") as f_in: + model2 = read_sbml_model(f_in) + + TestCobraIO.compare_models(name="filehandle", + model1=model1, model2=model2) + + +def test_from_sbml_string(data_directory): + """Test reading from SBML string.""" + sbml_path = join(data_directory, "mini_fbc2.xml") + with open(sbml_path, "r") as f_in: + sbml_str = f_in.read() + model1 = read_sbml_model(sbml_str) + + model2 = read_sbml_model(sbml_path) + TestCobraIO.compare_models(name="read from string", + model1=model1, model2=model2) + + +def test_model_history(tmp_path): + """Testing reading and writing of ModelHistory.""" + model = Model("test") + model._sbml = { + "creators": [{ + "familyName": "Mustermann", + "givenName": "Max", + "organisation": "Muster University", + "email": "muster@university.com", + }] + } + + sbml_path = join(str(tmp_path), "test.xml") + with open(sbml_path, "w") as f_out: + write_sbml_model(model, f_out) + + with open(sbml_path, "r") as f_in: + model2 = read_sbml_model(f_in) + + assert "creators" in model2._sbml + assert len(model2._sbml["creators"]) is 1 + c = model2._sbml["creators"][0] + assert c["familyName"] == "Mustermann" + assert c["givenName"] == "Max" + assert c["organisation"] == "Muster University" + assert c["email"] == "muster@university.com" + + +def test_groups(data_directory, tmp_path): + """Testing reading and writing of groups""" + sbml_path = join(data_directory, "e_coli_core.xml") + model = read_sbml_model(sbml_path) + assert model.groups is not None + assert len(model.groups) == 10 + g1 = model.groups[0] + assert len(g1.members) == 6 + + temp_path = join(str(tmp_path), "test.xml") + with open(temp_path, "w") as f_out: + write_sbml_model(model, f_out) + + with open(temp_path, "r") as f_in: + model2 = read_sbml_model(f_in) + + assert model2.groups is not None + assert len(model2.groups) == 10 + g1 = model2.groups[0] + assert len(g1.members) == 6 + +def test_validate(data_directory): + """Test the validation code. """ + sbml_path = join(data_directory, "mini_fbc2.xml") + with open(sbml_path, "r") as f_in: + model1, errors = validate_sbml_model(f_in, + check_modeling_practice=True) + assert model1 + assert errors + assert len(errors["SBML_WARNING"]) == 23 -@pytest.fixture(scope="function") -def mini_fbc1_model(data_directory): - return io.read_legacy_sbml(join(data_directory, "mini_fbc1.xml")) +def test_infinity_bounds(data_directory, tmp_path): + """Test infinity bound example. """ + sbml_path = join(data_directory, "fbc_ex1.xml") + model = read_sbml_model(sbml_path) -@pytest.fixture(scope="function") -def mini_cobra_model(data_directory): - return io.read_legacy_sbml(join(data_directory, "mini_cobra.xml")) + # check that simulation works + solution = model.optimize() + # check that values are set + r = model.reactions.get_by_id("EX_X") + assert r.lower_bound == -float("Inf") + assert r.upper_bound == float("Inf") -# TODO: parametrize the arguments after pytest.fixture_request() -# is approved -@pytest.mark.skipif(libsbml is None, reason="libsbml unavailable.") -def test_read_sbml_model(data_directory, mini_fbc1_model, mini_cobra_model): - """Test the reading of a model from SBML v2.""" - mini_fbc1 = io.read_legacy_sbml(join(data_directory, "mini_fbc1.xml")) - mini_cobra = io.read_legacy_sbml(join(data_directory, "mini_cobra.xml")) - assert compare_models(mini_fbc1_model, mini_fbc1) is None - assert compare_models(mini_cobra_model, mini_cobra) is None + temp_path = join(str(tmp_path), "test.xml") + with open(temp_path, "w") as f_out: + write_sbml_model(model, f_out) + with open(temp_path, "r") as f_in: + model2 = read_sbml_model(f_in) + r = model2.reactions.get_by_id("EX_X") + assert r.lower_bound == -float("Inf") + assert r.upper_bound == float("Inf") -@pytest.mark.skipif(libsbml is None, reason="libsbml unavailable.") -def test_read_file_handle(data_directory, mini_model): - """Test the reading of a model passed as a file handle.""" - with open(join(data_directory, "mini_cobra.xml"), "rb") as file_: - model_stream = BytesIO(file_.read()) - read_model = io.read_sbml_model(model_stream) - assert compare_models(mini_model, read_model) is None +def test_boundary_conditions(data_directory): + """Test infinity bound example. """ + sbml_path1 = join(data_directory, "fbc_ex1.xml") + model1 = read_sbml_model(sbml_path1) + sol1 = model1.optimize() -# TODO: parametrize the arguments after pytest.fixture_request() -# is approved -@pytest.mark.skipif(libsbml is None, reason="libsbml unavailable.") -def test_write_sbml_model(tmpdir, mini_fbc1_model, mini_cobra_model): - """Test the writing of a model to SBML v2.""" - mini_fbc1_output_file = tmpdir.join("mini_fbc1.xml") - mini_cobra_output_file = tmpdir.join("mini_cobra.xml") + # model with species boundaryCondition==True + sbml_path2 = join(data_directory, "fbc_ex2.xml") + model2 = read_sbml_model(sbml_path2) + sol2 = model2.optimize() - # convert to str object before passing the filename - io.write_legacy_sbml(mini_fbc1_model, str(mini_fbc1_output_file), - use_fbc_package=True) - io.write_legacy_sbml(mini_cobra_model, str(mini_cobra_output_file), - use_fbc_package=False) + r = model2.reactions.get_by_id("EX_X") + assert r.lower_bound == -float("Inf") + assert r.upper_bound == float("Inf") - assert mini_fbc1_output_file.check() - assert mini_cobra_output_file.check() + assert sol1.objective_value == sol2.objective_value diff --git a/cobra/test/test_io/test_sbml3.py b/cobra/test/test_io/test_sbml3.py deleted file mode 100644 index fcf69d022..000000000 --- a/cobra/test/test_io/test_sbml3.py +++ /dev/null @@ -1,68 +0,0 @@ -# -*- coding: utf-8 -*- - -"""Test functionalities provided by sbml3.py""" - -from __future__ import absolute_import - -from os.path import join - -import pytest -from six import itervalues - -from cobra import io -from cobra.test.test_io.conftest import compare_models - - -@pytest.fixture(scope="function") -def mini_fbc2_model(data_directory): - """Return mini_fbc2 model.""" - return io.sbml3.read_sbml_model(join(data_directory, "mini_fbc2.xml")) - - -# Benchmarks -def test_benchmark_read(data_directory, benchmark): - """Benchmark SBML read.""" - benchmark(io.sbml3.read_sbml_model, join(data_directory, "mini_fbc2.xml")) - - -def test_benchmark_write(model, benchmark, tmpdir): - """Benchmark SBML write.""" - benchmark(io.sbml3.write_sbml_model, model, tmpdir.join("-bench")) - - -# Tests -def test_sbml3_error(data_directory): - """Test invalid SBML read.""" - filename = join(data_directory, "invalid0.xml") - with pytest.raises(io.sbml3.CobraSBMLError): - io.read_sbml_model(filename) - - -def test_validate_sbml_model(data_directory): - """Test validation of SBML.""" - # invalid SBML - for i in range(3): - filename = join(data_directory, "invalid{}.xml".format(i)) - _, errors = io.sbml3.validate_sbml_model(filename) - assert all(len(v) >= 1 for v in itervalues(errors)) is False - - # valid SBML - filename = join(data_directory, "mini_fbc2.xml") - _, errors = io.sbml3.validate_sbml_model(filename) - assert all(len(v) == 0 for v in itervalues(errors)) - - -@pytest.mark.parametrize("sbml_file", ["mini_fbc2.xml", "mini_fbc2.xml.gz", - "mini_fbc2.xml.bz2"]) -def test_read_sbml_model(data_directory, mini_model, sbml_file): - """Test the reading of a model from SBML3.""" - sbml3_model = io.read_sbml_model(join(data_directory, sbml_file)) - assert compare_models(mini_model, sbml3_model) is None - - -@pytest.mark.parametrize("ext", [".xml", ".xml.gz", ".xml.bz2"]) -def test_write_sbml_model(tmpdir, mini_fbc2_model, ext): - """Test the writing of model to SBML3.""" - output_file = tmpdir.join("mini_fbc2{}".format(ext)) - io.write_sbml_model(mini_fbc2_model, output_file) - assert output_file.check() diff --git a/setup.py b/setup.py index 0a2e14253..356bd80d0 100644 --- a/setup.py +++ b/setup.py @@ -35,6 +35,7 @@ except IOError: setup_kwargs["long_description"] = '' + if __name__ == "__main__": setup( name="cobra", @@ -50,7 +51,8 @@ "pandas>=0.17.0", "optlang>=1.4.2", "tabulate", - "depinfo" + "depinfo", + "python-libsbml-experimental>=5.17.2", ], tests_require=[ "jsonschema > 2.5",