| 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   """ | 
| 381 | 127   reactionsData :Dict[str, str] = {row[0]: row[1] for row in utils.readCsv(utils.FilePath.fromStrPath(customReactionsPath), delimiter = "\t")} | 
| 4 | 128   return create_reaction_dict(reactionsData) | 
|  | 129 |