|
539
|
1 """
|
|
|
2 Helpers to parse reaction strings into structured dictionaries.
|
|
|
3
|
|
|
4 Features:
|
|
|
5 - Reaction direction detection (forward, backward, reversible)
|
|
|
6 - Parsing of custom reaction strings into stoichiometric maps
|
|
|
7 - Conversion of a dict of raw reactions into a directional reactions dict
|
|
|
8 - Loading custom reactions from a tabular file (TSV)
|
|
|
9 """
|
|
|
10 from enum import Enum
|
|
|
11 from typing import Dict
|
|
|
12 import re
|
|
|
13
|
|
542
|
14 try:
|
|
|
15 from . import general_utils as utils
|
|
|
16 except:
|
|
|
17 import general_utils as utils
|
|
|
18
|
|
539
|
19 # Reaction direction encoding:
|
|
|
20 class ReactionDir(Enum):
|
|
|
21 """
|
|
|
22 A reaction can go forward, backward, or be reversible (both directions).
|
|
|
23 Cobrapy-style formulas encode direction using specific arrows handled here.
|
|
|
24 """
|
|
|
25 FORWARD = "-->"
|
|
|
26 BACKWARD = "<--"
|
|
|
27 REVERSIBLE = "<=>"
|
|
|
28
|
|
|
29 @classmethod
|
|
|
30 def fromReaction(cls, reaction :str) -> 'ReactionDir':
|
|
|
31 """
|
|
|
32 Takes a whole reaction formula string and looks for one of the arrows, returning the
|
|
|
33 corresponding reaction direction.
|
|
|
34
|
|
|
35 Args:
|
|
|
36 reaction : the reaction's formula.
|
|
|
37
|
|
|
38 Raises:
|
|
|
39 ValueError : if no valid arrow is found.
|
|
|
40
|
|
|
41 Returns:
|
|
|
42 ReactionDir : the corresponding reaction direction.
|
|
|
43 """
|
|
|
44 for member in cls:
|
|
|
45 if member.value in reaction: return member
|
|
|
46
|
|
|
47 raise ValueError("No valid arrow found within reaction string.")
|
|
|
48
|
|
|
49 ReactionsDict = Dict[str, Dict[str, float]]
|
|
|
50
|
|
|
51
|
|
|
52 def add_custom_reaction(reactionsDict :ReactionsDict, rId :str, reaction :str) -> None:
|
|
|
53 """
|
|
|
54 Add one reaction entry to reactionsDict.
|
|
|
55
|
|
|
56 The entry maps each substrate ID to its stoichiometric coefficient.
|
|
|
57 If a substrate appears without an explicit coefficient, 1.0 is assumed.
|
|
|
58
|
|
|
59 Args:
|
|
|
60 reactionsDict: Dict to update in place.
|
|
|
61 rId: Unique reaction ID.
|
|
|
62 reaction: Reaction formula string.
|
|
|
63
|
|
|
64 Returns:
|
|
|
65 None
|
|
|
66
|
|
|
67 Side effects: updates reactionsDict in place.
|
|
|
68 """
|
|
|
69 reaction = reaction.strip()
|
|
|
70 if not reaction: return
|
|
|
71
|
|
|
72 reactionsDict[rId] = {}
|
|
|
73 # Assumes ' + ' is spaced to avoid confusion with charge symbols.
|
|
|
74 for word in reaction.split(" + "):
|
|
|
75 metabId, stoichCoeff = word, 1.0
|
|
|
76 # Coefficient can be integer or float (dot decimal) and must be space-separated.
|
|
|
77 foundCoeff = re.search(r"\d+(\.\d+)? ", word)
|
|
|
78 if foundCoeff:
|
|
|
79 wholeMatch = foundCoeff.group(0)
|
|
|
80 metabId = word[len(wholeMatch):].strip()
|
|
|
81 stoichCoeff = float(wholeMatch.strip())
|
|
|
82
|
|
|
83 reactionsDict[rId][metabId] = stoichCoeff
|
|
|
84
|
|
|
85 if not reactionsDict[rId]: del reactionsDict[rId] # Empty reactions are removed.
|
|
|
86
|
|
|
87
|
|
|
88 def create_reaction_dict(unparsed_reactions: Dict[str, str]) -> ReactionsDict:
|
|
|
89 """
|
|
|
90 Parse a dict of raw reaction strings into a directional reactions dict.
|
|
|
91
|
|
|
92 Args:
|
|
|
93 unparsed_reactions: Mapping reaction ID -> raw reaction string.
|
|
|
94
|
|
|
95 Returns:
|
|
|
96 ReactionsDict: Parsed dict. Reversible reactions produce two entries with _F and _B suffixes.
|
|
|
97 """
|
|
|
98 reactionsDict :ReactionsDict = {}
|
|
|
99 for rId, reaction in unparsed_reactions.items():
|
|
|
100 reactionDir = ReactionDir.fromReaction(reaction)
|
|
|
101 left, right = reaction.split(f" {reactionDir.value} ")
|
|
|
102
|
|
|
103 # Reversible reactions are split into two: forward (_F) and backward (_B).
|
|
|
104 reactionIsReversible = reactionDir is ReactionDir.REVERSIBLE
|
|
|
105 if reactionDir is not ReactionDir.BACKWARD:
|
|
|
106 add_custom_reaction(reactionsDict, rId + "_F" * reactionIsReversible, left)
|
|
|
107
|
|
|
108 if reactionDir is not ReactionDir.FORWARD:
|
|
|
109 add_custom_reaction(reactionsDict, rId + "_B" * reactionIsReversible, right)
|
|
|
110
|
|
|
111 return reactionsDict
|
|
|
112
|
|
|
113
|
|
|
114 def parse_custom_reactions(customReactionsPath :str) -> ReactionsDict:
|
|
|
115 """
|
|
|
116 Load custom reactions from a tabular file and parse into a reactions dict.
|
|
|
117
|
|
|
118 Args:
|
|
|
119 customReactionsPath: Path to the reactions file (TSV or CSV-like).
|
|
|
120
|
|
|
121 Returns:
|
|
|
122 ReactionsDict: Parsed reactions dictionary.
|
|
|
123 """
|
|
|
124 try:
|
|
|
125 rows = utils.readCsv(utils.FilePath.fromStrPath(customReactionsPath), delimiter = "\t", skipHeader=False)
|
|
|
126 if len(rows) <= 1:
|
|
|
127 raise ValueError("The custom reactions file must contain at least one reaction.")
|
|
|
128
|
|
|
129 id_idx, idx_formula = utils.findIdxByName(rows[0], "Formula")
|
|
|
130
|
|
|
131 except Exception as e:
|
|
|
132 # Fallback re-read with same settings; preserves original behavior
|
|
|
133 rows = utils.readCsv(utils.FilePath.fromStrPath(customReactionsPath), delimiter = "\t", skipHeader=False)
|
|
|
134 if len(rows) <= 1:
|
|
|
135 raise ValueError("The custom reactions file must contain at least one reaction.")
|
|
|
136
|
|
|
137 id_idx, idx_formula = utils.findIdxByName(rows[0], "Formula")
|
|
|
138
|
|
|
139 reactionsData = {row[id_idx] : row[idx_formula] for row in rows[1:]}
|
|
|
140
|
|
|
141 return create_reaction_dict(reactionsData)
|
|
|
142
|