4
|
1 from enum import Enum
|
|
2 import utils.general_utils as utils
|
|
3 from typing import Dict
|
|
4 import csv
|
|
5 import re
|
|
6
|
|
7 # Reaction direction encoding:
|
|
8 class ReactionDir(Enum):
|
|
9 """
|
|
10 A reaction can go forwards, backwards or be reversible (able to proceed in both directions).
|
|
11 Models created / managed with cobrapy encode this information within the reaction's
|
|
12 formula using the arrows this enum keeps as values.
|
|
13 """
|
|
14 FORWARD = "-->"
|
|
15 BACKWARD = "<--"
|
|
16 REVERSIBLE = "<=>"
|
|
17
|
|
18 @classmethod
|
|
19 def fromReaction(cls, reaction :str) -> 'ReactionDir':
|
|
20 """
|
|
21 Takes a whole reaction formula string and looks for one of the arrows, returning the
|
|
22 corresponding reaction direction.
|
|
23
|
|
24 Args:
|
|
25 reaction : the reaction's formula.
|
|
26
|
|
27 Raises:
|
|
28 ValueError : if no valid arrow is found.
|
|
29
|
|
30 Returns:
|
|
31 ReactionDir : the corresponding reaction direction.
|
|
32 """
|
|
33 for member in cls:
|
|
34 if member.value in reaction: return member
|
|
35
|
|
36 raise ValueError("No valid arrow found within reaction string.")
|
|
37
|
|
38 ReactionsDict = Dict[str, Dict[str, float]]
|
|
39
|
|
40
|
|
41 def add_custom_reaction(reactionsDict :ReactionsDict, rId :str, reaction :str) -> None:
|
|
42 """
|
|
43 Adds an entry to the given reactionsDict. Each entry consists of a given unique reaction id
|
|
44 (key) and a :dict (value) matching each substrate in the reaction to its stoichiometric coefficient.
|
|
45 Keys and values are both obtained from the reaction's formula: if a substrate (custom metabolite id)
|
|
46 appears without an explicit coeff, the value 1.0 will be used instead.
|
|
47
|
|
48 Args:
|
|
49 reactionsDict : dictionary encoding custom reactions information.
|
|
50 rId : unique reaction id.
|
|
51 reaction : the reaction's formula.
|
|
52
|
|
53 Returns:
|
|
54 None
|
|
55
|
|
56 Side effects:
|
|
57 reactionsDict : mut
|
|
58 """
|
|
59 reaction = reaction.strip()
|
|
60 if not reaction: return
|
|
61
|
|
62 reactionsDict[rId] = {}
|
|
63 # We assume the '+' separating consecutive metabs in a reaction is spaced from them,
|
|
64 # to avoid confusing it for electrical charge:
|
|
65 for word in reaction.split(" + "):
|
|
66 metabId, stoichCoeff = word, 1.0
|
|
67 # Implicit stoichiometric coeff is equal to 1, some coeffs are floats.
|
|
68
|
|
69 # Accepted coeffs can be integer or floats with a dot (.) decimal separator
|
|
70 # and must be separated from the metab with a space:
|
|
71 foundCoeff = re.search(r"\d+(\.\d+)? ", word)
|
|
72 if foundCoeff:
|
|
73 wholeMatch = foundCoeff.group(0)
|
|
74 metabId = word[len(wholeMatch):].strip()
|
|
75 stoichCoeff = float(wholeMatch.strip())
|
|
76
|
|
77 reactionsDict[rId][metabId] = stoichCoeff
|
|
78
|
|
79 if not reactionsDict[rId]: del reactionsDict[rId] # Empty reactions are removed.
|
|
80
|
|
81
|
|
82 def create_reaction_dict(unparsed_reactions: Dict[str, str]) -> ReactionsDict:
|
|
83 """
|
|
84 Parses the given dictionary into the correct format.
|
|
85
|
|
86 Args:
|
|
87 unparsed_reactions (Dict[str, str]): A dictionary where keys are reaction IDs and values are unparsed reaction strings.
|
|
88
|
|
89 Returns:
|
|
90 ReactionsDict: The correctly parsed dict.
|
|
91 """
|
|
92 reactionsDict :ReactionsDict = {}
|
|
93 for rId, reaction in unparsed_reactions.items():
|
|
94 reactionDir = ReactionDir.fromReaction(reaction)
|
|
95 left, right = reaction.split(f" {reactionDir.value} ")
|
|
96
|
|
97 # Reversible reactions are split into distinct reactions, one for each direction.
|
|
98 # In general we only care about substrates, the product information is lost.
|
|
99 reactionIsReversible = reactionDir is ReactionDir.REVERSIBLE
|
|
100 if reactionDir is not ReactionDir.BACKWARD:
|
|
101 add_custom_reaction(reactionsDict, rId + "_F" * reactionIsReversible, left)
|
|
102
|
|
103 if reactionDir is not ReactionDir.FORWARD:
|
|
104 add_custom_reaction(reactionsDict, rId + "_B" * reactionIsReversible, right)
|
|
105
|
|
106 # ^^^ to further clarify: if a reaction is NOT reversible it will not be marked as _F or _B
|
|
107 # and whichever direction we DO keep (forward if --> and backward if <--) loses this information.
|
|
108 # This IS a small problem when coloring the map in marea.py because the arrow IDs in the map follow
|
|
109 # through with a similar convention on ALL reactions and correctly encode direction based on their
|
|
110 # model of origin. TODO: a proposed solution is to unify the standard in RPS to fully mimic the maps,
|
|
111 # which involves re-writing the "reactions" dictionary.
|
|
112
|
|
113 return reactionsDict
|
|
114
|
|
115
|
|
116 def parse_custom_reactions(customReactionsPath :str) -> ReactionsDict:
|
|
117 """
|
|
118 Creates a custom dictionary encoding reactions information from a csv file containing
|
|
119 data about these reactions, the path of which is given as input.
|
|
120
|
|
121 Args:
|
|
122 customReactionsPath : path to the reactions information file.
|
|
123
|
|
124 Returns:
|
|
125 ReactionsDict : dictionary encoding custom reactions information.
|
|
126 """
|
|
127 reactionsData :Dict[str, str] = {row[0]: row[1] for row in utils.readCsv(utils.FilePath.fromStrPath(customReactionsPath))}
|
|
128
|
|
129 return create_reaction_dict(reactionsData)
|
|
130
|