pyreactlab-core 0.1.8__tar.gz → 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. {pyreactlab_core-0.1.8 → pyreactlab_core-0.2.0}/PKG-INFO +1 -1
  2. {pyreactlab_core-0.1.8 → pyreactlab_core-0.2.0}/pyproject.toml +1 -1
  3. {pyreactlab_core-0.1.8 → pyreactlab_core-0.2.0}/pyreactlab_core/__init__.py +11 -1
  4. pyreactlab_core-0.2.0/pyreactlab_core/configs/constants.py +95 -0
  5. {pyreactlab_core-0.1.8 → pyreactlab_core-0.2.0}/pyreactlab_core/configs/info.py +1 -1
  6. pyreactlab_core-0.2.0/pyreactlab_core/core/__init__.py +23 -0
  7. pyreactlab_core-0.2.0/pyreactlab_core/core/chem_react.py +421 -0
  8. pyreactlab_core-0.2.0/pyreactlab_core/core/chem_react_utils.py +181 -0
  9. pyreactlab_core-0.2.0/pyreactlab_core/core/reaction_component_mapper.py +206 -0
  10. pyreactlab_core-0.2.0/pyreactlab_core/core/reaction_network_analysis.py +291 -0
  11. {pyreactlab_core-0.1.8 → pyreactlab_core-0.2.0}/pyreactlab_core/models/reaction.py +24 -15
  12. pyreactlab_core-0.2.0/pyreactlab_core/utils/reaction_tools.py +153 -0
  13. {pyreactlab_core-0.1.8 → pyreactlab_core-0.2.0}/pyreactlab_core.egg-info/PKG-INFO +1 -1
  14. {pyreactlab_core-0.1.8 → pyreactlab_core-0.2.0}/pyreactlab_core.egg-info/SOURCES.txt +4 -0
  15. pyreactlab_core-0.1.8/pyreactlab_core/configs/constants.py +0 -40
  16. pyreactlab_core-0.1.8/pyreactlab_core/core/__init__.py +0 -17
  17. pyreactlab_core-0.1.8/pyreactlab_core/core/chem_react.py +0 -1122
  18. {pyreactlab_core-0.1.8 → pyreactlab_core-0.2.0}/LICENSE +0 -0
  19. {pyreactlab_core-0.1.8 → pyreactlab_core-0.2.0}/README.md +0 -0
  20. {pyreactlab_core-0.1.8 → pyreactlab_core-0.2.0}/pyreactlab_core/app.py +0 -0
  21. {pyreactlab_core-0.1.8 → pyreactlab_core-0.2.0}/pyreactlab_core/configs/__init__.py +0 -0
  22. {pyreactlab_core-0.1.8 → pyreactlab_core-0.2.0}/pyreactlab_core/docs/__init__.py +0 -0
  23. {pyreactlab_core-0.1.8 → pyreactlab_core-0.2.0}/pyreactlab_core/docs/chem_balance.py +0 -0
  24. {pyreactlab_core-0.1.8 → pyreactlab_core-0.2.0}/pyreactlab_core/docs/chem_utils.py +0 -0
  25. {pyreactlab_core-0.1.8 → pyreactlab_core-0.2.0}/pyreactlab_core/models/__init__.py +0 -0
  26. {pyreactlab_core-0.1.8 → pyreactlab_core-0.2.0}/pyreactlab_core/utils/__init__.py +0 -0
  27. {pyreactlab_core-0.1.8 → pyreactlab_core-0.2.0}/pyreactlab_core/utils/component_tools.py +0 -0
  28. {pyreactlab_core-0.1.8 → pyreactlab_core-0.2.0}/pyreactlab_core/utils/tools.py +0 -0
  29. {pyreactlab_core-0.1.8 → pyreactlab_core-0.2.0}/pyreactlab_core.egg-info/dependency_links.txt +0 -0
  30. {pyreactlab_core-0.1.8 → pyreactlab_core-0.2.0}/pyreactlab_core.egg-info/requires.txt +0 -0
  31. {pyreactlab_core-0.1.8 → pyreactlab_core-0.2.0}/pyreactlab_core.egg-info/top_level.txt +0 -0
  32. {pyreactlab_core-0.1.8 → pyreactlab_core-0.2.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyreactlab-core
3
- Version: 0.1.8
3
+ Version: 0.2.0
4
4
  Summary: pyreactlab-core is the core foundation of the PyReactLab ecosystem, offering shared data structures and algorithms for chemical reaction representation, stoichiometry, and reaction analysis.
5
5
  Author-email: Sina Gilassi <sina.gilassi@gmail.com>
6
6
  License-Expression: Apache-2.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pyreactlab-core"
7
- version = "0.1.8"
7
+ version = "0.2.0"
8
8
  description = "pyreactlab-core is the core foundation of the PyReactLab ecosystem, offering shared data structures and algorithms for chemical reaction representation, stoichiometry, and reaction analysis."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -1,4 +1,4 @@
1
- # config
1
+ # NOTE: config
2
2
  from .configs.info import (
3
3
  __version__,
4
4
  __author__,
@@ -8,6 +8,13 @@ from .configs.info import (
8
8
  __license__,
9
9
  )
10
10
 
11
+ # NOTE: configs
12
+ from .configs.constants import (
13
+ REACTION_SYMBOLIC_MODES,
14
+ ReactionMode
15
+ )
16
+
17
+ # NOTE: app
11
18
  from .app import (
12
19
  rxn,
13
20
  rxn_stoichiometry_matrix,
@@ -23,6 +30,9 @@ __all__ = [
23
30
  "__description__",
24
31
  "__email__",
25
32
  "__license__",
33
+ # configs
34
+ "REACTION_SYMBOLIC_MODES",
35
+ "ReactionMode",
26
36
  # app
27
37
  "rxn",
28
38
  "rxn_stoichiometry_matrix",
@@ -0,0 +1,95 @@
1
+ # import libs
2
+ from __future__ import annotations
3
+ from typing import Literal
4
+
5
+ # SECTION: PyThermoDBLink/PyThermoDB
6
+ import math
7
+ DATASOURCE = "datasource"
8
+ EQUATIONSOURCE = "equationsource"
9
+
10
+ # NOTE: universal gas constant [J/mol.K]
11
+ R_CONST_J__molK = 8.314472
12
+
13
+ # NOTE: pi
14
+ PI_CONST = math.pi
15
+
16
+ # NOTE: STP condition
17
+ # pressure [Pa]
18
+ PRESSURE_STP_Pa = 101325
19
+ # temperature [K]
20
+ TEMPERATURE_STP_K = 273.15
21
+ # reference pressure [Pa]
22
+ PRESSURE_REF_Pa = 101325
23
+ # reference temperature [K]
24
+ TEMPERATURE_REF_K = 298.15
25
+
26
+ # SECTION: Periodic Table Elements
27
+ PERIODIC_TABLE_ELEMENTS = [
28
+ "H", "He", "Li", "Be", "B", "C", "N", "O", "F", "Ne",
29
+ "Na", "Mg", "Al", "Si", "P", "S", "Cl", "Ar",
30
+ "K", "Ca", "Sc", "Ti", "V", "Cr", "Mn", "Fe", "Co", "Ni",
31
+ "Cu", "Zn", "Ga", "Ge", "As", "Se", "Br", "Kr",
32
+ "Rb", "Sr", "Y", "Zr", "Nb", "Mo", "Tc", "Ru", "Rh", "Pd",
33
+ "Ag", "Cd", "In", "Sn", "Sb", "Te", "I", "Xe",
34
+ "Cs", "Ba", "La", "Ce", "Pr", "Nd", "Pm", "Sm", "Eu", "Gd",
35
+ "Tb", "Dy", "Ho", "Er", "Tm", "Yb", "Lu",
36
+ "Hf", "Ta", "W", "Re", "Os", "Ir", "Pt",
37
+ "Au", "Hg", "Tl", "Pb", "Bi", "Po", "At", "Rn",
38
+ "Fr", "Ra", "Ac", "Th", "Pa", "U", "Np", "Pu",
39
+ "Am", "Cm", "Bk", "Cf", "Es", "Fm", "Md", "No", "Lr",
40
+ "Rf", "Db", "Sg", "Bh", "Hs", "Mt", "Ds", "Rg", "Cn",
41
+ "Nh", "Fl", "Mc", "Lv", "Ts", "Og",
42
+ ]
43
+
44
+ # SECTION: Reaction
45
+ REACTION_SYMBOLIC_MODES = {
46
+ # irreversible
47
+ "->": ("irreversible", "forward"),
48
+ "=>": ("irreversible", "forward"),
49
+ "→": ("irreversible", "forward"),
50
+ "⇒": ("irreversible", "forward"),
51
+
52
+ # backward irreversible
53
+ # ! not recommended for use in reaction expressions
54
+ "<-": ("irreversible", "backward"),
55
+ "←": ("irreversible", "backward"),
56
+
57
+ # reversible
58
+ "<=>": ("reversible", "forward"),
59
+ "<->": ("reversible", "forward"),
60
+ "⇌": ("reversible", "forward"),
61
+ "↔": ("reversible", "forward"),
62
+
63
+ # thermodynamic equilibrium
64
+ "=": ("equilibrium", "forward"),
65
+ }
66
+
67
+ # NOTE: irreversible reaction mode symbols
68
+ IRREVERSIBLE_REACTION_MODE_SYMBOLS = [
69
+ "->", "=>", "→", "⇒",
70
+ "<-", "←"
71
+ ]
72
+
73
+ # NOTE: reversible reaction mode symbols
74
+ REVERSIBLE_REACTION_MODE_SYMBOLS = [
75
+ "<=>", "<->", "⇌", "↔"
76
+ ]
77
+
78
+ # NOTE: equilibrium reaction mode symbols
79
+ EQUILIBRIUM_REACTION_MODE_SYMBOLS = [
80
+ "="
81
+ ]
82
+
83
+ # NOTE: all reaction mode symbols
84
+ ALL_REACTION_MODE_SYMBOLS = (
85
+ IRREVERSIBLE_REACTION_MODE_SYMBOLS +
86
+ REVERSIBLE_REACTION_MODE_SYMBOLS +
87
+ EQUILIBRIUM_REACTION_MODE_SYMBOLS
88
+ )
89
+
90
+ # NOTE: Reaction Mode
91
+ ReactionMode = Literal["<=>", "=>", "="]
92
+ # NOTE: Reaction Type
93
+ ReactionType = Literal["irreversible", "reversible", "equilibrium"]
94
+ # NOTE: Reaction Direction
95
+ ReactionDirection = Literal["forward", "backward"]
@@ -1,5 +1,5 @@
1
1
  # version
2
- __version__ = "0.1.8"
2
+ __version__ = "0.2.0"
3
3
  # author
4
4
  __author__ = "Sina Gilassi"
5
5
  # email
@@ -0,0 +1,23 @@
1
+
2
+ # NOTE: chem react
3
+ from .chem_react import (
4
+ ReactionMode,
5
+ PhaseRule,
6
+ Reactant,
7
+ Product,
8
+ ChemReact
9
+ )
10
+ from .chem_react_utils import ChemReactUtils
11
+ from .reaction_component_mapper import ReactionComponentMapper
12
+ from .reaction_network_analysis import ReactionNetworkAnalysis
13
+
14
+ __all__ = [
15
+ "ReactionMode",
16
+ "PhaseRule",
17
+ "Reactant",
18
+ "Product",
19
+ "ChemReact",
20
+ "ChemReactUtils",
21
+ "ReactionComponentMapper",
22
+ "ReactionNetworkAnalysis",
23
+ ]
@@ -0,0 +1,421 @@
1
+ # import libs
2
+ import logging
3
+ import re
4
+ from typing import Dict, Any, List, Optional, Literal, TypedDict
5
+ from pythermodb_settings.models import Component, ComponentKey
6
+ # locals
7
+ from ..configs.constants import (
8
+ R_CONST_J__molK,
9
+ PRESSURE_REF_Pa,
10
+ TEMPERATURE_REF_K,
11
+ ReactionMode,
12
+ )
13
+ from .chem_react_utils import ChemReactUtils
14
+ from .reaction_component_mapper import ReactionComponentMapper
15
+ from .reaction_network_analysis import ReactionNetworkAnalysis
16
+
17
+
18
+ # NOTE: logger
19
+ logger = logging.getLogger(__name__)
20
+
21
+ # NOTE: Phase Rule
22
+ PhaseRule = Literal["gas", "liquid", "aqueous", "solid"]
23
+
24
+ # SECTION: Models
25
+ # NOTE: reactants
26
+
27
+
28
+ class Reactant(TypedDict):
29
+ coefficient: float
30
+ molecule: str
31
+ state: str
32
+ molecule_state: str
33
+
34
+
35
+ class Product(TypedDict):
36
+ coefficient: float
37
+ molecule: str
38
+ state: str
39
+ molecule_state: str
40
+
41
+
42
+ # SECTION: ChemReact class
43
+ class ChemReact(
44
+ ChemReactUtils,
45
+ ReactionComponentMapper,
46
+ ReactionNetworkAnalysis,
47
+ ):
48
+ """
49
+ Chemical Reaction Utilities
50
+
51
+ The ChemReact class provides utilities for analyzing and processing chemical reactions in various phases and conditions. These reactions can be represented in different ways depending on the dominant factors influencing them:
52
+
53
+ - Use = → when thermodynamics dominates
54
+ - Use <=> → when kinetics + thermodynamics matter
55
+ - Use => → when kinetics only matter
56
+ - The class supports reactions involving components in gas, liquid, aqueous, and solid phases.
57
+ - It includes methods for analyzing reactions, counting carbon atoms, determining reaction phases, and more.
58
+ """
59
+ # # NOTE: variables
60
+ # system inputs
61
+ _system_inputs = None
62
+ # universal gas constant [J/mol.K]
63
+ R = R_CONST_J__molK
64
+ # temperature [K]
65
+ T_Ref = TEMPERATURE_REF_K
66
+ # pressure [bar]
67
+ P_Ref = PRESSURE_REF_Pa/1e5
68
+
69
+ # available phases
70
+ available_phases = PhaseRule.__args__
71
+
72
+ # NOTE: id separator
73
+ # ! used to separate component name and state
74
+ _id_separator: str = '-'
75
+
76
+ # NOTE: component checker
77
+ _component_checker: bool = False
78
+
79
+ # NOTE: stoichiometry source
80
+ _stoichiometry_source: dict[str, Any] = {}
81
+
82
+ def __init__(
83
+ self,
84
+ reaction_mode_symbol: ReactionMode,
85
+ components: Optional[List[Component]],
86
+ component_keys: Optional[List[ComponentKey]] = None
87
+ ):
88
+ """
89
+ Initialize the ChemReactUtils class.
90
+
91
+ Parameters
92
+ ----------
93
+ reaction_mode_symbol : ReactionMode, optional
94
+ The symbol used to separate reactants and products in a reaction equation.
95
+ components : Optional[List[Component]]
96
+ A list of Component objects involved in the reaction.
97
+ component_keys : List[ComponentKey], optional
98
+ The key used to identify components in the reaction.
99
+
100
+ Notes
101
+ -----
102
+ - Use "<=>" when kinetics + thermodynamics matter
103
+ - Use "=" when thermodynamics dominates
104
+ - Use "=>" when kinetics only matter
105
+ - If components is None, component IDs will be an empty list.
106
+ - Component IDs are generated by combining the formula and state of each component, separated by a hyphen such as "H2O-l" for liquid water.
107
+ """
108
+ # SECTION: parent class initialization
109
+ # NOTE: initialize general reaction utility settings
110
+ ChemReactUtils.__init__(
111
+ self,
112
+ available_phases=PhaseRule.__args__,
113
+ )
114
+
115
+ # NOTE: set reaction mode symbol used by this reaction parser
116
+ self.reaction_mode_symbol = reaction_mode_symbol
117
+ # >> reaction type
118
+ self.reaction_type = self.get_reaction_type(reaction_mode_symbol)
119
+
120
+ # NOTE: initialize component mapping settings
121
+ ReactionComponentMapper.__init__(
122
+ self,
123
+ components=components,
124
+ component_keys=component_keys,
125
+ id_separator=self._id_separator,
126
+ )
127
+
128
+ @property
129
+ def system_inputs(self) -> Dict[str, Any]:
130
+ """Get the system inputs."""
131
+ # check
132
+ if self._system_inputs is None:
133
+ raise ValueError("System inputs are not set.")
134
+ return self._system_inputs
135
+
136
+ @property
137
+ def stoichiometry_source(self) -> Dict[str, Any]:
138
+ """Get the stoichiometry source."""
139
+ # res
140
+ return self._stoichiometry_source
141
+
142
+ def analyze_reaction(
143
+ self,
144
+ reaction_pack: Dict[str, str],
145
+ phase_rule: Optional[str] = None
146
+ ) -> Dict[str, Any]:
147
+ """
148
+ Analyze a chemical reaction and extract relevant information.
149
+
150
+ Parameters
151
+ ----------
152
+ reaction_pack : dict
153
+ A dictionary containing the reaction and its name.
154
+ phase_rule : str, optional
155
+ The phase of the reaction, which can be 'gas', 'liquid', 'aqueous', or 'solid'.
156
+
157
+ Returns
158
+ -------
159
+ dict
160
+ A dictionary containing the analyzed reaction data, including reactants,
161
+ products, reaction coefficient, and carbon count.
162
+ """
163
+ try:
164
+ # NOTE: check reaction_pack
165
+ if not isinstance(reaction_pack, dict):
166
+ raise ValueError("reaction_pack must be a dictionary.")
167
+
168
+ if 'reaction' not in reaction_pack or 'name' not in reaction_pack:
169
+ raise ValueError(
170
+ "reaction_pack must contain 'reaction' and 'name' keys.")
171
+
172
+ # NOTE: check phase
173
+ # set phase
174
+ phase_set = self.phase_rule_analysis(phase_rule)
175
+
176
+ # SECTION: extract data from reaction
177
+ reaction = reaction_pack['reaction']
178
+ name = reaction_pack['name']
179
+
180
+ # ! Split the reaction into left and right sides
181
+ sides = reaction.split(self.reaction_mode_symbol.strip())
182
+
183
+ # Define a regex pattern to match reactants/products
184
+ # pattern = r'(\d*)?(\w+)\((\w)\)'
185
+ # pattern = r'(\d*\.?\d+)?(\w+)\((\w)\)'
186
+ # pattern = r'(?:(\d*\.?\d+)\s*)?([A-Z][a-zA-Z0-9]*)\s*(?:\((\w)\))?'
187
+ # NOTE: multi-purpose pattern
188
+ pattern = r'(?:(\d*\.?\d+)\s*)?(e(?:\{-?1?\}|[+-])?|\[[^\]\s]+\](?:\d+)?(?:\{[^{}\s]+\})?|(?:(?:\((?!(?:g|l|s|aq)\))[A-Za-z0-9]+\)\d*)*[A-Z][A-Za-z0-9]*(?:\((?!(?:g|l|s|aq)\))[A-Za-z0-9]+\)\d*)*)(?:[·*](?:\d+)?(?:(?:\((?!(?:g|l|s|aq)\))[A-Za-z0-9]+\)\d*)*[A-Z][A-Za-z0-9]*(?:\((?!(?:g|l|s|aq)\))[A-Za-z0-9]+\)\d*)*))*(?:\{[^{}\s]+\})?)\s*(?:\((g|l|s|aq)\))?'
189
+
190
+ # SECTION: SECTION: Extract reactants and products
191
+ # Extract reactants
192
+ reactants_raw = re.findall(pattern, sides[0])
193
+ reactants: List[Reactant] = [
194
+ {
195
+ 'coefficient': float(r[0]) if r[0] else float(1),
196
+ 'molecule': r[1],
197
+ 'state': r[2] if r[2] else phase_set,
198
+ 'molecule_state': ''
199
+ } for r in reactants_raw
200
+ ]
201
+
202
+ # NOTE: reactants full name
203
+ reactants_names = []
204
+ # loop over reactants
205
+ for i, item in enumerate(reactants):
206
+ # ! check phase_set and phase_rule
207
+ if phase_rule is None:
208
+ # check item state
209
+ if item['state'] == 'empty':
210
+ raise ValueError(
211
+ f"Phase rule is empty but reactant '{item['molecule']}' has state '{item['state']}'.")
212
+ else:
213
+ # check item state
214
+ if item['state'] != phase_set:
215
+ raise ValueError(
216
+ f"Phase rule is '{phase_set}' but reactant '{item['molecule']}' has state '{item['state']}'.")
217
+
218
+ # generate full name
219
+ full_name = item['molecule'] + "-" + item['state']
220
+ # append to list
221
+ reactants_names.append(full_name)
222
+ # update source
223
+ reactants[i]['molecule_state'] = full_name
224
+
225
+ # Extract products
226
+ products_raw = re.findall(pattern, sides[1])
227
+ products: List[Product] = [
228
+ {
229
+ 'coefficient': float(p[0]) if p[0] else float(1),
230
+ 'molecule': p[1],
231
+ 'state': p[2] if p[2] else phase_set,
232
+ 'molecule_state': ''
233
+ } for p in products_raw
234
+ ]
235
+
236
+ # NOTE: products full name
237
+ products_names = []
238
+ # loop over products
239
+ for i, item in enumerate(products):
240
+ # ! check phase_set and phase_rule
241
+ if phase_rule is None:
242
+ # check item state
243
+ if item['state'] == 'empty':
244
+ raise ValueError(
245
+ f"Phase rule is empty but product '{item['molecule']}' has state '{item['state']}'.")
246
+ else:
247
+ # check item state
248
+ if item['state'] != phase_set:
249
+ raise ValueError(
250
+ f"Phase rule is '{phase_set}' but product '{item['molecule']}' has state '{item['state']}'.")
251
+
252
+ # generate full name
253
+ full_name = item['molecule'] + "-" + item['state']
254
+ # append to list
255
+ products_names.append(full_name)
256
+ # update source
257
+ products[i]['molecule_state'] = full_name
258
+
259
+ # SECTION: all components
260
+ all_components = reactants_names + products_names
261
+ # >> remove duplicates
262
+ all_components: List[str] = list(set(all_components))
263
+
264
+ # SECTION: reaction coefficient and stoichiometry
265
+ reaction_coefficients = 0
266
+ reaction_stoichiometry = {}
267
+ reaction_stoichiometry_matrix = []
268
+
269
+ # iterate over reactants and products to calculate reaction coefficients
270
+ # NOTE: reactants
271
+ for item in reactants:
272
+ reaction_coefficients += item['coefficient']
273
+ reaction_stoichiometry[
274
+ item['molecule_state']
275
+ ] = -1 * item['coefficient']
276
+ # append to stoichiometric matrix
277
+ reaction_stoichiometry_matrix.append(
278
+ -1 * item['coefficient']
279
+ )
280
+
281
+ # NOTE: products
282
+ for item in products:
283
+ reaction_coefficients -= item['coefficient']
284
+ reaction_stoichiometry[
285
+ item['molecule_state']
286
+ ] = item['coefficient']
287
+ # append to stoichiometric matrix
288
+ reaction_stoichiometry_matrix.append(
289
+ item['coefficient']
290
+ )
291
+
292
+ # SECTION: Carbon count for each component
293
+ carbon_count = {}
294
+ for r in reactants:
295
+ carbon_count[r['molecule_state']] = self.count_carbon(
296
+ r['molecule'],
297
+ r['coefficient']
298
+ )
299
+ for p in products:
300
+ carbon_count[p['molecule_state']] = self.count_carbon(
301
+ p['molecule'],
302
+ p['coefficient']
303
+ )
304
+
305
+ # SECTION: reaction state
306
+ reaction_state = {}
307
+ for r in reactants:
308
+ # set
309
+ reaction_state[r['molecule_state']] = r['state']
310
+ for p in products:
311
+ # set
312
+ reaction_state[p['molecule_state']] = p['state']
313
+
314
+ # NOTE: reaction phase
315
+ # reaction
316
+ reaction_phase = self.determine_reaction_phase(
317
+ reaction_state
318
+ )
319
+
320
+ # NOTE: unique states
321
+ state_count = self.count_reaction_states(
322
+ reaction_state
323
+ )
324
+
325
+ # SECTION: Symbolic reaction without states
326
+ symbolic_reaction = ""
327
+ symbolic_unbalanced_reaction = ""
328
+
329
+ # reactants
330
+ for i, r in enumerate(reactants):
331
+ if i == 0:
332
+ if r['coefficient'] == 1:
333
+ symbolic_reaction += f"{r['molecule']}"
334
+ else:
335
+ symbolic_reaction += f"{r['coefficient']}{r['molecule']}"
336
+ # unbalanced
337
+ symbolic_unbalanced_reaction += f"{r['molecule']}"
338
+ else:
339
+ if r['coefficient'] == 1:
340
+ symbolic_reaction += f" + {r['molecule']}"
341
+ else:
342
+ symbolic_reaction += f" + {r['coefficient']}{r['molecule']}"
343
+ # unbalanced
344
+ symbolic_unbalanced_reaction += f" + {r['molecule']}"
345
+ # reaction mode symbol
346
+ symbolic_reaction += f" {self.reaction_mode_symbol} "
347
+ symbolic_unbalanced_reaction += f" {self.reaction_mode_symbol} "
348
+
349
+ # products
350
+ for i, p in enumerate(products):
351
+ if i == 0:
352
+ if p['coefficient'] == 1:
353
+ symbolic_reaction += f"{p['molecule']}"
354
+ else:
355
+ symbolic_reaction += f"{p['coefficient']}{p['molecule']}"
356
+ # unbalanced
357
+ symbolic_unbalanced_reaction += f"{p['molecule']}"
358
+ else:
359
+ if p['coefficient'] == 1:
360
+ symbolic_reaction += f" + {p['molecule']}"
361
+ else:
362
+ symbolic_reaction += f" + {p['coefficient']}{p['molecule']}"
363
+ # unbalanced
364
+ symbolic_unbalanced_reaction += f" + {p['molecule']}"
365
+
366
+ # SECTION: set id for each component
367
+ # NOTE: component ids
368
+ component_ids = {}
369
+ for i, r in enumerate(reactants):
370
+ component_ids[r['molecule_state']] = i+1
371
+ offset = len(reactants)
372
+ for i, p in enumerate(products):
373
+ component_ids[p['molecule_state']] = offset + i + 1
374
+
375
+ # SECTION: collect components
376
+ components = self.collect_components(
377
+ reactants,
378
+ products
379
+ )
380
+
381
+ # SECTION: map components
382
+ map_components = self.map_components(
383
+ reactants,
384
+ products,
385
+ )
386
+
387
+ # SECTION: build stoichiometry source
388
+ stoichiometry_source = self.build_stoichiometry_source(
389
+ reaction_stoichiometry=reaction_stoichiometry
390
+ )
391
+
392
+ # res
393
+ res = {
394
+ 'name': name,
395
+ 'reaction': reaction,
396
+ 'reaction_mode_symbol': self.reaction_mode_symbol,
397
+ 'reaction_type': self.reaction_type,
398
+ "component_ids": component_ids,
399
+ "all_components": all_components,
400
+ "symbolic_reaction": symbolic_reaction,
401
+ "symbolic_unbalanced_reaction": symbolic_unbalanced_reaction,
402
+ 'reactants': reactants,
403
+ 'reactants_names': reactants_names,
404
+ 'products': products,
405
+ 'products_names': products_names,
406
+ 'reaction_coefficients': reaction_coefficients,
407
+ 'reaction_stoichiometry': reaction_stoichiometry,
408
+ 'reaction_stoichiometry_matrix': reaction_stoichiometry_matrix,
409
+ 'reaction_stoichiometry_source': stoichiometry_source,
410
+ 'carbon_count': carbon_count,
411
+ 'reaction_state': reaction_state,
412
+ 'reaction_phase': reaction_phase,
413
+ 'state_count': state_count,
414
+ 'components': components,
415
+ 'map_components': map_components,
416
+ 'component_checker': self._component_checker,
417
+ }
418
+
419
+ return res
420
+ except Exception as e:
421
+ raise Exception(f"Error analyzing reaction: {e}")