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