pyreactlab-core 0.1.0__py3-none-any.whl
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.
- pyreactlab_core/__init__.py +25 -0
- pyreactlab_core/app.py +123 -0
- pyreactlab_core/configs/__init__.py +0 -0
- pyreactlab_core/configs/constants.py +40 -0
- pyreactlab_core/configs/info.py +12 -0
- pyreactlab_core/core/__init__.py +1 -0
- pyreactlab_core/core/chem_react.py +839 -0
- pyreactlab_core/docs/__init__.py +14 -0
- pyreactlab_core/docs/chem_balance.py +719 -0
- pyreactlab_core/docs/chem_utils.py +114 -0
- pyreactlab_core/models/__init__.py +0 -0
- pyreactlab_core/models/reaction.py +174 -0
- pyreactlab_core/utils/__init__.py +0 -0
- pyreactlab_core/utils/tools.py +30 -0
- pyreactlab_core-0.1.0.dist-info/METADATA +179 -0
- pyreactlab_core-0.1.0.dist-info/RECORD +19 -0
- pyreactlab_core-0.1.0.dist-info/WHEEL +5 -0
- pyreactlab_core-0.1.0.dist-info/licenses/LICENSE +201 -0
- pyreactlab_core-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,839 @@
|
|
|
1
|
+
# import libs
|
|
2
|
+
import logging
|
|
3
|
+
import re
|
|
4
|
+
from typing import Dict, Any, List, Optional, Literal
|
|
5
|
+
from ..configs.constants import (
|
|
6
|
+
R_CONST_J__molK,
|
|
7
|
+
PRESSURE_REF_Pa,
|
|
8
|
+
TEMPERATURE_REF_K,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
# NOTE: logger
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
# NOTE: Reaction Mode
|
|
15
|
+
ReactionMode = Literal["<=>", "=>", "="]
|
|
16
|
+
|
|
17
|
+
# NOTE: Phase Rule
|
|
18
|
+
PhaseRule = Literal["gas", "liquid", "aqueous", "solid"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ChemReact:
|
|
22
|
+
"""
|
|
23
|
+
Chemical Reaction Utilities
|
|
24
|
+
|
|
25
|
+
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:
|
|
26
|
+
|
|
27
|
+
- Use = → when thermodynamics dominates
|
|
28
|
+
- Use <=> → when kinetics + thermodynamics matter
|
|
29
|
+
- Use => → when kinetics only matter
|
|
30
|
+
"""
|
|
31
|
+
# # NOTE: variables
|
|
32
|
+
# system inputs
|
|
33
|
+
_system_inputs = None
|
|
34
|
+
# universal gas constant [J/mol.K]
|
|
35
|
+
R = R_CONST_J__molK
|
|
36
|
+
# temperature [K]
|
|
37
|
+
T_Ref = TEMPERATURE_REF_K
|
|
38
|
+
# pressure [bar]
|
|
39
|
+
P_Ref = PRESSURE_REF_Pa/1e5
|
|
40
|
+
|
|
41
|
+
# available phases
|
|
42
|
+
available_phases = PhaseRule.__args__
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
reaction_mode_symbol: ReactionMode
|
|
47
|
+
):
|
|
48
|
+
"""
|
|
49
|
+
Initialize the ChemReactUtils class.
|
|
50
|
+
|
|
51
|
+
Parameters
|
|
52
|
+
----------
|
|
53
|
+
reaction_mode_symbol : ReactionMode, optional
|
|
54
|
+
The symbol used to separate reactants and products in a reaction equation.
|
|
55
|
+
|
|
56
|
+
Notes
|
|
57
|
+
-----
|
|
58
|
+
- Use "<=>" when kinetics + thermodynamics matter
|
|
59
|
+
- Use "=" when thermodynamics dominates
|
|
60
|
+
- Use "=>" when kinetics only matter
|
|
61
|
+
"""
|
|
62
|
+
self.reaction_mode_symbol = reaction_mode_symbol
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def system_inputs(self) -> Dict[str, Any]:
|
|
66
|
+
"""Get the system inputs."""
|
|
67
|
+
# check
|
|
68
|
+
if self._system_inputs is None:
|
|
69
|
+
raise ValueError("System inputs are not set.")
|
|
70
|
+
return self._system_inputs
|
|
71
|
+
|
|
72
|
+
def count_carbon(self, molecule: str, coefficient: float) -> float:
|
|
73
|
+
"""
|
|
74
|
+
Count the number of carbon atoms in a molecule.
|
|
75
|
+
|
|
76
|
+
Parameters
|
|
77
|
+
----------
|
|
78
|
+
molecule : str
|
|
79
|
+
The chemical formula of the molecule.
|
|
80
|
+
coefficient : float
|
|
81
|
+
The coefficient of the molecule in the reaction.
|
|
82
|
+
|
|
83
|
+
Returns
|
|
84
|
+
-------
|
|
85
|
+
float
|
|
86
|
+
The number of carbon atoms in the molecule multiplied by the coefficient.
|
|
87
|
+
"""
|
|
88
|
+
try:
|
|
89
|
+
# NOTE: check molecule
|
|
90
|
+
if not isinstance(molecule, str):
|
|
91
|
+
raise ValueError("Molecule must be a string.")
|
|
92
|
+
|
|
93
|
+
# NOTE: check coefficient
|
|
94
|
+
if not isinstance(coefficient, (int, float)):
|
|
95
|
+
raise ValueError("Coefficient must be an integer or float.")
|
|
96
|
+
|
|
97
|
+
# NOTE: Check if the molecule contains carbon atoms
|
|
98
|
+
if re.search(r'C(?![a-z])', molecule):
|
|
99
|
+
carbon_count = len(re.findall(
|
|
100
|
+
r'C(?![a-z])', molecule)) * coefficient
|
|
101
|
+
return carbon_count
|
|
102
|
+
else:
|
|
103
|
+
return 0.0
|
|
104
|
+
except Exception as e:
|
|
105
|
+
raise Exception(
|
|
106
|
+
f"Error counting carbon in molecule '{molecule}': {e}")
|
|
107
|
+
|
|
108
|
+
def analyze_reaction(
|
|
109
|
+
self,
|
|
110
|
+
reaction_pack: Dict[str, str],
|
|
111
|
+
phase_rule: Optional[str] = None
|
|
112
|
+
) -> Dict[str, Any]:
|
|
113
|
+
"""
|
|
114
|
+
Analyze a chemical reaction and extract relevant information.
|
|
115
|
+
|
|
116
|
+
Parameters
|
|
117
|
+
----------
|
|
118
|
+
reaction_pack : dict
|
|
119
|
+
A dictionary containing the reaction and its name.
|
|
120
|
+
phase_rule : str, optional
|
|
121
|
+
The phase of the reaction, which can be 'gas', 'liquid', 'aqueous', or 'solid'.
|
|
122
|
+
|
|
123
|
+
Returns
|
|
124
|
+
-------
|
|
125
|
+
dict
|
|
126
|
+
A dictionary containing the analyzed reaction data, including reactants,
|
|
127
|
+
products, reaction coefficient, and carbon count.
|
|
128
|
+
"""
|
|
129
|
+
try:
|
|
130
|
+
# NOTE: check reaction_pack
|
|
131
|
+
if not isinstance(reaction_pack, dict):
|
|
132
|
+
raise ValueError("reaction_pack must be a dictionary.")
|
|
133
|
+
|
|
134
|
+
if 'reaction' not in reaction_pack or 'name' not in reaction_pack:
|
|
135
|
+
raise ValueError(
|
|
136
|
+
"reaction_pack must contain 'reaction' and 'name' keys.")
|
|
137
|
+
|
|
138
|
+
# NOTE: check phase
|
|
139
|
+
# set phase
|
|
140
|
+
phase_set = self.phase_rule_analysis(phase_rule)
|
|
141
|
+
|
|
142
|
+
# SECTION: extract data from reaction
|
|
143
|
+
reaction = reaction_pack['reaction']
|
|
144
|
+
name = reaction_pack['name']
|
|
145
|
+
|
|
146
|
+
# ! Split the reaction into left and right sides
|
|
147
|
+
sides = reaction.split(self.reaction_mode_symbol.strip())
|
|
148
|
+
|
|
149
|
+
# Define a regex pattern to match reactants/products
|
|
150
|
+
# pattern = r'(\d*)?(\w+)\((\w)\)'
|
|
151
|
+
# pattern = r'(\d*\.?\d+)?(\w+)\((\w)\)'
|
|
152
|
+
# pattern = r'(?:(\d*\.?\d+)\s*)?([A-Z][a-zA-Z0-9]*)\s*(?:\((\w)\))?'
|
|
153
|
+
# NOTE: multi-purpose pattern
|
|
154
|
+
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)\))?'
|
|
155
|
+
|
|
156
|
+
# SECTION: SECTION: Extract reactants and products
|
|
157
|
+
# Extract reactants
|
|
158
|
+
reactants = re.findall(pattern, sides[0])
|
|
159
|
+
reactants = [
|
|
160
|
+
{
|
|
161
|
+
'coefficient': float(r[0]) if r[0] else float(1),
|
|
162
|
+
'molecule': r[1],
|
|
163
|
+
'state': r[2] if r[2] else phase_set
|
|
164
|
+
} for r in reactants
|
|
165
|
+
]
|
|
166
|
+
|
|
167
|
+
# NOTE: reactants full name
|
|
168
|
+
reactants_names = []
|
|
169
|
+
# loop over reactants
|
|
170
|
+
for i, item in enumerate(reactants):
|
|
171
|
+
# ! check phase_set and phase_rule
|
|
172
|
+
if phase_rule is None:
|
|
173
|
+
# check item state
|
|
174
|
+
if item['state'] == 'empty':
|
|
175
|
+
raise ValueError(
|
|
176
|
+
f"Phase rule is empty but reactant '{item['molecule']}' has state '{item['state']}'.")
|
|
177
|
+
else:
|
|
178
|
+
# check item state
|
|
179
|
+
if item['state'] != phase_set:
|
|
180
|
+
raise ValueError(
|
|
181
|
+
f"Phase rule is '{phase_set}' but reactant '{item['molecule']}' has state '{item['state']}'.")
|
|
182
|
+
|
|
183
|
+
# generate full name
|
|
184
|
+
full_name = item['molecule'] + "-" + item['state']
|
|
185
|
+
# append to list
|
|
186
|
+
reactants_names.append(full_name)
|
|
187
|
+
# update source
|
|
188
|
+
reactants[i]['molecule_state'] = full_name
|
|
189
|
+
|
|
190
|
+
# Extract products
|
|
191
|
+
products = re.findall(pattern, sides[1])
|
|
192
|
+
products = [
|
|
193
|
+
{
|
|
194
|
+
'coefficient': float(p[0]) if p[0] else float(1),
|
|
195
|
+
'molecule': p[1],
|
|
196
|
+
'state': p[2] if p[2] else phase_set
|
|
197
|
+
} for p in products
|
|
198
|
+
]
|
|
199
|
+
|
|
200
|
+
# NOTE: products full name
|
|
201
|
+
products_names = []
|
|
202
|
+
# loop over products
|
|
203
|
+
for i, item in enumerate(products):
|
|
204
|
+
# ! check phase_set and phase_rule
|
|
205
|
+
if phase_rule is None:
|
|
206
|
+
# check item state
|
|
207
|
+
if item['state'] == 'empty':
|
|
208
|
+
raise ValueError(
|
|
209
|
+
f"Phase rule is empty but product '{item['molecule']}' has state '{item['state']}'.")
|
|
210
|
+
else:
|
|
211
|
+
# check item state
|
|
212
|
+
if item['state'] != phase_set:
|
|
213
|
+
raise ValueError(
|
|
214
|
+
f"Phase rule is '{phase_set}' but product '{item['molecule']}' has state '{item['state']}'.")
|
|
215
|
+
|
|
216
|
+
# generate full name
|
|
217
|
+
full_name = item['molecule'] + "-" + item['state']
|
|
218
|
+
# append to list
|
|
219
|
+
products_names.append(full_name)
|
|
220
|
+
# update source
|
|
221
|
+
products[i]['molecule_state'] = full_name
|
|
222
|
+
|
|
223
|
+
# SECTION: all components
|
|
224
|
+
all_components = reactants_names + products_names
|
|
225
|
+
# >> remove duplicates
|
|
226
|
+
all_components: List[str] = list(set(all_components))
|
|
227
|
+
|
|
228
|
+
# SECTION: reaction coefficient and stoichiometry
|
|
229
|
+
reaction_coefficients = 0
|
|
230
|
+
reaction_stoichiometry = {}
|
|
231
|
+
reaction_stoichiometry_matrix = []
|
|
232
|
+
|
|
233
|
+
# iterate over reactants and products to calculate reaction coefficients
|
|
234
|
+
# NOTE: reactants
|
|
235
|
+
for item in reactants:
|
|
236
|
+
reaction_coefficients += item['coefficient']
|
|
237
|
+
reaction_stoichiometry[
|
|
238
|
+
item['molecule_state']
|
|
239
|
+
] = -1 * item['coefficient']
|
|
240
|
+
# append to stoichiometric matrix
|
|
241
|
+
reaction_stoichiometry_matrix.append(
|
|
242
|
+
-1 * item['coefficient']
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
# NOTE: products
|
|
246
|
+
for item in products:
|
|
247
|
+
reaction_coefficients -= item['coefficient']
|
|
248
|
+
reaction_stoichiometry[
|
|
249
|
+
item['molecule_state']
|
|
250
|
+
] = item['coefficient']
|
|
251
|
+
# append to stoichiometric matrix
|
|
252
|
+
reaction_stoichiometry_matrix.append(
|
|
253
|
+
item['coefficient']
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
# SECTION: Carbon count for each component
|
|
257
|
+
carbon_count = {}
|
|
258
|
+
for r in reactants:
|
|
259
|
+
carbon_count[r['molecule_state']] = self.count_carbon(
|
|
260
|
+
r['molecule'],
|
|
261
|
+
r['coefficient']
|
|
262
|
+
)
|
|
263
|
+
for p in products:
|
|
264
|
+
carbon_count[p['molecule_state']] = self.count_carbon(
|
|
265
|
+
p['molecule'],
|
|
266
|
+
p['coefficient']
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
# SECTION: reaction state
|
|
270
|
+
reaction_state = {}
|
|
271
|
+
for r in reactants:
|
|
272
|
+
# set
|
|
273
|
+
reaction_state[r['molecule_state']] = r['state']
|
|
274
|
+
for p in products:
|
|
275
|
+
# set
|
|
276
|
+
reaction_state[p['molecule_state']] = p['state']
|
|
277
|
+
|
|
278
|
+
# NOTE: reaction phase
|
|
279
|
+
# reaction
|
|
280
|
+
reaction_phase = self.determine_reaction_phase(
|
|
281
|
+
reaction_state
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
# NOTE: unique states
|
|
285
|
+
state_count = self.count_reaction_states(
|
|
286
|
+
reaction_state
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
# SECTION: Symbolic reaction without states
|
|
290
|
+
symbolic_reaction = ""
|
|
291
|
+
symbolic_unbalanced_reaction = ""
|
|
292
|
+
|
|
293
|
+
# reactants
|
|
294
|
+
for i, r in enumerate(reactants):
|
|
295
|
+
if i == 0:
|
|
296
|
+
if r['coefficient'] == 1:
|
|
297
|
+
symbolic_reaction += f"{r['molecule']}"
|
|
298
|
+
else:
|
|
299
|
+
symbolic_reaction += f"{r['coefficient']}{r['molecule']}"
|
|
300
|
+
# unbalanced
|
|
301
|
+
symbolic_unbalanced_reaction += f"{r['molecule']}"
|
|
302
|
+
else:
|
|
303
|
+
if r['coefficient'] == 1:
|
|
304
|
+
symbolic_reaction += f" + {r['molecule']}"
|
|
305
|
+
else:
|
|
306
|
+
symbolic_reaction += f" + {r['coefficient']}{r['molecule']}"
|
|
307
|
+
# unbalanced
|
|
308
|
+
symbolic_unbalanced_reaction += f" + {r['molecule']}"
|
|
309
|
+
# reaction mode symbol
|
|
310
|
+
symbolic_reaction += f" {self.reaction_mode_symbol} "
|
|
311
|
+
symbolic_unbalanced_reaction += f" {self.reaction_mode_symbol} "
|
|
312
|
+
|
|
313
|
+
# products
|
|
314
|
+
for i, p in enumerate(products):
|
|
315
|
+
if i == 0:
|
|
316
|
+
if p['coefficient'] == 1:
|
|
317
|
+
symbolic_reaction += f"{p['molecule']}"
|
|
318
|
+
else:
|
|
319
|
+
symbolic_reaction += f"{p['coefficient']}{p['molecule']}"
|
|
320
|
+
# unbalanced
|
|
321
|
+
symbolic_unbalanced_reaction += f"{p['molecule']}"
|
|
322
|
+
else:
|
|
323
|
+
if p['coefficient'] == 1:
|
|
324
|
+
symbolic_reaction += f" + {p['molecule']}"
|
|
325
|
+
else:
|
|
326
|
+
symbolic_reaction += f" + {p['coefficient']}{p['molecule']}"
|
|
327
|
+
# unbalanced
|
|
328
|
+
symbolic_unbalanced_reaction += f" + {p['molecule']}"
|
|
329
|
+
|
|
330
|
+
# SECTION: set id for each component
|
|
331
|
+
# NOTE: component ids
|
|
332
|
+
component_ids = {}
|
|
333
|
+
for i, r in enumerate(reactants):
|
|
334
|
+
component_ids[r['molecule_state']] = i+1
|
|
335
|
+
offset = len(reactants)
|
|
336
|
+
for i, p in enumerate(products):
|
|
337
|
+
component_ids[p['molecule_state']] = offset + i + 1
|
|
338
|
+
|
|
339
|
+
# res
|
|
340
|
+
res = {
|
|
341
|
+
'name': name,
|
|
342
|
+
'reaction': reaction,
|
|
343
|
+
"component_ids": component_ids,
|
|
344
|
+
"all_components": all_components,
|
|
345
|
+
"symbolic_reaction": symbolic_reaction,
|
|
346
|
+
"symbolic_unbalanced_reaction": symbolic_unbalanced_reaction,
|
|
347
|
+
'reactants': reactants,
|
|
348
|
+
'reactants_names': reactants_names,
|
|
349
|
+
'products': products,
|
|
350
|
+
'products_names': products_names,
|
|
351
|
+
'reaction_coefficients': reaction_coefficients,
|
|
352
|
+
'reaction_stoichiometry': reaction_stoichiometry,
|
|
353
|
+
'reaction_stoichiometry_matrix': reaction_stoichiometry_matrix,
|
|
354
|
+
'carbon_count': carbon_count,
|
|
355
|
+
'reaction_state': reaction_state,
|
|
356
|
+
'reaction_phase': reaction_phase,
|
|
357
|
+
'state_count': state_count,
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return res
|
|
361
|
+
except Exception as e:
|
|
362
|
+
raise Exception(f"Error analyzing reaction: {e}")
|
|
363
|
+
|
|
364
|
+
def phase_rule_analysis(self, phase_rule: Optional[str] = None) -> str:
|
|
365
|
+
"""
|
|
366
|
+
Analyze the phase rule of a reaction.
|
|
367
|
+
|
|
368
|
+
Parameters
|
|
369
|
+
----------
|
|
370
|
+
phase_rule : str, optional
|
|
371
|
+
The phase rule of the reaction.
|
|
372
|
+
|
|
373
|
+
Returns
|
|
374
|
+
-------
|
|
375
|
+
phase_symbol : str
|
|
376
|
+
The phase symbol of the reaction, which can be 'g', 'l', or 'empty'.
|
|
377
|
+
"""
|
|
378
|
+
try:
|
|
379
|
+
# SECTION: check phase rule
|
|
380
|
+
# Check if phase_rule is None
|
|
381
|
+
if phase_rule is None or phase_rule == 'None':
|
|
382
|
+
# set default phase
|
|
383
|
+
return 'empty'
|
|
384
|
+
|
|
385
|
+
# SECTION: check phase rule
|
|
386
|
+
# Check if the phase rule is valid
|
|
387
|
+
if phase_rule not in self.available_phases:
|
|
388
|
+
raise ValueError(
|
|
389
|
+
f"Phase rule must be {', '.join(self.available_phases)}.")
|
|
390
|
+
|
|
391
|
+
# check phase
|
|
392
|
+
if phase_rule == 'gas':
|
|
393
|
+
phase_symbol = 'g'
|
|
394
|
+
elif phase_rule == 'liquid':
|
|
395
|
+
phase_symbol = 'l'
|
|
396
|
+
elif phase_rule == 'aqueous':
|
|
397
|
+
phase_symbol = 'aq'
|
|
398
|
+
elif phase_rule == 'solid':
|
|
399
|
+
phase_symbol = 's'
|
|
400
|
+
else:
|
|
401
|
+
phase_symbol = 'empty'
|
|
402
|
+
|
|
403
|
+
return phase_symbol
|
|
404
|
+
except Exception as e:
|
|
405
|
+
raise Exception(f"Error analyzing phase rule: {e}")
|
|
406
|
+
|
|
407
|
+
def analyze_overall_reactions(
|
|
408
|
+
self,
|
|
409
|
+
reactions: List[Dict[str, str]]
|
|
410
|
+
) -> Dict[str, List[str]]:
|
|
411
|
+
"""
|
|
412
|
+
Analyze a list of chemical reactions and classify species as consumed, produced, or intermediate.
|
|
413
|
+
|
|
414
|
+
Parameters
|
|
415
|
+
----------
|
|
416
|
+
reactions : list
|
|
417
|
+
A list of dictionaries, each containing a reaction string and its name.
|
|
418
|
+
|
|
419
|
+
Returns
|
|
420
|
+
-------
|
|
421
|
+
dict
|
|
422
|
+
A dictionary containing three lists: 'consumed', 'produced', and 'intermediate'.
|
|
423
|
+
- 'consumed': List of species consumed in the reactions.
|
|
424
|
+
- 'produced': List of species produced in the reactions.
|
|
425
|
+
- 'intermediate': List of species that are both consumed and produced in the reactions.
|
|
426
|
+
"""
|
|
427
|
+
try:
|
|
428
|
+
# Initialize sets for all reactants and products
|
|
429
|
+
all_reactants = set()
|
|
430
|
+
all_products = set()
|
|
431
|
+
|
|
432
|
+
# Iterate over reactions
|
|
433
|
+
for reaction in reactions:
|
|
434
|
+
# NOTE: reaction string
|
|
435
|
+
# ! Split the reaction into left and right sides
|
|
436
|
+
sides = reaction['reaction'].split(
|
|
437
|
+
self.reaction_mode_symbol.strip()
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
# NOTE: Define a regex pattern to match reactants/products
|
|
441
|
+
# pattern = r'(\d*)?(\w+)\((\w)\)'
|
|
442
|
+
pattern = r'(?:(\d*\.?\d+)\s*)?([A-Z][a-zA-Z0-9]*)\s*(?:\((\w)\))?'
|
|
443
|
+
|
|
444
|
+
# SECTION: Extract reactants
|
|
445
|
+
reactants = re.findall(pattern, sides[0])
|
|
446
|
+
reactants = [r[1] for r in reactants]
|
|
447
|
+
|
|
448
|
+
# SECTION: Extract products
|
|
449
|
+
products = re.findall(pattern, sides[1])
|
|
450
|
+
products = [p[1] for p in products]
|
|
451
|
+
|
|
452
|
+
# NOTE: Update sets
|
|
453
|
+
all_reactants.update(reactants)
|
|
454
|
+
all_products.update(products)
|
|
455
|
+
|
|
456
|
+
# Classify species
|
|
457
|
+
consumed = list(all_reactants - all_products)
|
|
458
|
+
produced = list(all_products - all_reactants)
|
|
459
|
+
intermediate = list(all_reactants & all_products)
|
|
460
|
+
|
|
461
|
+
# res
|
|
462
|
+
res = {
|
|
463
|
+
'consumed': consumed,
|
|
464
|
+
'produced': produced,
|
|
465
|
+
'intermediate': intermediate
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
return res
|
|
469
|
+
except Exception as e:
|
|
470
|
+
raise Exception(f"Error analyzing overall reactions: {e}")
|
|
471
|
+
|
|
472
|
+
def analyze_overall_reactions_v2(
|
|
473
|
+
self,
|
|
474
|
+
reactions: Dict[str, Any]
|
|
475
|
+
) -> Dict[str, List[str]]:
|
|
476
|
+
"""
|
|
477
|
+
Analyze a list of chemical reactions and classify species as consumed, produced, or intermediate (version 2).
|
|
478
|
+
|
|
479
|
+
Parameters
|
|
480
|
+
----------
|
|
481
|
+
reactions : list
|
|
482
|
+
A list of dictionaries, each containing a reaction string and its name.
|
|
483
|
+
|
|
484
|
+
Returns
|
|
485
|
+
-------
|
|
486
|
+
dict
|
|
487
|
+
A dictionary containing three lists: 'consumed', 'produced', and 'intermediate'.
|
|
488
|
+
- 'consumed': List of species consumed in the reactions.
|
|
489
|
+
- 'produced': List of species produced in the reactions.
|
|
490
|
+
- 'intermediate': List of species that are both consumed and produced in the reactions.
|
|
491
|
+
"""
|
|
492
|
+
try:
|
|
493
|
+
# Initialize sets for all reactants and products
|
|
494
|
+
all_reactants = set()
|
|
495
|
+
all_products = set()
|
|
496
|
+
|
|
497
|
+
# Iterate over reactions
|
|
498
|
+
for reaction_name, reaction_value in reactions.items():
|
|
499
|
+
# SECTION: Extract reactants
|
|
500
|
+
reactants = reaction_value['reactants_names']
|
|
501
|
+
|
|
502
|
+
# SECTION: Extract products
|
|
503
|
+
products = reaction_value['products_names']
|
|
504
|
+
|
|
505
|
+
# NOTE: Update sets
|
|
506
|
+
all_reactants.update(reactants)
|
|
507
|
+
all_products.update(products)
|
|
508
|
+
|
|
509
|
+
# Classify species
|
|
510
|
+
consumed = list(all_reactants - all_products)
|
|
511
|
+
produced = list(all_products - all_reactants)
|
|
512
|
+
intermediate = list(all_reactants & all_products)
|
|
513
|
+
|
|
514
|
+
# res
|
|
515
|
+
res = {
|
|
516
|
+
'consumed': consumed,
|
|
517
|
+
'produced': produced,
|
|
518
|
+
'intermediate': intermediate
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
return res
|
|
522
|
+
except Exception as e:
|
|
523
|
+
raise Exception(f"Error analyzing overall reactions: {e}")
|
|
524
|
+
|
|
525
|
+
def define_component_id(self, reaction_res):
|
|
526
|
+
'''
|
|
527
|
+
Define component ID
|
|
528
|
+
|
|
529
|
+
Parameters
|
|
530
|
+
----------
|
|
531
|
+
reaction_res: dict
|
|
532
|
+
reaction_res
|
|
533
|
+
|
|
534
|
+
Returns
|
|
535
|
+
-------
|
|
536
|
+
component_list: list
|
|
537
|
+
component list
|
|
538
|
+
component_dict: dict
|
|
539
|
+
component dict
|
|
540
|
+
comp_list: list
|
|
541
|
+
component list
|
|
542
|
+
comp_coeff: list
|
|
543
|
+
component coefficient
|
|
544
|
+
'''
|
|
545
|
+
try:
|
|
546
|
+
# NOTE: component list
|
|
547
|
+
component_list = []
|
|
548
|
+
|
|
549
|
+
# SECTION: Iterate over reactions and extract reactants and products
|
|
550
|
+
for item in reaction_res:
|
|
551
|
+
for reactant in reaction_res[item]['reactants']:
|
|
552
|
+
component_list.append(reactant['molecule'])
|
|
553
|
+
for product in reaction_res[item]['products']:
|
|
554
|
+
component_list.append(product['molecule'])
|
|
555
|
+
|
|
556
|
+
# remove duplicate
|
|
557
|
+
component_list = list(set(component_list))
|
|
558
|
+
|
|
559
|
+
# component id: key, value
|
|
560
|
+
component_dict = {}
|
|
561
|
+
for i, item in enumerate(component_list):
|
|
562
|
+
component_dict[item] = i
|
|
563
|
+
|
|
564
|
+
# SECTION: Initialize the component list
|
|
565
|
+
comp_list = [
|
|
566
|
+
{i: 0.0 for i in component_dict.keys()} for _ in range(len(reaction_res))
|
|
567
|
+
]
|
|
568
|
+
|
|
569
|
+
# NOTE: Iterate over reactions and components
|
|
570
|
+
for j, reaction in enumerate(reaction_res):
|
|
571
|
+
for item in component_dict.keys():
|
|
572
|
+
# Check reactants
|
|
573
|
+
for reactant in reaction_res[reaction]['reactants']:
|
|
574
|
+
if reactant['molecule'] == item:
|
|
575
|
+
comp_list[j][item] = -1 * \
|
|
576
|
+
float(reactant['coefficient'])
|
|
577
|
+
|
|
578
|
+
# Check products
|
|
579
|
+
for product in reaction_res[reaction]['products']:
|
|
580
|
+
if product['molecule'] == item:
|
|
581
|
+
comp_list[j][item] = float(product['coefficient'])
|
|
582
|
+
|
|
583
|
+
# Convert comp_list to comp_matrix
|
|
584
|
+
comp_coeff = [
|
|
585
|
+
[comp_list[j][item] for item in component_dict.keys()] for j in range(len(reaction_res))
|
|
586
|
+
]
|
|
587
|
+
|
|
588
|
+
# res
|
|
589
|
+
return component_list, component_dict, comp_list, comp_coeff
|
|
590
|
+
except Exception as e:
|
|
591
|
+
raise Exception(f"Error defining component ID: {e}")
|
|
592
|
+
|
|
593
|
+
@staticmethod
|
|
594
|
+
def define_component_id_v2(reaction_res):
|
|
595
|
+
'''
|
|
596
|
+
Define component ID (version 2)
|
|
597
|
+
|
|
598
|
+
Parameters
|
|
599
|
+
----------
|
|
600
|
+
reaction_res: dict
|
|
601
|
+
reaction_res
|
|
602
|
+
|
|
603
|
+
Returns
|
|
604
|
+
-------
|
|
605
|
+
component_list: list
|
|
606
|
+
component list
|
|
607
|
+
component_dict: dict
|
|
608
|
+
component dict
|
|
609
|
+
comp_list: list
|
|
610
|
+
component list
|
|
611
|
+
comp_coeff: list
|
|
612
|
+
component coefficient
|
|
613
|
+
component_state_list: list
|
|
614
|
+
component state list
|
|
615
|
+
'''
|
|
616
|
+
try:
|
|
617
|
+
# NOTE: component list
|
|
618
|
+
component_list = []
|
|
619
|
+
component_state_list = []
|
|
620
|
+
|
|
621
|
+
# SECTION: Iterate over reactions and extract reactants and products
|
|
622
|
+
for item in reaction_res:
|
|
623
|
+
# reactants
|
|
624
|
+
for reactant in reaction_res[item]['reactants']:
|
|
625
|
+
component_list.append(reactant['molecule_state'])
|
|
626
|
+
# add molecule and molecule state
|
|
627
|
+
component_state_list.append(
|
|
628
|
+
(
|
|
629
|
+
reactant['molecule'],
|
|
630
|
+
reactant['state'],
|
|
631
|
+
reactant['molecule_state']
|
|
632
|
+
)
|
|
633
|
+
)
|
|
634
|
+
# products
|
|
635
|
+
for product in reaction_res[item]['products']:
|
|
636
|
+
component_list.append(product['molecule_state'])
|
|
637
|
+
# add molecule and molecule state
|
|
638
|
+
component_state_list.append(
|
|
639
|
+
(
|
|
640
|
+
product['molecule'],
|
|
641
|
+
product['state'],
|
|
642
|
+
product['molecule_state']
|
|
643
|
+
)
|
|
644
|
+
)
|
|
645
|
+
|
|
646
|
+
# remove duplicate
|
|
647
|
+
component_list = list(set(component_list))
|
|
648
|
+
# remove duplicates in component_state_list
|
|
649
|
+
component_state_list = list(set(component_state_list))
|
|
650
|
+
|
|
651
|
+
# component id: key, value
|
|
652
|
+
component_dict = {}
|
|
653
|
+
|
|
654
|
+
# loop over component list
|
|
655
|
+
for i, item in enumerate(component_list):
|
|
656
|
+
component_dict[item] = i
|
|
657
|
+
|
|
658
|
+
# SECTION: Initialize the component list
|
|
659
|
+
comp_list = [
|
|
660
|
+
{i: 0.0 for i in component_dict.keys()} for _ in range(len(reaction_res))
|
|
661
|
+
]
|
|
662
|
+
|
|
663
|
+
# NOTE: Iterate over reactions and components
|
|
664
|
+
for j, reaction in enumerate(reaction_res):
|
|
665
|
+
for item in component_dict.keys():
|
|
666
|
+
# Check reactants
|
|
667
|
+
for reactant in reaction_res[reaction]['reactants']:
|
|
668
|
+
if reactant['molecule_state'] == item:
|
|
669
|
+
comp_list[j][item] = -1 * \
|
|
670
|
+
float(reactant['coefficient'])
|
|
671
|
+
|
|
672
|
+
# Check products
|
|
673
|
+
for product in reaction_res[reaction]['products']:
|
|
674
|
+
if product['molecule_state'] == item:
|
|
675
|
+
comp_list[j][item] = float(product['coefficient'])
|
|
676
|
+
|
|
677
|
+
# Convert comp_list to comp_matrix
|
|
678
|
+
comp_coeff = [
|
|
679
|
+
[comp_list[j][item] for item in component_dict.keys()] for j in range(len(reaction_res))
|
|
680
|
+
]
|
|
681
|
+
|
|
682
|
+
# res
|
|
683
|
+
return (
|
|
684
|
+
component_list,
|
|
685
|
+
component_dict,
|
|
686
|
+
comp_list,
|
|
687
|
+
comp_coeff,
|
|
688
|
+
component_state_list
|
|
689
|
+
)
|
|
690
|
+
except Exception as e:
|
|
691
|
+
raise Exception(f"Error defining component ID: {e}")
|
|
692
|
+
|
|
693
|
+
def state_name_set(self, state_set: set) -> List[str]:
|
|
694
|
+
'''
|
|
695
|
+
Convert state set to full names
|
|
696
|
+
|
|
697
|
+
Parameters
|
|
698
|
+
----------
|
|
699
|
+
state_set: set
|
|
700
|
+
Set of states
|
|
701
|
+
|
|
702
|
+
Returns
|
|
703
|
+
-------
|
|
704
|
+
state_names: list
|
|
705
|
+
List of full state names
|
|
706
|
+
'''
|
|
707
|
+
try:
|
|
708
|
+
state_dict = {
|
|
709
|
+
'g': 'gas',
|
|
710
|
+
'l': 'liquid',
|
|
711
|
+
'aq': 'aqueous',
|
|
712
|
+
's': 'solid'
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
# Map the states from the set to their full names
|
|
716
|
+
return [state_dict[state] for state in state_set]
|
|
717
|
+
except Exception as e:
|
|
718
|
+
raise Exception(f"Error converting state set to full names: {e}")
|
|
719
|
+
|
|
720
|
+
def determine_reaction_phase(self, reaction_dict: Dict[str, str]) -> str:
|
|
721
|
+
'''
|
|
722
|
+
Determine the phase of a reaction based on the states of its components.
|
|
723
|
+
|
|
724
|
+
Parameters
|
|
725
|
+
----------
|
|
726
|
+
reaction_dict: dict
|
|
727
|
+
A dictionary where keys are component names and values are their states.
|
|
728
|
+
|
|
729
|
+
Returns
|
|
730
|
+
-------
|
|
731
|
+
str
|
|
732
|
+
The phase of the reaction, which can be 'gas', 'liquid', 'aqueous', 'solid', or a combination of these.
|
|
733
|
+
'''
|
|
734
|
+
try:
|
|
735
|
+
# Collect the states from the values in the dictionary
|
|
736
|
+
available_states = set(reaction_dict.values())
|
|
737
|
+
|
|
738
|
+
# Convert the states to full names
|
|
739
|
+
state_names = self.state_name_set(available_states)
|
|
740
|
+
|
|
741
|
+
# Determine phase based on the number of unique states
|
|
742
|
+
if len(state_names) == 1:
|
|
743
|
+
return f'{state_names[0]}'
|
|
744
|
+
else:
|
|
745
|
+
return f'{"-".join(state_names)}'
|
|
746
|
+
except Exception as e:
|
|
747
|
+
raise Exception(f"Error determining reaction phase: {e}")
|
|
748
|
+
|
|
749
|
+
def count_reaction_states(self, reaction_dict: Dict[str, str]) -> Dict[str, int]:
|
|
750
|
+
'''
|
|
751
|
+
Counts the number of unique states in a reaction as g, l, aq, or s.
|
|
752
|
+
|
|
753
|
+
Parameters
|
|
754
|
+
----------
|
|
755
|
+
reaction_dict: dict
|
|
756
|
+
A dictionary where keys are component names and values are their states.
|
|
757
|
+
|
|
758
|
+
Returns
|
|
759
|
+
-------
|
|
760
|
+
dict
|
|
761
|
+
A dictionary with the counts of each state (g, l, aq, s).
|
|
762
|
+
'''
|
|
763
|
+
try:
|
|
764
|
+
# Collect the states from the values in the dictionary
|
|
765
|
+
available_states = reaction_dict.values()
|
|
766
|
+
|
|
767
|
+
# how many g, l, aq, or s
|
|
768
|
+
state_count = {
|
|
769
|
+
'g': 0,
|
|
770
|
+
'l': 0,
|
|
771
|
+
'aq': 0,
|
|
772
|
+
's': 0
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
# Count the occurrences of each state
|
|
776
|
+
for state in available_states:
|
|
777
|
+
if state in state_count:
|
|
778
|
+
state_count[state] += 1
|
|
779
|
+
|
|
780
|
+
return state_count
|
|
781
|
+
|
|
782
|
+
except Exception as e:
|
|
783
|
+
raise Exception(f"Error determining reaction phase: {e}")
|
|
784
|
+
|
|
785
|
+
def reaction_phase_analysis(
|
|
786
|
+
self,
|
|
787
|
+
reaction_res: Dict[str, Any],
|
|
788
|
+
):
|
|
789
|
+
'''
|
|
790
|
+
Analyze the reaction phase and separate reactants and products by their phases.
|
|
791
|
+
|
|
792
|
+
Parameters
|
|
793
|
+
----------
|
|
794
|
+
reaction_res: dict
|
|
795
|
+
A dictionary containing the reaction results, including reactants and products.
|
|
796
|
+
|
|
797
|
+
Returns
|
|
798
|
+
-------
|
|
799
|
+
|
|
800
|
+
'''
|
|
801
|
+
try:
|
|
802
|
+
# NOTE: initialize phase dict
|
|
803
|
+
phase_dict = {
|
|
804
|
+
'g': [],
|
|
805
|
+
'l': [],
|
|
806
|
+
'aq': [],
|
|
807
|
+
's': []
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
# SECTION: Iterate over reactions and classify reactants and products by phase
|
|
811
|
+
for reaction_name, reaction_data in reaction_res.items():
|
|
812
|
+
# reactants
|
|
813
|
+
for reactant in reaction_data['reactants']:
|
|
814
|
+
phase = reactant['state']
|
|
815
|
+
if phase in phase_dict:
|
|
816
|
+
# ! molecule state
|
|
817
|
+
phase_dict[phase].append(reactant['molecule_state'])
|
|
818
|
+
else:
|
|
819
|
+
raise ValueError(
|
|
820
|
+
f"Unknown phase '{phase}' for reactant '{reactant['molecule']}'.")
|
|
821
|
+
|
|
822
|
+
# products
|
|
823
|
+
for product in reaction_data['products']:
|
|
824
|
+
phase = product['state']
|
|
825
|
+
if phase in phase_dict:
|
|
826
|
+
# ! molecule state
|
|
827
|
+
phase_dict[phase].append(product['molecule_state'])
|
|
828
|
+
else:
|
|
829
|
+
raise ValueError(
|
|
830
|
+
f"Unknown phase '{phase}' for product '{product['molecule']}'.")
|
|
831
|
+
|
|
832
|
+
# NOTE: remove duplicates in each phase
|
|
833
|
+
for phase in phase_dict:
|
|
834
|
+
phase_dict[phase] = list(set(phase_dict[phase]))
|
|
835
|
+
|
|
836
|
+
# res
|
|
837
|
+
return phase_dict
|
|
838
|
+
except Exception as e:
|
|
839
|
+
raise Exception(f"Error analyzing reaction phase: {e}")
|