| 
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 
 |