Skip to content

Commit

Permalink
feat: add yaml round tripping support (#496)
Browse files Browse the repository at this point in the history
- introduce read/write for yaml, io.yaml
- move dict related functions from io.json to io.dict
  • Loading branch information
Midnighter authored and hredestig committed May 3, 2017
1 parent 42c6921 commit 83fc700
Show file tree
Hide file tree
Showing 15 changed files with 1,737 additions and 396 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ matrix:

before_install:
- if [[ -n "${MB_PYTHON_VERSION}" ]]; then
(travis_retry git clone https://github.com/matthew-brett/multibuild.git && cd multibuild && git checkout e6ebbfa);
(travis_retry git clone https://github.com/matthew-brett/multibuild.git && cd multibuild && git checkout edf5b691d0d565b4e65e655b983c11c883acbeca);
TEST_DEPENDS="swiglpk optlang sympy decorator cython codecov coverage numpy scipy jsonschema six pytest pytest-cov pytest-benchmark tabulate";
BUILD_DEPENDS="swiglpk optlang sympy cython numpy scipy auditwheel==1.5";
source multibuild/common_utils.sh;
Expand Down
51 changes: 27 additions & 24 deletions cobra/core/reaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from collections import defaultdict
from copy import copy, deepcopy
from functools import partial
from operator import attrgetter
from warnings import warn

from six import iteritems, string_types
Expand All @@ -19,7 +20,7 @@
from cobra.util.context import resettable, get_context
from cobra.util.solver import (
linear_reaction_coefficients, set_objective, check_solver_status)
from cobra.util.util import Frozendict, _is_positive
from cobra.util.util import FrozenDict

# precompiled regular expressions
# Matches and/or in a gene reaction rule
Expand Down Expand Up @@ -65,7 +66,7 @@ def __init__(self, id=None, name='', subsystem='', lower_bound=0.,

# A dictionary of metabolites and their stoichiometric coefficients in
# this reaction.
self._metabolites = {}
self._metabolites = dict()

# The set of compartments that partaking metabolites are in.
self._compartments = None
Expand Down Expand Up @@ -374,7 +375,7 @@ def reduced_cost(self):
# read-only
@property
def metabolites(self):
return Frozendict(self._metabolites)
return FrozenDict(self._metabolites)

@property
def genes(self):
Expand Down Expand Up @@ -537,8 +538,9 @@ def remove_from_model(self, model=None, remove_orphans=False):
model : deprecated argument, must be None
"""

new_metabolites = {copy(met): value for met, value
in iteritems(self._metabolites)}
new_metabolites = {
copy(met): value for met, value in iteritems(self._metabolites)}

new_genes = {copy(i) for i in self._genes}

self._model.remove_reactions([self], remove_orphans=remove_orphans)
Expand Down Expand Up @@ -656,8 +658,9 @@ def __imul__(self, coefficient):
E.g. A -> B becomes 2A -> 2B
"""
self._metabolites = {k: coefficient * v for k, v in
iteritems(self._metabolites)}
self._metabolites = {
met: value * coefficient
for met, value in iteritems(self._metabolites)}
return self

def __mul__(self, coefficient):
Expand All @@ -668,27 +671,26 @@ def __mul__(self, coefficient):
@property
def reactants(self):
"""Return a list of reactants for the reaction."""
return [k for k, v in self._metabolites.items() if not _is_positive(v)]
return [k for k, v in iteritems(self._metabolites) if v < 0]

@property
def products(self):
"""Return a list of products for the reaction"""
return [k for k, v in self._metabolites.items() if _is_positive(v)]
return [k for k, v in iteritems(self._metabolites) if v >= 0]

def get_coefficient(self, metabolite_id):
"""Return the stoichiometric coefficient for a metabolite in
the reaction.
"""
Return the stoichiometric coefficient of a metabolite.
Parameters
----------
metabolite_id : str or cobra.core.Metabolite.Metabolite
metabolite_id : str or cobra.Metabolite
"""
_id_to_metabolites = dict([(x.id, x)
for x in self._metabolites])
if isinstance(metabolite_id, Metabolite):
return self._metabolites[metabolite_id]

if hasattr(metabolite_id, 'id'):
metabolite_id = metabolite_id.id
_id_to_metabolites = {m.id: m for m in self._metabolites}
return self._metabolites[_id_to_metabolites[metabolite_id]]

def get_coefficients(self, metabolite_ids):
Expand All @@ -698,7 +700,7 @@ def get_coefficients(self, metabolite_ids):
Parameters
----------
metabolite_ids : iterable
Containing str or :class:`~cobra.core.Metabolite.Metabolite`
Containing ``str`` or ``cobra.Metabolite``s.
"""
return map(self.get_coefficient, metabolite_ids)
Expand Down Expand Up @@ -828,8 +830,9 @@ def subtract_metabolites(self, metabolites, combine=True, reversibly=True):
.. note:: A final coefficient < 0 implies a reactant.
"""
self.add_metabolites({k: -v for k, v in iteritems(metabolites)},
combine=combine, reversibly=reversibly)
self.add_metabolites({
k: -v for k, v in iteritems(metabolites)},
combine=combine, reversibly=reversibly)

@property
def reaction(self):
Expand All @@ -851,10 +854,10 @@ def format(number):
id_type = 'name'
reactant_bits = []
product_bits = []
for the_metabolite, coefficient in sorted(
iteritems(self._metabolites), key=lambda x: x[0].id):
name = str(getattr(the_metabolite, id_type))
if _is_positive(coefficient):
for met in sorted(self._metabolites, key=attrgetter("id")):
coefficient = self._metabolites[met]
name = str(getattr(met, id_type))
if coefficient >= 0:
product_bits.append(format(coefficient) + name)
else:
reactant_bits.append(format(abs(coefficient)) + name)
Expand All @@ -878,7 +881,7 @@ def check_mass_balance(self):
This should be empty for balanced reactions.
"""
reaction_element_dict = defaultdict(int)
for metabolite, coefficient in self._metabolites.items():
for metabolite, coefficient in iteritems(self._metabolites):
if metabolite.charge is not None:
reaction_element_dict["charge"] += \
coefficient * metabolite.charge
Expand Down
7 changes: 5 additions & 2 deletions cobra/io/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@

from __future__ import absolute_import

from cobra.io.json import (load_json_model, save_json_model, to_json,
model_from_dict, model_to_dict)
from cobra.io.dict import (model_from_dict, model_to_dict)
from cobra.io.json import (
to_json, from_json, load_json_model, save_json_model)
from cobra.io.yaml import (
to_yaml, from_yaml, load_yaml_model, save_yaml_model)
from cobra.io.sbml3 import read_sbml_model, write_sbml_model
from cobra.io.sbml import read_legacy_sbml
from cobra.io.sbml import write_cobra_model_to_sbml_file as \
Expand Down
223 changes: 223 additions & 0 deletions cobra/io/dict.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
# -*- coding: utf-8 -*-

from __future__ import absolute_import

from collections import OrderedDict
from operator import attrgetter

from numpy import bool_, float_
from six import iteritems, string_types

from cobra.core import Gene, Metabolite, Model, Reaction
from cobra.util.solver import set_objective

_REQUIRED_REACTION_ATTRIBUTES = [
"id", "name", "metabolites", "lower_bound", "upper_bound",
"gene_reaction_rule"]
_ORDERED_OPTIONAL_REACTION_KEYS = [
"objective_coefficient", "variable_kind", "subsystem", "notes",
"annotation"]
_OPTIONAL_REACTION_ATTRIBUTES = {
"objective_coefficient": 0,
"variable_kind": "continuous",
"subsystem": "",
"notes": {},
"annotation": {},
}

_REQUIRED_METABOLITE_ATTRIBUTES = ["id", "name", "compartment"]
_ORDERED_OPTIONAL_METABOLITE_KEYS = [
"charge", "formula", "_bound", "_constraint_sense", "notes", "annotation"]
_OPTIONAL_METABOLITE_ATTRIBUTES = {
"charge": None,
"formula": None,
"_bound": 0,
"_constraint_sense": "E",
"notes": {},
"annotation": {},
}

_REQUIRED_GENE_ATTRIBUTES = ["id", "name"]
_ORDERED_OPTIONAL_GENE_KEYS = ["notes", "annotation"]
_OPTIONAL_GENE_ATTRIBUTES = {
"notes": {},
"annotation": {},
}

_ORDERED_OPTIONAL_MODEL_KEYS = ["name", "compartments", "notes", "annotation"]
_OPTIONAL_MODEL_ATTRIBUTES = {
"name": None,
# "description": None, should not actually be included
"compartments": [],
"notes": {},
"annotation": {},
}


def _fix_type(value):
"""convert possible types to str, float, and bool"""
# Because numpy floats can not be pickled to json
if isinstance(value, string_types):
return str(value)
if isinstance(value, float_):
return float(value)
if isinstance(value, bool_):
return bool(value)
if isinstance(value, set):
return list(value)
# handle legacy Formula type
if value.__class__.__name__ == "Formula":
return str(value)
if value is None:
return ""
return value


def _update_optional(cobra_object, new_dict, optional_attribute_dict,
ordered_keys):
"""update new_dict with optional attributes from cobra_object"""
for key in ordered_keys:
default = optional_attribute_dict[key]
value = getattr(cobra_object, key)
if value is None or value == default:
continue
new_dict[key] = _fix_type(value)


def metabolite_to_dict(metabolite):
new_met = OrderedDict()
for key in _REQUIRED_METABOLITE_ATTRIBUTES:
new_met[key] = _fix_type(getattr(metabolite, key))
_update_optional(metabolite, new_met, _OPTIONAL_METABOLITE_ATTRIBUTES,
_ORDERED_OPTIONAL_METABOLITE_KEYS)
return new_met


def metabolite_from_dict(metabolite):
new_metabolite = Metabolite()
for k, v in iteritems(metabolite):
setattr(new_metabolite, k, v)
return new_metabolite


def gene_to_dict(gene):
new_gene = OrderedDict()
for key in _REQUIRED_GENE_ATTRIBUTES:
new_gene[key] = _fix_type(getattr(gene, key))
_update_optional(gene, new_gene, _OPTIONAL_GENE_ATTRIBUTES,
_ORDERED_OPTIONAL_GENE_KEYS)
return new_gene


def gene_from_dict(gene):
new_gene = Gene(gene["id"])
for k, v in iteritems(gene):
setattr(new_gene, k, v)
return new_gene


def reaction_to_dict(reaction):
new_reaction = OrderedDict()
for key in _REQUIRED_REACTION_ATTRIBUTES:
if key != "metabolites":
new_reaction[key] = _fix_type(getattr(reaction, key))
continue
mets = OrderedDict()
for met in sorted(reaction.metabolites, key=attrgetter("id")):
mets[str(met)] = reaction.metabolites[met]
new_reaction["metabolites"] = mets
_update_optional(reaction, new_reaction, _OPTIONAL_REACTION_ATTRIBUTES,
_ORDERED_OPTIONAL_REACTION_KEYS)
return new_reaction


def reaction_from_dict(reaction, model):
new_reaction = Reaction()
for k, v in iteritems(reaction):
if k in {'objective_coefficient', 'reversibility', 'reaction'}:
continue
elif k == 'metabolites':
new_reaction.add_metabolites(OrderedDict(
(model.metabolites.get_by_id(str(met)), coeff)
for met, coeff in iteritems(v)))
else:
setattr(new_reaction, k, v)
return new_reaction


def model_to_dict(model):
"""Convert model to a dict.
Parameters
----------
model : cobra.Model
The model to reformulate as a dict
Returns
-------
OrderedDict
A dictionary with elements, 'genes', 'compartments', 'id',
'metabolites', 'notes' and 'reactions'; where 'metabolites', 'genes'
and 'metabolites' are in turn lists with dictionaries holding all
attributes to form the corresponding object.
See Also
--------
cobra.io.model_from_dict
"""
obj = OrderedDict()
obj["reactions"] = [reaction_to_dict(reaction)
for reaction in model.reactions]
obj["metabolites"] = [metabolite_to_dict(metabolite)
for metabolite in model.metabolites]
obj["genes"] = [gene_to_dict(gene)
for gene in model.genes]
obj["id"] = model.id
_update_optional(model, obj, _OPTIONAL_MODEL_ATTRIBUTES,
_ORDERED_OPTIONAL_MODEL_KEYS)
return obj


def model_from_dict(obj):
"""Build a model from a dict.
Models stored in json are first formulated as a dict that can be read to
cobra model using this function.
Parameters
----------
obj : dict
A dictionary with elements, 'genes', 'compartments', 'id',
'metabolites', 'notes' and 'reactions'; where 'metabolites', 'genes'
and 'metabolites' are in turn lists with dictionaries holding all
attributes to form the corresponding object.
Returns
-------
cora.core.Model
The generated model.
See Also
--------
cobra.io.model_to_dict
"""
if 'reactions' not in obj:
raise ValueError('Object has no reactions attribute. Cannot load.')
model = Model()
model.add_metabolites(
[metabolite_from_dict(metabolite) for metabolite in obj['metabolites']]
)
model.genes.extend([gene_from_dict(gene) for gene in obj['genes']])
model.add_reactions(
[reaction_from_dict(reaction, model) for reaction in obj['reactions']]
)
objective_reactions = [rxn for rxn in obj['reactions'] if
rxn.get('objective_coefficient', 0) != 0]
coefficients = {
model.reactions.get_by_id(rxn['id']): rxn['objective_coefficient'] for
rxn in objective_reactions}
set_objective(model, coefficients)
for k, v in iteritems(obj):
if k in {'id', 'name', 'notes', 'compartments', 'annotation'}:
setattr(model, k, v)
return model

0 comments on commit 83fc700

Please sign in to comment.