Skip to content

Commit 783f5c0

Browse files
committed
fix: production envelope with unspecified c-source
Fix bug in production_envelopee occurring for models where the C-source was formulated as -> x instead of x <- Also support guessing the C-source reaction.
1 parent 7109843 commit 783f5c0

File tree

3 files changed

+116
-62
lines changed

3 files changed

+116
-62
lines changed

cobra/flux_analysis/phenotype_phase_plane.py

Lines changed: 105 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
# -*- coding: utf-8 -*-
22

3-
from __future__ import absolute_import
3+
from __future__ import absolute_import, division
44

55
from warnings import warn
66
from collections import defaultdict
7+
from operator import itemgetter
78
from itertools import product
9+
from six import iteritems
810
from multiprocessing import Pool
911
import pandas as pd
1012
from optlang.interface import OPTIMAL
@@ -16,6 +18,7 @@
1618
from cobra.solvers import get_solver_name, solver_dict
1719
import cobra.util.solver as sutil
1820
from cobra.flux_analysis import flux_variability_analysis as fva
21+
from cobra.exceptions import OptimizationError
1922

2023
# attempt to import plotting libraries
2124
try:
@@ -351,7 +354,7 @@ def production_envelope(model, reactions, objective=None, c_source=None,
351354
- carbon_yield: if carbon source is defined and the product is a
352355
single metabolite (mol carbon product per mol carbon feeding source)
353356
354-
- _mass_yield: if carbon source is defined and the product is a
357+
- mass_yield: if carbon source is defined and the product is a
355358
single metabolite (gram product per 1 g of feeding source)
356359
357360
- direction: the direction of the optimization.
@@ -371,36 +374,28 @@ def production_envelope(model, reactions, objective=None, c_source=None,
371374
'based solver interfaces.')
372375
reactions = model.reactions.get_by_any(reactions)
373376
objective = model.solver.objective if objective is None else objective
377+
result = None
374378
with model:
375379
model.objective = objective
376-
carbon_io = _c_input_output(model, c_source)
380+
if c_source is None:
381+
c_input = get_c_input(model)
382+
else:
383+
c_input = model.reactions.get_by_any(c_source)[0]
384+
objective_reactions = list(sutil.linear_reaction_coefficients(model))
385+
if len(objective_reactions) != 1:
386+
raise ValueError('cannot calculate yields for objectives with '
387+
'multiple reactions')
388+
carbon_io = c_input, objective_reactions[0]
377389
min_max = fva(model, reactions, fraction_of_optimum=0)
378390
grid = [linspace(min_max.minimum[rxn.id], min_max.maximum[rxn.id],
379391
points, endpoint=True) for rxn in reactions]
380392
grid_list = list(product(*grid))
381-
result = _envelope_for_points(model, reactions, grid_list, carbon_io)
393+
result = envelope_for_points(model, reactions, grid_list, carbon_io)
382394

383395
return pd.DataFrame(result)
384396

385397

386-
def _c_input_output(model, c_source=None):
387-
if c_source is None:
388-
return None, None
389-
c_input = _carbon_reaction(model.reactions.get_by_any(c_source))
390-
objective_coefficients = sutil.linear_reaction_coefficients(model)
391-
c_output = _carbon_reaction(list(objective_coefficients))
392-
return c_input, c_output
393-
394-
395-
def _carbon_reaction(reactions):
396-
exchanges = [rxn for rxn in reactions if len(rxn.reactants) == 1 and
397-
rxn.reactants[0].elements.get('C', 0) > 1]
398-
if len(exchanges) != 1:
399-
raise ValueError('cannot calculate yields for %s' % reactions)
400-
return exchanges[0]
401-
402-
403-
def _envelope_for_points(model, reactions, grid, carbon_io):
398+
def envelope_for_points(model, reactions, grid, carbon_io):
404399
results = defaultdict(list)
405400
for direction in ('minimum', 'maximum'):
406401
sense = "min" if direction == "minimum" else "max"
@@ -416,39 +411,40 @@ def _envelope_for_points(model, reactions, grid, carbon_io):
416411
results['direction'].append(direction)
417412
results['flux'].append(model.solver.objective.value)
418413
if carbon_io[0] is not None:
419-
results['carbon_yield'].append(
420-
_carbon_yield(carbon_io))
421-
results['mass_yield'].append(_mass_yield(carbon_io))
414+
results['carbon_yield'].append(carbon_yield(carbon_io))
415+
results['mass_yield'].append(mass_yield(carbon_io))
422416
for key, value in results.items():
423417
results[key] = array(value)
424418
if carbon_io[0] is not None:
425419
results['carbon_source'] = carbon_io[0].id
426420
return results
427421

428422

429-
def _carbon_flux(reaction):
430-
"""Carbon flux for a reaction
431-
432-
Parameters
433-
----------
434-
reaction : cobra.Reaction
435-
the reaction to carbon return flux for
423+
def carbon_yield(c_input_output):
424+
""" mol product per mol carbon input
436425
437426
Returns
438427
-------
439428
float
440-
reaction flux multiplied by number of carbon in reactants"""
441-
carbon = sum(metabolite.elements.get('C', 0) for
442-
metabolite in reaction.reactants)
429+
the mol carbon atoms in the product (as defined by the model
430+
objective) divided by the mol carbon in the input reactions (as
431+
defined by the model medium) or zero in case of division by zero
432+
arises
433+
"""
443434

435+
c_input, c_output = c_input_output
436+
if c_input is None:
437+
return nan
438+
carbon_input_flux = total_carbon_flux(c_input, consumption=True)
439+
carbon_output_flux = total_carbon_flux(c_output, consumption=False)
444440
try:
445-
return reaction.flux * carbon
446-
except AssertionError:
441+
return carbon_output_flux / carbon_input_flux
442+
except ZeroDivisionError:
447443
return nan
448444

449445

450-
def _carbon_yield(c_input_output):
451-
"""Mol product per mol carbon input
446+
def mass_yield(c_input_output):
447+
"""Gram product divided by gram of carbon input source
452448
453449
Parameters
454450
----------
@@ -459,39 +455,88 @@ def _carbon_yield(c_input_output):
459455
Returns
460456
-------
461457
float
462-
the mol carbon atoms in the product (as defined by the model
463-
objective) divided by the mol carbon in the input reactions (as
464-
defined by the model medium) or zero in case of division by zero
465-
arises
458+
gram product per 1 g of feeding source
466459
"""
467460
c_input, c_output = c_input_output
468-
carbon_input_flux = _carbon_flux(c_input)
469-
carbon_output_flux = _carbon_flux(c_output)
461+
if input is None:
462+
return nan
470463
try:
471-
return carbon_output_flux / (carbon_input_flux * -1)
472-
except ZeroDivisionError:
464+
c_source, source_flux = single_flux(c_input, consumption=True)
465+
c_product, product_flux = single_flux(c_output, consumption=False)
466+
except ValueError:
473467
return nan
468+
mol_prod_mol_src = product_flux / source_flux
469+
x = mol_prod_mol_src * c_product.formula_weight
470+
return x / c_source.formula_weight
474471

475472

476-
def _mass_yield(c_input_output):
477-
"""Gram product divided by gram of carbon input source
473+
def total_carbon_flux(reaction, consumption=True):
474+
"""summed product carbon flux for a reaction
478475
479476
Parameters
480477
----------
481-
c_input_output : tuple
482-
Two reactions, the one that feeds carbon to the system and the one
483-
that produces carbon containing compound.
478+
reaction : Reaction
479+
the reaction to carbon return flux for
480+
consumption : bool
481+
flux for consumed metabolite, else produced
484482
485483
Returns
486484
-------
487485
float
488-
gram product per 1 g of feeding source
486+
reaction flux multiplied by number of carbon for the products of the
487+
reaction
488+
"""
489+
direction = 1 if consumption else -1
490+
c_flux = [reaction.flux * coeff * met.elements.get('C', 0) * direction
491+
for met, coeff in reaction.metabolites.items()]
492+
return sum([flux for flux in c_flux if flux > 0])
493+
494+
495+
def single_flux(reaction, consumption=True):
496+
"""flux into single product for a reaction
497+
498+
only defined for reactions with single products
499+
500+
Parameters
501+
----------
502+
reaction : Reaction
503+
the reaction to product flux for
504+
consumption : bool
505+
flux for consumed metabolite, else produced
506+
507+
Returns
508+
-------
509+
tuple
510+
metabolite, flux for the metabolite
511+
"""
512+
if len(list(reaction.metabolites)) != 1:
513+
raise ValueError('product flux only defined for single metabolite '
514+
'reactions')
515+
met, coeff = next(iteritems(reaction.metabolites))
516+
direction = 1 if consumption else -1
517+
return met, reaction.flux * coeff * direction
518+
519+
520+
def get_c_input(model):
521+
""" carbon source reactions
522+
523+
Returns
524+
-------
525+
Reaction
526+
The medium reaction with highest input carbon flux
489527
"""
490-
c_input, c_output = c_input_output
491-
source_mass = sum(met.formula_weight for met in c_input.reactants)
492-
product_mass = sum(met.formula_weight for met in c_output.reactants)
493528
try:
494-
mol_prod_mol_src = c_output.flux / (c_input.flux * -1)
495-
except AssertionError:
496-
return nan
497-
return (mol_prod_mol_src * product_mass) / source_mass
529+
model.solver.optimize()
530+
sutil.assert_optimal(model)
531+
except OptimizationError:
532+
return None
533+
534+
reactions = model.reactions.get_by_any(list(model.medium))
535+
reactions_fluxes = [(rxn, total_carbon_flux(rxn, consumption=True))
536+
for rxn in reactions]
537+
source_reactions = [(rxn, c_flux) for rxn, c_flux
538+
in reactions_fluxes if c_flux > 0]
539+
try:
540+
return max(source_reactions, key=itemgetter(1))[0]
541+
except ValueError:
542+
return None

cobra/test/test_flux_analysis.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -699,10 +699,15 @@ def test_envelope_one(self, model):
699699
df = production_envelope(model, ["EX_o2_e"])
700700
assert abs(sum(df.flux) - 9.342) < 0.001
701701

702+
def test_envelope_multi_reaction_objective(self, model):
703+
obj = {model.reactions.EX_ac_e: 1,
704+
model.reactions.EX_co2_e: 1}
705+
with pytest.raises(ValueError):
706+
production_envelope(model, "EX_o2_e", obj)
707+
702708
def test_envelope_two(self, model):
703709
df = production_envelope(model, ["EX_glc__D_e", "EX_o2_e"],
704-
objective="EX_ac_e",
705-
c_source="EX_glc__D_e")
710+
objective="EX_ac_e")
706711
assert abs(numpy.sum(df.carbon_yield) - 83.579) < 0.001
707712
assert abs(numpy.sum(df.flux) - 1737.466) < 0.001
708713
assert abs(numpy.sum(df.mass_yield) - 82.176) < 0.001

release-notes/next-release.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
[was broken](https://github.com/opencobra/cobrapy/issues/537)
77
following the release of 0.6.0 and has now been fixed (and now with
88
unit tests).
9+
- `production_envelope` failed when model C-source was formulated as
10+
-> x instead of x <-. Fixed added option to guess the C-source by
11+
taking the medium reaction with the highest input C flux.
12+
- `model_to_pymatbridge` needs scipy and that's correctly handled now.
913

1014
## New features
1115
- `flux_variability_analysis` now has the `pfba_factor` parameter

0 commit comments

Comments
 (0)