# -*- coding: utf-8 -*-
from __future__ import absolute_import
import re
from ast import (
And, BitAnd, BitOr, BoolOp, Expression, Name, NodeTransformer, Or)
from ast import parse as ast_parse
from keyword import kwlist
from warnings import warn
from cobra.core.species import Species
from cobra.util import resettable
from cobra.util.util import format_long_string
keywords.remove("and")
keywords.remove("or")
keywords.extend(("True", "False"))
[docs]keyword_re = re.compile(r"(?=\b(%s)\b)" % "|".join(keywords))
[docs]number_start_re = re.compile(r"(?=\b[0-9])")
[docs]replacements = (
(".", "__COBRA_DOT__"),
("'", "__COBRA_SQUOTE__"),
('"', "__COBRA_DQUOTE__"),
(":", "__COBRA_COLON__"),
("/", "__COBRA_FSLASH__"),
("\\", "__COBRA_BSLASH"),
("-", "__COBRA_DASH__"),
("=", "__COBRA_EQ__")
)
# functions for gene reaction rules
[docs]def ast2str(expr, level=0, names=None):
"""convert compiled ast to gene_reaction_rule str
Parameters
----------
expr : str
string for a gene reaction rule, e.g "a and b"
level : int
internal use only
names : dict
Dict where each element id a gene identifier and the value is the
gene name. Use this to get a rule str which uses names instead. This
should be done for display purposes only. All gene_reaction_rule
strings which are computed with should use the id.
Returns
------
string
The gene reaction rule
"""
if isinstance(expr, Expression):
return ast2str(expr.body, 0, names) \
if hasattr(expr, "body") else ""
elif isinstance(expr, Name):
return names.get(expr.id, expr.id) if names else expr.id
elif isinstance(expr, BoolOp):
op = expr.op
if isinstance(op, Or):
str_exp = " or ".join(ast2str(i, level + 1, names)
for i in expr.values)
elif isinstance(op, And):
str_exp = " and ".join(ast2str(i, level + 1, names)
for i in expr.values)
else:
raise TypeError("unsupported operation " + op.__class__.__name)
return "(" + str_exp + ")" if level else str_exp
elif expr is None:
return ""
else:
raise TypeError("unsupported operation " + repr(expr))
[docs]def eval_gpr(expr, knockouts):
"""evaluate compiled ast of gene_reaction_rule with knockouts
Parameters
----------
expr : Expression
The ast of the gene reaction rule
knockouts : DictList, set
Set of genes that are knocked out
Returns
-------
bool
True if the gene reaction rule is true with the given knockouts
otherwise false
"""
if isinstance(expr, Expression):
return eval_gpr(expr.body, knockouts)
elif isinstance(expr, Name):
return expr.id not in knockouts
elif isinstance(expr, BoolOp):
op = expr.op
if isinstance(op, Or):
return any(eval_gpr(i, knockouts) for i in expr.values)
elif isinstance(op, And):
return all(eval_gpr(i, knockouts) for i in expr.values)
else:
raise TypeError("unsupported operation " + op.__class__.__name__)
elif expr is None:
return True
else:
raise TypeError("unsupported operation " + repr(expr))
[docs]class GPRCleaner(NodeTransformer):
"""Parses compiled ast of a gene_reaction_rule and identifies genes
Parts of the tree are rewritten to allow periods in gene ID's and
bitwise boolean operations"""
def __init__(self):
NodeTransformer.__init__(self)
self.gene_set = set()
[docs] def visit_Name(self, node):
if node.id.startswith("__cobra_escape__"):
node.id = node.id[16:]
for char, escaped in replacements:
if escaped in node.id:
node.id = node.id.replace(escaped, char)
self.gene_set.add(node.id)
return node
[docs] def visit_BinOp(self, node):
self.generic_visit(node)
if isinstance(node.op, BitAnd):
return BoolOp(And(), (node.left, node.right))
elif isinstance(node.op, BitOr):
return BoolOp(Or(), (node.left, node.right))
else:
raise TypeError("unsupported operation '%s'" %
node.op.__class__.__name__)
[docs]def parse_gpr(str_expr):
"""parse gpr into AST
Parameters
----------
str_expr : string
string with the gene reaction rule to parse
Returns
-------
tuple
elements ast_tree and gene_ids as a set
"""
str_expr = str_expr.strip()
if len(str_expr) == 0:
return None, set()
for char, escaped in replacements:
if char in str_expr:
str_expr = str_expr.replace(char, escaped)
escaped_str = keyword_re.sub("__cobra_escape__", str_expr)
escaped_str = number_start_re.sub("__cobra_escape__", escaped_str)
tree = ast_parse(escaped_str, "<string>", "eval")
cleaner = GPRCleaner()
cleaner.visit(tree)
eval_gpr(tree, set()) # ensure the rule can be evaluated
return tree, cleaner.gene_set
[docs]class Gene(Species):
"""A Gene in a cobra model
Parameters
----------
id : string
The identifier to associate the gene with
name: string
A longer human readable name for the gene
functional: bool
Indicates whether the gene is functional. If it is not functional
then it cannot be used in an enzyme complex nor can its products be
used.
"""
def __init__(self, id=None, name="", functional=True):
Species.__init__(self, id=id, name=name)
self._functional = functional
@property
[docs] def functional(self):
"""A flag indicating if the gene is functional.
Changing the flag is reverted upon exit if executed within the model
as context.
"""
return self._functional
@functional.setter
@resettable
def functional(self, value):
if not isinstance(value, bool):
raise ValueError('expected boolean')
self._functional = value
[docs] def knock_out(self):
"""Knockout gene by marking it as non-functional and setting all
associated reactions bounds to zero.
The change is reverted upon exit if executed within the model as
context.
"""
self.functional = False
for reaction in self.reactions:
if not reaction.functional:
reaction.bounds = (0, 0)
[docs] def remove_from_model(self, model=None,
make_dependent_reactions_nonfunctional=True):
"""Removes the association
Parameters
----------
model : cobra model
The model to remove the gene from
make_dependent_reactions_nonfunctional : bool
If True then replace the gene with 'False' in the gene
association, else replace the gene with 'True'
.. deprecated :: 0.4
Use cobra.manipulation.delete_model_genes to simulate knockouts
and cobra.manipulation.remove_genes to remove genes from
the model.
"""
warn("Use cobra.manipulation.remove_genes instead")
if model is not None:
if model != self._model:
raise Exception("%s is a member of %s, not %s" %
(repr(self), repr(self._model), repr(model)))
if self._model is None:
raise Exception('%s is not in a model' % repr(self))
if make_dependent_reactions_nonfunctional:
gene_state = 'False'
else:
gene_state = 'True'
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
for the_reaction in list(self._reaction):
the_reaction._gene_reaction_rule = the_gene_re.sub(
gene_state, the_reaction.gene_reaction_rule)
the_reaction._genes.remove(self)
# Now, deactivate the reaction if its gene association evaluates
# to False
the_gene_reaction_relation = the_reaction.gene_reaction_rule
for other_gene in the_reaction._genes:
other_gene_re = re.compile('(^|(?<=( |\()))%s(?=( |\)|$))' %
re.escape(other_gene.id))
the_gene_reaction_relation = other_gene_re.sub(
'True',
the_gene_reaction_relation)
if not eval(the_gene_reaction_relation):
the_reaction.lower_bound = 0
the_reaction.upper_bound = 0
self._reaction.clear()
[docs] def _repr_html_(self):
return """
<table>
<tr>
<td><strong>Gene identifier</strong></td><td>{id}</td>
</tr><tr>
<td><strong>Name</strong></td><td>{name}</td>
</tr><tr>
<td><strong>Memory address</strong></td>
<td>{address}</td>
</tr><tr>
<td><strong>Functional</strong></td><td>{functional}</td>
</tr><tr>
<td><strong>In {n_reactions} reaction(s)</strong></td><td>
{reactions}</td>
</tr>
</table>""".format(id=self.id, name=self.name,
functional=self.functional,
address='0x0%x' % id(self),
n_reactions=len(self.reactions),
reactions=format_long_string(
', '.join(r.id for r in self.reactions), 200))