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.
@@ -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}")