pyreactlab-core 0.2.0__tar.gz → 0.3.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.
- {pyreactlab_core-0.2.0 → pyreactlab_core-0.3.0}/PKG-INFO +1 -1
- {pyreactlab_core-0.2.0 → pyreactlab_core-0.3.0}/pyproject.toml +1 -1
- {pyreactlab_core-0.2.0 → pyreactlab_core-0.3.0}/pyreactlab_core/configs/info.py +1 -1
- {pyreactlab_core-0.2.0 → pyreactlab_core-0.3.0}/pyreactlab_core/core/chem_react.py +62 -25
- pyreactlab_core-0.3.0/pyreactlab_core/core/chem_react_utils.py +344 -0
- {pyreactlab_core-0.2.0 → pyreactlab_core-0.3.0}/pyreactlab_core/models/reaction.py +37 -1
- pyreactlab_core-0.3.0/pyreactlab_core/models/reactions.py +30 -0
- {pyreactlab_core-0.2.0 → pyreactlab_core-0.3.0}/pyreactlab_core/utils/component_tools.py +1 -2
- {pyreactlab_core-0.2.0 → pyreactlab_core-0.3.0}/pyreactlab_core/utils/reaction_tools.py +1 -4
- {pyreactlab_core-0.2.0 → pyreactlab_core-0.3.0}/pyreactlab_core/utils/tools.py +1 -0
- {pyreactlab_core-0.2.0 → pyreactlab_core-0.3.0}/pyreactlab_core.egg-info/PKG-INFO +1 -1
- {pyreactlab_core-0.2.0 → pyreactlab_core-0.3.0}/pyreactlab_core.egg-info/SOURCES.txt +1 -0
- pyreactlab_core-0.2.0/pyreactlab_core/core/chem_react_utils.py +0 -181
- {pyreactlab_core-0.2.0 → pyreactlab_core-0.3.0}/LICENSE +0 -0
- {pyreactlab_core-0.2.0 → pyreactlab_core-0.3.0}/README.md +0 -0
- {pyreactlab_core-0.2.0 → pyreactlab_core-0.3.0}/pyreactlab_core/__init__.py +0 -0
- {pyreactlab_core-0.2.0 → pyreactlab_core-0.3.0}/pyreactlab_core/app.py +0 -0
- {pyreactlab_core-0.2.0 → pyreactlab_core-0.3.0}/pyreactlab_core/configs/__init__.py +0 -0
- {pyreactlab_core-0.2.0 → pyreactlab_core-0.3.0}/pyreactlab_core/configs/constants.py +0 -0
- {pyreactlab_core-0.2.0 → pyreactlab_core-0.3.0}/pyreactlab_core/core/__init__.py +0 -0
- {pyreactlab_core-0.2.0 → pyreactlab_core-0.3.0}/pyreactlab_core/core/reaction_component_mapper.py +0 -0
- {pyreactlab_core-0.2.0 → pyreactlab_core-0.3.0}/pyreactlab_core/core/reaction_network_analysis.py +0 -0
- {pyreactlab_core-0.2.0 → pyreactlab_core-0.3.0}/pyreactlab_core/docs/__init__.py +0 -0
- {pyreactlab_core-0.2.0 → pyreactlab_core-0.3.0}/pyreactlab_core/docs/chem_balance.py +0 -0
- {pyreactlab_core-0.2.0 → pyreactlab_core-0.3.0}/pyreactlab_core/docs/chem_utils.py +0 -0
- {pyreactlab_core-0.2.0 → pyreactlab_core-0.3.0}/pyreactlab_core/models/__init__.py +0 -0
- {pyreactlab_core-0.2.0 → pyreactlab_core-0.3.0}/pyreactlab_core/utils/__init__.py +0 -0
- {pyreactlab_core-0.2.0 → pyreactlab_core-0.3.0}/pyreactlab_core.egg-info/dependency_links.txt +0 -0
- {pyreactlab_core-0.2.0 → pyreactlab_core-0.3.0}/pyreactlab_core.egg-info/requires.txt +0 -0
- {pyreactlab_core-0.2.0 → pyreactlab_core-0.3.0}/pyreactlab_core.egg-info/top_level.txt +0 -0
- {pyreactlab_core-0.2.0 → pyreactlab_core-0.3.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyreactlab-core
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.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.
|
|
7
|
+
version = "0.3.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"
|
|
@@ -13,31 +13,12 @@ from ..configs.constants import (
|
|
|
13
13
|
from .chem_react_utils import ChemReactUtils
|
|
14
14
|
from .reaction_component_mapper import ReactionComponentMapper
|
|
15
15
|
from .reaction_network_analysis import ReactionNetworkAnalysis
|
|
16
|
+
from ..models.reactions import Reactant, Product, PhaseRule
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
# NOTE: logger
|
|
19
20
|
logger = logging.getLogger(__name__)
|
|
20
21
|
|
|
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
22
|
|
|
42
23
|
# SECTION: ChemReact class
|
|
43
24
|
class ChemReact(
|
|
@@ -185,7 +166,15 @@ class ChemReact(
|
|
|
185
166
|
# pattern = r'(\d*\.?\d+)?(\w+)\((\w)\)'
|
|
186
167
|
# pattern = r'(?:(\d*\.?\d+)\s*)?([A-Z][a-zA-Z0-9]*)\s*(?:\((\w)\))?'
|
|
187
168
|
# 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)\))?'
|
|
169
|
+
# 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)\))?'
|
|
170
|
+
|
|
171
|
+
# v2
|
|
172
|
+
pattern = r'(?:(\d*\.?\d+)\s*)?(e|[A-Z][A-Za-z0-9]*(?:\((?!(?:g|l|s|aq)\))[A-Za-z0-9]+\)\d*)*(?:[·*]\d*[A-Z][A-Za-z0-9]*(?:\((?!(?:g|l|s|aq)\))[A-Za-z0-9]+\)\d*)*)*)\s*(?:\{?((?:\d+)?[+-])\}?)?\s*(?:\((g|l|s|aq)\))?'
|
|
173
|
+
# ! parse as:
|
|
174
|
+
# 1. Optional coefficient (digits with optional decimal)
|
|
175
|
+
# 2. Molecule formula (starting with uppercase letter, followed by alphanumeric characters)
|
|
176
|
+
# 3. Optional charge (digits with optional sign)
|
|
177
|
+
# 4. Optional state (g, l, s, aq)
|
|
189
178
|
|
|
190
179
|
# SECTION: SECTION: Extract reactants and products
|
|
191
180
|
# Extract reactants
|
|
@@ -193,8 +182,9 @@ class ChemReact(
|
|
|
193
182
|
reactants: List[Reactant] = [
|
|
194
183
|
{
|
|
195
184
|
'coefficient': float(r[0]) if r[0] else float(1),
|
|
196
|
-
'molecule': r[1],
|
|
197
|
-
'
|
|
185
|
+
'molecule': self.parse_molecule(r[1], r[2]),
|
|
186
|
+
'charge': self.parse_charge(r[2]),
|
|
187
|
+
'state': r[3] if r[3] else phase_set,
|
|
198
188
|
'molecule_state': ''
|
|
199
189
|
} for r in reactants_raw
|
|
200
190
|
]
|
|
@@ -227,8 +217,9 @@ class ChemReact(
|
|
|
227
217
|
products: List[Product] = [
|
|
228
218
|
{
|
|
229
219
|
'coefficient': float(p[0]) if p[0] else float(1),
|
|
230
|
-
'molecule': p[1],
|
|
231
|
-
'
|
|
220
|
+
'molecule': self.parse_molecule(p[1], p[2]),
|
|
221
|
+
'charge': self.parse_charge(p[2]),
|
|
222
|
+
'state': p[3] if p[3] else phase_set,
|
|
232
223
|
'molecule_state': ''
|
|
233
224
|
} for p in products_raw
|
|
234
225
|
]
|
|
@@ -302,6 +293,18 @@ class ChemReact(
|
|
|
302
293
|
p['coefficient']
|
|
303
294
|
)
|
|
304
295
|
|
|
296
|
+
# NOTE total carbon count for reactants and products
|
|
297
|
+
total_carbon_count = self.count_total_carbon(
|
|
298
|
+
reactants=reactants,
|
|
299
|
+
products=products
|
|
300
|
+
)
|
|
301
|
+
# ? total reactant carbon count
|
|
302
|
+
total_reactant_carbon_count = total_carbon_count['total_reactant_carbon_count']
|
|
303
|
+
# ? total product carbon count
|
|
304
|
+
total_product_carbon_count = total_carbon_count['total_product_carbon_count']
|
|
305
|
+
# ? net carbon count
|
|
306
|
+
net_carbon_count = total_carbon_count['net_carbon_count']
|
|
307
|
+
|
|
305
308
|
# SECTION: reaction state
|
|
306
309
|
reaction_state = {}
|
|
307
310
|
for r in reactants:
|
|
@@ -311,6 +314,33 @@ class ChemReact(
|
|
|
311
314
|
# set
|
|
312
315
|
reaction_state[p['molecule_state']] = p['state']
|
|
313
316
|
|
|
317
|
+
# SECTION: charge count for each component
|
|
318
|
+
charge_count = {}
|
|
319
|
+
for r in reactants:
|
|
320
|
+
charge_count[r['molecule_state']] = self.count_charge(
|
|
321
|
+
r['molecule'],
|
|
322
|
+
r['coefficient'],
|
|
323
|
+
r['charge']
|
|
324
|
+
)
|
|
325
|
+
for p in products:
|
|
326
|
+
charge_count[p['molecule_state']] = self.count_charge(
|
|
327
|
+
p['molecule'],
|
|
328
|
+
p['coefficient'],
|
|
329
|
+
p['charge']
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
# SECTION: total charge count for reactants and products
|
|
333
|
+
total_charge_count = self.count_total_charge(
|
|
334
|
+
reactants=reactants,
|
|
335
|
+
products=products
|
|
336
|
+
)
|
|
337
|
+
# ? total reactant charge
|
|
338
|
+
total_reactant_charge = total_charge_count['total_reactant_charge']
|
|
339
|
+
# ? total product charge
|
|
340
|
+
total_product_charge = total_charge_count['total_product_charge']
|
|
341
|
+
# ? net charge
|
|
342
|
+
net_charge = total_charge_count['net_charge']
|
|
343
|
+
|
|
314
344
|
# NOTE: reaction phase
|
|
315
345
|
# reaction
|
|
316
346
|
reaction_phase = self.determine_reaction_phase(
|
|
@@ -408,9 +438,16 @@ class ChemReact(
|
|
|
408
438
|
'reaction_stoichiometry_matrix': reaction_stoichiometry_matrix,
|
|
409
439
|
'reaction_stoichiometry_source': stoichiometry_source,
|
|
410
440
|
'carbon_count': carbon_count,
|
|
441
|
+
'total_reactant_carbon_count': total_reactant_carbon_count,
|
|
442
|
+
'total_product_carbon_count': total_product_carbon_count,
|
|
443
|
+
'net_carbon_count': net_carbon_count,
|
|
411
444
|
'reaction_state': reaction_state,
|
|
412
445
|
'reaction_phase': reaction_phase,
|
|
413
446
|
'state_count': state_count,
|
|
447
|
+
'charge_count': charge_count,
|
|
448
|
+
'total_reactant_charge': total_reactant_charge,
|
|
449
|
+
'total_product_charge': total_product_charge,
|
|
450
|
+
'net_charge': net_charge,
|
|
414
451
|
'components': components,
|
|
415
452
|
'map_components': map_components,
|
|
416
453
|
'component_checker': self._component_checker,
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
# import libs
|
|
2
|
+
import re
|
|
3
|
+
from typing import Dict, List, Optional, Any
|
|
4
|
+
# locals
|
|
5
|
+
from ..models.reactions import Reactant, Product
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# SECTION: ChemReactUtils class
|
|
9
|
+
class ChemReactUtils:
|
|
10
|
+
"""General-purpose helpers for chemical reaction analysis."""
|
|
11
|
+
|
|
12
|
+
# NOTE: supported full phase names
|
|
13
|
+
available_phases = ("gas", "liquid", "aqueous", "solid")
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
available_phases: tuple[str, ...] | None = None,
|
|
18
|
+
):
|
|
19
|
+
"""
|
|
20
|
+
Initialize general chemical reaction utility settings.
|
|
21
|
+
"""
|
|
22
|
+
# SECTION: phase configuration
|
|
23
|
+
# NOTE: child classes can override the supported phase names
|
|
24
|
+
if available_phases is not None:
|
|
25
|
+
self.available_phases = available_phases
|
|
26
|
+
|
|
27
|
+
# ! ::: parse molecule
|
|
28
|
+
def parse_molecule(
|
|
29
|
+
self,
|
|
30
|
+
id: str,
|
|
31
|
+
charge: Any
|
|
32
|
+
) -> str:
|
|
33
|
+
"""
|
|
34
|
+
Parse molecule Id and charge to a formatted string. Charge is optional and will be appended using {+} or {-} notation if provided.
|
|
35
|
+
"""
|
|
36
|
+
try:
|
|
37
|
+
# SECTION: format molecule string
|
|
38
|
+
# NOTE: parse charge to ensure it is in the correct format
|
|
39
|
+
charge = self.parse_charge(charge)
|
|
40
|
+
|
|
41
|
+
# >> check if charge is non-zero, append it to the molecule Id
|
|
42
|
+
if charge == 1:
|
|
43
|
+
return f"{id}{{+}}"
|
|
44
|
+
elif charge == -1:
|
|
45
|
+
return f"{id}{{-}}"
|
|
46
|
+
elif charge > 0:
|
|
47
|
+
return f"{id}{{{charge}+}}"
|
|
48
|
+
elif charge < 0:
|
|
49
|
+
return f"{id}{{{abs(charge)}-}}"
|
|
50
|
+
elif charge == 0:
|
|
51
|
+
return id
|
|
52
|
+
else:
|
|
53
|
+
return id
|
|
54
|
+
except Exception as e:
|
|
55
|
+
raise Exception(
|
|
56
|
+
f"Error parsing molecule '{id}' with charge '{charge}': {e}"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# ! ::: count carbon
|
|
60
|
+
def count_carbon(self, molecule: str, coefficient: float) -> float:
|
|
61
|
+
"""
|
|
62
|
+
Count the total number of carbon atoms in a molecule,
|
|
63
|
+
scaled by the stoichiometric coefficient.
|
|
64
|
+
|
|
65
|
+
Examples
|
|
66
|
+
--------
|
|
67
|
+
CO2 -> 1 carbon
|
|
68
|
+
C2H6 -> 2 carbons
|
|
69
|
+
CH3COOH -> 2 carbons
|
|
70
|
+
CaCO3 -> 1 carbon
|
|
71
|
+
CuSO4 -> 0 carbon
|
|
72
|
+
C6H12O6 -> 6 carbons
|
|
73
|
+
"""
|
|
74
|
+
try:
|
|
75
|
+
# SECTION: validate inputs
|
|
76
|
+
# NOTE: molecule formula must be text for regex parsing
|
|
77
|
+
if not isinstance(molecule, str):
|
|
78
|
+
raise ValueError("Molecule must be a string.")
|
|
79
|
+
|
|
80
|
+
# NOTE: coefficient scales the carbon count
|
|
81
|
+
if not isinstance(coefficient, (int, float)):
|
|
82
|
+
raise ValueError("Coefficient must be an integer or float.")
|
|
83
|
+
|
|
84
|
+
# SECTION: find carbon atoms
|
|
85
|
+
# C(?![a-z]) ensures C is not part of Ca, Cu, Cl, Co, ...
|
|
86
|
+
# (\d*) captures an optional numeric subscript after C
|
|
87
|
+
matches = re.findall(r'C(?![a-z])(\d*)', molecule)
|
|
88
|
+
|
|
89
|
+
# SECTION: calculate carbon count
|
|
90
|
+
carbon_count = sum(
|
|
91
|
+
int(count) if count else 1
|
|
92
|
+
for count in matches
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
return carbon_count * coefficient
|
|
96
|
+
except Exception as e:
|
|
97
|
+
raise Exception(
|
|
98
|
+
f"Error counting carbon in molecule '{molecule}': {e}")
|
|
99
|
+
|
|
100
|
+
# ! ::: count total carbon
|
|
101
|
+
def count_total_carbon(
|
|
102
|
+
self,
|
|
103
|
+
reactants: List[Reactant],
|
|
104
|
+
products: List[Product]
|
|
105
|
+
) -> Dict[str, float]:
|
|
106
|
+
"""
|
|
107
|
+
Count the total number of carbon atoms in reactants and products.
|
|
108
|
+
"""
|
|
109
|
+
try:
|
|
110
|
+
# SECTION: calculate total carbon for reactants
|
|
111
|
+
total_reactant_carbon = sum(
|
|
112
|
+
self.count_carbon(r['molecule'], r['coefficient'])
|
|
113
|
+
for r in reactants
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# SECTION: calculate total carbon for products
|
|
117
|
+
total_product_carbon = sum(
|
|
118
|
+
self.count_carbon(p['molecule'], p['coefficient'])
|
|
119
|
+
for p in products
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# NOTE: return total carbon counts as a dictionary
|
|
123
|
+
return {
|
|
124
|
+
'total_reactant_carbon_count': total_reactant_carbon,
|
|
125
|
+
'total_product_carbon_count': total_product_carbon,
|
|
126
|
+
'net_carbon_count': total_product_carbon - total_reactant_carbon
|
|
127
|
+
}
|
|
128
|
+
except Exception as e:
|
|
129
|
+
raise Exception(f"Error counting total carbon in reaction: {e}")
|
|
130
|
+
|
|
131
|
+
# ! :::phase rule analysis
|
|
132
|
+
def phase_rule_analysis(self, phase_rule: Optional[str] = None) -> str:
|
|
133
|
+
"""
|
|
134
|
+
Analyze the phase rule of a reaction.
|
|
135
|
+
"""
|
|
136
|
+
try:
|
|
137
|
+
# SECTION: default phase rule
|
|
138
|
+
# NOTE: empty means component states must be present in the reaction
|
|
139
|
+
if phase_rule is None or phase_rule == 'None':
|
|
140
|
+
return 'empty'
|
|
141
|
+
|
|
142
|
+
# SECTION: validate phase rule
|
|
143
|
+
# ? keep this aligned with PhaseRule in chem_react.py
|
|
144
|
+
if phase_rule not in self.available_phases:
|
|
145
|
+
raise ValueError(
|
|
146
|
+
f"Phase rule must be {', '.join(self.available_phases)}.")
|
|
147
|
+
|
|
148
|
+
# SECTION: convert full phase name to reaction state symbol
|
|
149
|
+
if phase_rule == 'gas':
|
|
150
|
+
phase_symbol = 'g'
|
|
151
|
+
elif phase_rule == 'liquid':
|
|
152
|
+
phase_symbol = 'l'
|
|
153
|
+
elif phase_rule == 'aqueous':
|
|
154
|
+
phase_symbol = 'aq'
|
|
155
|
+
elif phase_rule == 'solid':
|
|
156
|
+
phase_symbol = 's'
|
|
157
|
+
else:
|
|
158
|
+
phase_symbol = 'empty'
|
|
159
|
+
|
|
160
|
+
# NOTE: return compact state symbol used by parsed components
|
|
161
|
+
return phase_symbol
|
|
162
|
+
except Exception as e:
|
|
163
|
+
raise Exception(f"Error analyzing phase rule: {e}")
|
|
164
|
+
|
|
165
|
+
# ! ::: state name set
|
|
166
|
+
def state_name_set(self, state_set: set) -> List[str]:
|
|
167
|
+
"""
|
|
168
|
+
Convert state set to full names.
|
|
169
|
+
"""
|
|
170
|
+
try:
|
|
171
|
+
# SECTION: state name mapping
|
|
172
|
+
# NOTE: keys match state symbols parsed from reaction strings
|
|
173
|
+
state_dict = {
|
|
174
|
+
'g': 'gas',
|
|
175
|
+
'l': 'liquid',
|
|
176
|
+
'aq': 'aqueous',
|
|
177
|
+
's': 'solid'
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
# NOTE: convert each compact symbol to its full phase name
|
|
181
|
+
return [state_dict[state] for state in state_set]
|
|
182
|
+
except Exception as e:
|
|
183
|
+
raise Exception(f"Error converting state set to full names: {e}")
|
|
184
|
+
|
|
185
|
+
# ! ::: determine reaction phase
|
|
186
|
+
def determine_reaction_phase(self, reaction_dict: Dict[str, str]) -> str:
|
|
187
|
+
"""
|
|
188
|
+
Determine the phase of a reaction based on component states.
|
|
189
|
+
"""
|
|
190
|
+
try:
|
|
191
|
+
# SECTION: collect unique states
|
|
192
|
+
available_states = set(reaction_dict.values())
|
|
193
|
+
# NOTE: convert state symbols before formatting phase text
|
|
194
|
+
state_names = self.state_name_set(available_states)
|
|
195
|
+
|
|
196
|
+
# SECTION: determine reaction phase label
|
|
197
|
+
if len(state_names) == 1:
|
|
198
|
+
# NOTE: single-phase reaction
|
|
199
|
+
return f'{state_names[0]}'
|
|
200
|
+
else:
|
|
201
|
+
# NOTE: multi-phase reaction
|
|
202
|
+
return f'{"-".join(state_names)}'
|
|
203
|
+
except Exception as e:
|
|
204
|
+
raise Exception(f"Error determining reaction phase: {e}")
|
|
205
|
+
|
|
206
|
+
# ! ::: count reaction states
|
|
207
|
+
def count_reaction_states(self, reaction_dict: Dict[str, str]) -> Dict[str, int]:
|
|
208
|
+
"""
|
|
209
|
+
Count the number of component states in a reaction.
|
|
210
|
+
"""
|
|
211
|
+
try:
|
|
212
|
+
# SECTION: collect component states
|
|
213
|
+
available_states = reaction_dict.values()
|
|
214
|
+
# NOTE: initialize all supported state buckets
|
|
215
|
+
state_count = {
|
|
216
|
+
'g': 0,
|
|
217
|
+
'l': 0,
|
|
218
|
+
'aq': 0,
|
|
219
|
+
's': 0
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
# SECTION: count state occurrences
|
|
223
|
+
for state in available_states:
|
|
224
|
+
# ! ignore unsupported states instead of adding new keys
|
|
225
|
+
if state in state_count:
|
|
226
|
+
state_count[state] += 1
|
|
227
|
+
|
|
228
|
+
# NOTE: return counts for every supported state symbol
|
|
229
|
+
return state_count
|
|
230
|
+
except Exception as e:
|
|
231
|
+
raise Exception(f"Error determining reaction phase: {e}")
|
|
232
|
+
|
|
233
|
+
# ! ::: reaction types
|
|
234
|
+
def get_reaction_type(self, reaction_mode_symbol: str) -> str:
|
|
235
|
+
"""
|
|
236
|
+
Determine the type of reaction based on the reaction mode symbol.
|
|
237
|
+
|
|
238
|
+
Reaction Mode Symbols:
|
|
239
|
+
- `Reversible`: "<=>"
|
|
240
|
+
- `Irreversible`: "=>"
|
|
241
|
+
- `Equilibrium`: "="
|
|
242
|
+
"""
|
|
243
|
+
try:
|
|
244
|
+
# SECTION: validate reaction mode symbol
|
|
245
|
+
if reaction_mode_symbol not in ("<=>", "=>", "="):
|
|
246
|
+
raise ValueError(
|
|
247
|
+
f"Invalid reaction mode symbol: {reaction_mode_symbol}")
|
|
248
|
+
|
|
249
|
+
# SECTION: determine reaction type
|
|
250
|
+
if reaction_mode_symbol == "<=>":
|
|
251
|
+
return "reversible"
|
|
252
|
+
elif reaction_mode_symbol == "=>":
|
|
253
|
+
return "irreversible"
|
|
254
|
+
elif reaction_mode_symbol == "=":
|
|
255
|
+
return "equilibrium"
|
|
256
|
+
else:
|
|
257
|
+
raise ValueError(
|
|
258
|
+
f"Unknown reaction mode symbol: {reaction_mode_symbol}")
|
|
259
|
+
except Exception as e:
|
|
260
|
+
raise Exception(f"Error determining reaction type: {e}")
|
|
261
|
+
|
|
262
|
+
# ! ::: parse charge
|
|
263
|
+
def parse_charge(self, charge: str) -> int:
|
|
264
|
+
"""
|
|
265
|
+
Convert reaction charge notation to an integer charge.
|
|
266
|
+
"""
|
|
267
|
+
try:
|
|
268
|
+
# SECTION: empty or missing charge
|
|
269
|
+
if charge == "":
|
|
270
|
+
return 0
|
|
271
|
+
|
|
272
|
+
# SECTION: normalize notation
|
|
273
|
+
charge = charge.strip()
|
|
274
|
+
if charge in ("+", "-"):
|
|
275
|
+
return 1 if charge == "+" else -1
|
|
276
|
+
|
|
277
|
+
# SECTION: charge magnitude with trailing sign, e.g. 2+ or 3-
|
|
278
|
+
match = re.fullmatch(r"(\d+)([+-])", charge)
|
|
279
|
+
if match:
|
|
280
|
+
magnitude = int(match.group(1))
|
|
281
|
+
sign = match.group(2)
|
|
282
|
+
return magnitude if sign == "+" else -magnitude
|
|
283
|
+
|
|
284
|
+
# SECTION: signed integer fallback, e.g. +2 or -2
|
|
285
|
+
return int(charge)
|
|
286
|
+
except Exception as e:
|
|
287
|
+
raise Exception(f"Error parsing charge '{charge}': {e}")
|
|
288
|
+
|
|
289
|
+
# ! ::: count charge
|
|
290
|
+
def count_charge(
|
|
291
|
+
self,
|
|
292
|
+
molecule: str,
|
|
293
|
+
coefficient: float,
|
|
294
|
+
charge: int
|
|
295
|
+
) -> float:
|
|
296
|
+
"""
|
|
297
|
+
Count the total charge of a molecule based on its charge and coefficient.
|
|
298
|
+
"""
|
|
299
|
+
try:
|
|
300
|
+
# SECTION: validate inputs
|
|
301
|
+
if not isinstance(molecule, str):
|
|
302
|
+
raise ValueError("Molecule must be a string.")
|
|
303
|
+
if not isinstance(coefficient, (int, float)):
|
|
304
|
+
raise ValueError("Coefficient must be an integer or float.")
|
|
305
|
+
if not isinstance(charge, int):
|
|
306
|
+
raise ValueError("Charge must be an integer.")
|
|
307
|
+
|
|
308
|
+
# SECTION: calculate total charge
|
|
309
|
+
total_charge = charge * coefficient
|
|
310
|
+
return total_charge
|
|
311
|
+
except Exception as e:
|
|
312
|
+
raise Exception(
|
|
313
|
+
f"Error counting charge in molecule '{molecule}': {e}")
|
|
314
|
+
|
|
315
|
+
# ! ::: count total charge in reaction
|
|
316
|
+
def count_total_charge(
|
|
317
|
+
self,
|
|
318
|
+
reactants: List[Reactant],
|
|
319
|
+
products: List[Product]
|
|
320
|
+
) -> Dict[str, float]:
|
|
321
|
+
"""
|
|
322
|
+
Count the total charge of reactants and products in a reaction.
|
|
323
|
+
"""
|
|
324
|
+
try:
|
|
325
|
+
# SECTION: calculate total charge for reactants
|
|
326
|
+
total_reactant_charge = sum(
|
|
327
|
+
self.count_charge(r['molecule'], r['coefficient'], r['charge'])
|
|
328
|
+
for r in reactants
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
# SECTION: calculate total charge for products
|
|
332
|
+
total_product_charge = sum(
|
|
333
|
+
self.count_charge(p['molecule'], p['coefficient'], p['charge'])
|
|
334
|
+
for p in products
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
# NOTE: return total charges as a dictionary
|
|
338
|
+
return {
|
|
339
|
+
'total_reactant_charge': total_reactant_charge,
|
|
340
|
+
'total_product_charge': total_product_charge,
|
|
341
|
+
'net_charge': total_product_charge - total_reactant_charge
|
|
342
|
+
}
|
|
343
|
+
except Exception as e:
|
|
344
|
+
raise Exception(f"Error counting total charge in reaction: {e}")
|
|
@@ -28,7 +28,7 @@ class Reaction(BaseModel):
|
|
|
28
28
|
analysis : Dict[str, Any]
|
|
29
29
|
A dictionary containing the analysis results of the reaction.
|
|
30
30
|
component_keys : List[ComponentKey]
|
|
31
|
-
The key used to identify components in the reaction analysis, with a default value of ["Formula-State", "Name-State", "Name-Formula"].
|
|
31
|
+
The key used to identify components in the reaction analysis, with a default value of ["Formula-State", "Name-State", "Name-Formula", "Name-Formula-State"].
|
|
32
32
|
|
|
33
33
|
Properties
|
|
34
34
|
----------
|
|
@@ -87,6 +87,7 @@ class Reaction(BaseModel):
|
|
|
87
87
|
"Formula-State",
|
|
88
88
|
"Name-State",
|
|
89
89
|
"Name-Formula",
|
|
90
|
+
"Name-Formula-State",
|
|
90
91
|
],
|
|
91
92
|
description="The key used to identify components in the reaction analysis."
|
|
92
93
|
)
|
|
@@ -185,6 +186,21 @@ class Reaction(BaseModel):
|
|
|
185
186
|
def carbon_count(self) -> Dict[str, float]:
|
|
186
187
|
return self.analysis.get("carbon_count", {})
|
|
187
188
|
|
|
189
|
+
@computed_field
|
|
190
|
+
@property
|
|
191
|
+
def net_carbon_count(self) -> float:
|
|
192
|
+
return self.analysis.get("net_carbon_count", 0.0)
|
|
193
|
+
|
|
194
|
+
@computed_field
|
|
195
|
+
@property
|
|
196
|
+
def total_reactant_carbon_count(self) -> float:
|
|
197
|
+
return self.analysis.get("total_reactant_carbon_count", 0.0)
|
|
198
|
+
|
|
199
|
+
@computed_field
|
|
200
|
+
@property
|
|
201
|
+
def total_product_carbon_count(self) -> float:
|
|
202
|
+
return self.analysis.get("total_product_carbon_count", 0.0)
|
|
203
|
+
|
|
188
204
|
@computed_field
|
|
189
205
|
@property
|
|
190
206
|
def reaction_state(self) -> Dict[str, str]:
|
|
@@ -200,6 +216,26 @@ class Reaction(BaseModel):
|
|
|
200
216
|
def state_count(self) -> Dict[str, int]:
|
|
201
217
|
return self.analysis.get("state_count", {})
|
|
202
218
|
|
|
219
|
+
@computed_field
|
|
220
|
+
@property
|
|
221
|
+
def charge_count(self) -> Dict[str, int]:
|
|
222
|
+
return self.analysis.get("charge_count", {})
|
|
223
|
+
|
|
224
|
+
@computed_field
|
|
225
|
+
@property
|
|
226
|
+
def total_reactant_charge(self) -> int:
|
|
227
|
+
return self.analysis.get("total_reactant_charge", 0)
|
|
228
|
+
|
|
229
|
+
@computed_field
|
|
230
|
+
@property
|
|
231
|
+
def total_product_charge(self) -> int:
|
|
232
|
+
return self.analysis.get("total_product_charge", 0)
|
|
233
|
+
|
|
234
|
+
@computed_field
|
|
235
|
+
@property
|
|
236
|
+
def net_charge(self) -> int:
|
|
237
|
+
return self.analysis.get("net_charge", 0)
|
|
238
|
+
|
|
203
239
|
@computed_field
|
|
204
240
|
@property
|
|
205
241
|
def component_ids(self) -> Dict[str, int]:
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# import libs
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
from typing import TypedDict, Literal, Dict, List, Optional
|
|
4
|
+
from pydantic import BaseModel, Field
|
|
5
|
+
from pythermodb_settings.models import Component
|
|
6
|
+
# locals
|
|
7
|
+
|
|
8
|
+
# SECTION: Models
|
|
9
|
+
# NOTE: Phase Rule
|
|
10
|
+
PhaseRule = Literal["gas", "liquid", "aqueous", "solid"]
|
|
11
|
+
|
|
12
|
+
# NOTE: reactants
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Reactant(TypedDict):
|
|
16
|
+
coefficient: float
|
|
17
|
+
molecule: str
|
|
18
|
+
charge: int
|
|
19
|
+
state: str
|
|
20
|
+
molecule_state: str
|
|
21
|
+
|
|
22
|
+
# NOTE: products
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Product(TypedDict):
|
|
26
|
+
coefficient: float
|
|
27
|
+
molecule: str
|
|
28
|
+
charge: int
|
|
29
|
+
state: str
|
|
30
|
+
molecule_state: str
|
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
# import libs
|
|
2
2
|
import logging
|
|
3
|
-
from typing import
|
|
3
|
+
from typing import cast, NamedTuple
|
|
4
4
|
# locals
|
|
5
5
|
from ..configs.constants import (
|
|
6
|
-
IRREVERSIBLE_REACTION_MODE_SYMBOLS,
|
|
7
|
-
REVERSIBLE_REACTION_MODE_SYMBOLS,
|
|
8
|
-
EQUILIBRIUM_REACTION_MODE_SYMBOLS,
|
|
9
6
|
ReactionMode,
|
|
10
7
|
ReactionDirection,
|
|
11
8
|
ReactionType,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyreactlab-core
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.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
|
|
@@ -21,6 +21,7 @@ pyreactlab_core/docs/chem_balance.py
|
|
|
21
21
|
pyreactlab_core/docs/chem_utils.py
|
|
22
22
|
pyreactlab_core/models/__init__.py
|
|
23
23
|
pyreactlab_core/models/reaction.py
|
|
24
|
+
pyreactlab_core/models/reactions.py
|
|
24
25
|
pyreactlab_core/utils/__init__.py
|
|
25
26
|
pyreactlab_core/utils/component_tools.py
|
|
26
27
|
pyreactlab_core/utils/reaction_tools.py
|
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
# import libs
|
|
2
|
-
import re
|
|
3
|
-
from typing import Dict, List, Optional
|
|
4
|
-
# locals
|
|
5
|
-
from ..configs.constants import REACTION_SYMBOLIC_MODES
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
# SECTION: ChemReactUtils class
|
|
9
|
-
class ChemReactUtils:
|
|
10
|
-
"""General-purpose helpers for chemical reaction analysis."""
|
|
11
|
-
|
|
12
|
-
# NOTE: supported full phase names
|
|
13
|
-
available_phases = ("gas", "liquid", "aqueous", "solid")
|
|
14
|
-
|
|
15
|
-
def __init__(
|
|
16
|
-
self,
|
|
17
|
-
available_phases: tuple[str, ...] | None = None,
|
|
18
|
-
):
|
|
19
|
-
"""
|
|
20
|
-
Initialize general chemical reaction utility settings.
|
|
21
|
-
"""
|
|
22
|
-
# SECTION: phase configuration
|
|
23
|
-
# NOTE: child classes can override the supported phase names
|
|
24
|
-
if available_phases is not None:
|
|
25
|
-
self.available_phases = available_phases
|
|
26
|
-
|
|
27
|
-
def count_carbon(self, molecule: str, coefficient: float) -> float:
|
|
28
|
-
"""
|
|
29
|
-
Count the number of carbon atoms in a molecule.
|
|
30
|
-
"""
|
|
31
|
-
try:
|
|
32
|
-
# SECTION: validate inputs
|
|
33
|
-
# NOTE: molecule formula must be text for regex parsing
|
|
34
|
-
if not isinstance(molecule, str):
|
|
35
|
-
raise ValueError("Molecule must be a string.")
|
|
36
|
-
|
|
37
|
-
# NOTE: coefficient scales the carbon count
|
|
38
|
-
if not isinstance(coefficient, (int, float)):
|
|
39
|
-
raise ValueError("Coefficient must be an integer or float.")
|
|
40
|
-
|
|
41
|
-
# SECTION: carbon symbol matching
|
|
42
|
-
# ! do not count lowercase carbon inside another element symbol
|
|
43
|
-
if re.search(r'C(?![a-z])', molecule):
|
|
44
|
-
# NOTE: multiply atom occurrences by stoichiometric coefficient
|
|
45
|
-
carbon_count = len(re.findall(
|
|
46
|
-
r'C(?![a-z])', molecule)) * coefficient
|
|
47
|
-
return carbon_count
|
|
48
|
-
else:
|
|
49
|
-
# NOTE: molecule has no carbon atoms
|
|
50
|
-
return 0.0
|
|
51
|
-
except Exception as e:
|
|
52
|
-
raise Exception(
|
|
53
|
-
f"Error counting carbon in molecule '{molecule}': {e}")
|
|
54
|
-
|
|
55
|
-
def phase_rule_analysis(self, phase_rule: Optional[str] = None) -> str:
|
|
56
|
-
"""
|
|
57
|
-
Analyze the phase rule of a reaction.
|
|
58
|
-
"""
|
|
59
|
-
try:
|
|
60
|
-
# SECTION: default phase rule
|
|
61
|
-
# NOTE: empty means component states must be present in the reaction
|
|
62
|
-
if phase_rule is None or phase_rule == 'None':
|
|
63
|
-
return 'empty'
|
|
64
|
-
|
|
65
|
-
# SECTION: validate phase rule
|
|
66
|
-
# ? keep this aligned with PhaseRule in chem_react.py
|
|
67
|
-
if phase_rule not in self.available_phases:
|
|
68
|
-
raise ValueError(
|
|
69
|
-
f"Phase rule must be {', '.join(self.available_phases)}.")
|
|
70
|
-
|
|
71
|
-
# SECTION: convert full phase name to reaction state symbol
|
|
72
|
-
if phase_rule == 'gas':
|
|
73
|
-
phase_symbol = 'g'
|
|
74
|
-
elif phase_rule == 'liquid':
|
|
75
|
-
phase_symbol = 'l'
|
|
76
|
-
elif phase_rule == 'aqueous':
|
|
77
|
-
phase_symbol = 'aq'
|
|
78
|
-
elif phase_rule == 'solid':
|
|
79
|
-
phase_symbol = 's'
|
|
80
|
-
else:
|
|
81
|
-
phase_symbol = 'empty'
|
|
82
|
-
|
|
83
|
-
# NOTE: return compact state symbol used by parsed components
|
|
84
|
-
return phase_symbol
|
|
85
|
-
except Exception as e:
|
|
86
|
-
raise Exception(f"Error analyzing phase rule: {e}")
|
|
87
|
-
|
|
88
|
-
def state_name_set(self, state_set: set) -> List[str]:
|
|
89
|
-
"""
|
|
90
|
-
Convert state set to full names.
|
|
91
|
-
"""
|
|
92
|
-
try:
|
|
93
|
-
# SECTION: state name mapping
|
|
94
|
-
# NOTE: keys match state symbols parsed from reaction strings
|
|
95
|
-
state_dict = {
|
|
96
|
-
'g': 'gas',
|
|
97
|
-
'l': 'liquid',
|
|
98
|
-
'aq': 'aqueous',
|
|
99
|
-
's': 'solid'
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
# NOTE: convert each compact symbol to its full phase name
|
|
103
|
-
return [state_dict[state] for state in state_set]
|
|
104
|
-
except Exception as e:
|
|
105
|
-
raise Exception(f"Error converting state set to full names: {e}")
|
|
106
|
-
|
|
107
|
-
def determine_reaction_phase(self, reaction_dict: Dict[str, str]) -> str:
|
|
108
|
-
"""
|
|
109
|
-
Determine the phase of a reaction based on component states.
|
|
110
|
-
"""
|
|
111
|
-
try:
|
|
112
|
-
# SECTION: collect unique states
|
|
113
|
-
available_states = set(reaction_dict.values())
|
|
114
|
-
# NOTE: convert state symbols before formatting phase text
|
|
115
|
-
state_names = self.state_name_set(available_states)
|
|
116
|
-
|
|
117
|
-
# SECTION: determine reaction phase label
|
|
118
|
-
if len(state_names) == 1:
|
|
119
|
-
# NOTE: single-phase reaction
|
|
120
|
-
return f'{state_names[0]}'
|
|
121
|
-
else:
|
|
122
|
-
# NOTE: multi-phase reaction
|
|
123
|
-
return f'{"-".join(state_names)}'
|
|
124
|
-
except Exception as e:
|
|
125
|
-
raise Exception(f"Error determining reaction phase: {e}")
|
|
126
|
-
|
|
127
|
-
def count_reaction_states(self, reaction_dict: Dict[str, str]) -> Dict[str, int]:
|
|
128
|
-
"""
|
|
129
|
-
Count the number of component states in a reaction.
|
|
130
|
-
"""
|
|
131
|
-
try:
|
|
132
|
-
# SECTION: collect component states
|
|
133
|
-
available_states = reaction_dict.values()
|
|
134
|
-
# NOTE: initialize all supported state buckets
|
|
135
|
-
state_count = {
|
|
136
|
-
'g': 0,
|
|
137
|
-
'l': 0,
|
|
138
|
-
'aq': 0,
|
|
139
|
-
's': 0
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
# SECTION: count state occurrences
|
|
143
|
-
for state in available_states:
|
|
144
|
-
# ! ignore unsupported states instead of adding new keys
|
|
145
|
-
if state in state_count:
|
|
146
|
-
state_count[state] += 1
|
|
147
|
-
|
|
148
|
-
# NOTE: return counts for every supported state symbol
|
|
149
|
-
return state_count
|
|
150
|
-
except Exception as e:
|
|
151
|
-
raise Exception(f"Error determining reaction phase: {e}")
|
|
152
|
-
|
|
153
|
-
# ! reaction types
|
|
154
|
-
|
|
155
|
-
def get_reaction_type(self, reaction_mode_symbol: str) -> str:
|
|
156
|
-
"""
|
|
157
|
-
Determine the type of reaction based on the reaction mode symbol.
|
|
158
|
-
|
|
159
|
-
Reaction Mode Symbols:
|
|
160
|
-
- `Reversible`: "<=>"
|
|
161
|
-
- `Irreversible`: "=>"
|
|
162
|
-
- `Equilibrium`: "="
|
|
163
|
-
"""
|
|
164
|
-
try:
|
|
165
|
-
# SECTION: validate reaction mode symbol
|
|
166
|
-
if reaction_mode_symbol not in ("<=>", "=>", "="):
|
|
167
|
-
raise ValueError(
|
|
168
|
-
f"Invalid reaction mode symbol: {reaction_mode_symbol}")
|
|
169
|
-
|
|
170
|
-
# SECTION: determine reaction type
|
|
171
|
-
if reaction_mode_symbol == "<=>":
|
|
172
|
-
return "reversible"
|
|
173
|
-
elif reaction_mode_symbol == "=>":
|
|
174
|
-
return "irreversible"
|
|
175
|
-
elif reaction_mode_symbol == "=":
|
|
176
|
-
return "equilibrium"
|
|
177
|
-
else:
|
|
178
|
-
raise ValueError(
|
|
179
|
-
f"Unknown reaction mode symbol: {reaction_mode_symbol}")
|
|
180
|
-
except Exception as e:
|
|
181
|
-
raise Exception(f"Error determining reaction type: {e}")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pyreactlab_core-0.2.0 → pyreactlab_core-0.3.0}/pyreactlab_core/core/reaction_component_mapper.py
RENAMED
|
File without changes
|
{pyreactlab_core-0.2.0 → pyreactlab_core-0.3.0}/pyreactlab_core/core/reaction_network_analysis.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pyreactlab_core-0.2.0 → pyreactlab_core-0.3.0}/pyreactlab_core.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|