chemql-python 0.1.0__tar.gz → 0.2.1__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.
- {chemql_python-0.1.0/src/chemql_python.egg-info → chemql_python-0.2.1}/PKG-INFO +14 -2
- {chemql_python-0.1.0 → chemql_python-0.2.1}/README.md +13 -1
- {chemql_python-0.1.0 → chemql_python-0.2.1}/pyproject.toml +1 -1
- {chemql_python-0.1.0 → chemql_python-0.2.1}/src/chemql/__init__.py +184 -4
- {chemql_python-0.1.0 → chemql_python-0.2.1/src/chemql_python.egg-info}/PKG-INFO +14 -2
- {chemql_python-0.1.0 → chemql_python-0.2.1}/tests/test_public_api.py +2 -1
- {chemql_python-0.1.0 → chemql_python-0.2.1}/tests/test_reactions.py +39 -0
- {chemql_python-0.1.0 → chemql_python-0.2.1}/LICENSE +0 -0
- {chemql_python-0.1.0 → chemql_python-0.2.1}/setup.cfg +0 -0
- {chemql_python-0.1.0 → chemql_python-0.2.1}/src/chemql/__main__.py +0 -0
- {chemql_python-0.1.0 → chemql_python-0.2.1}/src/chemql/data/BondEnergies.json +0 -0
- {chemql_python-0.1.0 → chemql_python-0.2.1}/src/chemql/data/MoleculeBonds.json +0 -0
- {chemql_python-0.1.0 → chemql_python-0.2.1}/src/chemql/data/Molecules.json +0 -0
- {chemql_python-0.1.0 → chemql_python-0.2.1}/src/chemql/data/PeriodicTableJSON.json +0 -0
- {chemql_python-0.1.0 → chemql_python-0.2.1}/src/chemql/data/Reactions.json +0 -0
- {chemql_python-0.1.0 → chemql_python-0.2.1}/src/chemql/data/THIRD_PARTY_LICENSES/PeriodicTableJSON.txt +0 -0
- {chemql_python-0.1.0 → chemql_python-0.2.1}/src/chemql/data/THIRD_PARTY_LICENSES/RHEA.txt +0 -0
- {chemql_python-0.1.0 → chemql_python-0.2.1}/src/chemql/html/periodic_table.html +0 -0
- {chemql_python-0.1.0 → chemql_python-0.2.1}/src/chemql_python.egg-info/SOURCES.txt +0 -0
- {chemql_python-0.1.0 → chemql_python-0.2.1}/src/chemql_python.egg-info/dependency_links.txt +0 -0
- {chemql_python-0.1.0 → chemql_python-0.2.1}/src/chemql_python.egg-info/entry_points.txt +0 -0
- {chemql_python-0.1.0 → chemql_python-0.2.1}/src/chemql_python.egg-info/requires.txt +0 -0
- {chemql_python-0.1.0 → chemql_python-0.2.1}/src/chemql_python.egg-info/top_level.txt +0 -0
- {chemql_python-0.1.0 → chemql_python-0.2.1}/tests/test_external_molecule_lookup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: chemql-python
|
|
3
|
-
Version: 0.1
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: A chemistry-focused query language and REPL
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -12,6 +12,8 @@ Dynamic: license-file
|
|
|
12
12
|
|
|
13
13
|
# ChemQL
|
|
14
14
|
|
|
15
|
+

|
|
16
|
+
|
|
15
17
|
ChemQL is a chemistry-focused query language and REPL for exploring element, molecule, and reaction datasets. It lets you search the built-in chemistry data, filter results with expressions, set reaction conditions, and combine queries with small Python snippets when needed.
|
|
16
18
|
|
|
17
19
|
## What ChemQL does
|
|
@@ -80,9 +82,11 @@ The Python package is `chemql`. Its query engine, chemistry types, and bundled
|
|
|
80
82
|
datasets can be imported directly:
|
|
81
83
|
|
|
82
84
|
```python
|
|
83
|
-
from chemql import execute_query_text
|
|
85
|
+
from chemql import balance_stoichiometry, execute_query_text
|
|
84
86
|
|
|
85
87
|
result = execute_query_text('search elements name = "Hydrogen"')
|
|
88
|
+
reaction = balance_stoichiometry(["O2", "H2"], ["H2O"], reversible=True)
|
|
89
|
+
print(reaction)
|
|
86
90
|
```
|
|
87
91
|
|
|
88
92
|
## Core query syntax
|
|
@@ -159,10 +163,18 @@ add catalyst "Vanadium oxide"
|
|
|
159
163
|
remove catalyst "Vanadium oxide"
|
|
160
164
|
conditions
|
|
161
165
|
react N2 3H2
|
|
166
|
+
balance O2 + H2 -> H2O
|
|
167
|
+
balance O2 + H2 <-> H2O
|
|
162
168
|
```
|
|
163
169
|
|
|
164
170
|
Temperature accepts `C`, `F`, or `K`; pressure accepts `Pa`, `N/m^2`, `Nm^-2`, `bar`, or `atm`. The keywords `standard` and `room` are also accepted.
|
|
165
171
|
|
|
172
|
+
`balance` accepts chemical formulas on each side, separated by `+`. It returns
|
|
173
|
+
a `Reaction` with the smallest positive integer stoichiometric coefficients.
|
|
174
|
+
The arrow determines reaction direction: `->` is irreversible and `<->` is
|
|
175
|
+
reversible. `//` begins a comment, as it does elsewhere in ChemQL. The function
|
|
176
|
+
API is `balance_stoichiometry(reactants, products, reversible=None)`.
|
|
177
|
+
|
|
166
178
|
## Operators
|
|
167
179
|
|
|
168
180
|
ChemQL supports these comparison operators:
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# ChemQL
|
|
2
2
|
|
|
3
|
+

|
|
4
|
+
|
|
3
5
|
ChemQL is a chemistry-focused query language and REPL for exploring element, molecule, and reaction datasets. It lets you search the built-in chemistry data, filter results with expressions, set reaction conditions, and combine queries with small Python snippets when needed.
|
|
4
6
|
|
|
5
7
|
## What ChemQL does
|
|
@@ -68,9 +70,11 @@ The Python package is `chemql`. Its query engine, chemistry types, and bundled
|
|
|
68
70
|
datasets can be imported directly:
|
|
69
71
|
|
|
70
72
|
```python
|
|
71
|
-
from chemql import execute_query_text
|
|
73
|
+
from chemql import balance_stoichiometry, execute_query_text
|
|
72
74
|
|
|
73
75
|
result = execute_query_text('search elements name = "Hydrogen"')
|
|
76
|
+
reaction = balance_stoichiometry(["O2", "H2"], ["H2O"], reversible=True)
|
|
77
|
+
print(reaction)
|
|
74
78
|
```
|
|
75
79
|
|
|
76
80
|
## Core query syntax
|
|
@@ -147,10 +151,18 @@ add catalyst "Vanadium oxide"
|
|
|
147
151
|
remove catalyst "Vanadium oxide"
|
|
148
152
|
conditions
|
|
149
153
|
react N2 3H2
|
|
154
|
+
balance O2 + H2 -> H2O
|
|
155
|
+
balance O2 + H2 <-> H2O
|
|
150
156
|
```
|
|
151
157
|
|
|
152
158
|
Temperature accepts `C`, `F`, or `K`; pressure accepts `Pa`, `N/m^2`, `Nm^-2`, `bar`, or `atm`. The keywords `standard` and `room` are also accepted.
|
|
153
159
|
|
|
160
|
+
`balance` accepts chemical formulas on each side, separated by `+`. It returns
|
|
161
|
+
a `Reaction` with the smallest positive integer stoichiometric coefficients.
|
|
162
|
+
The arrow determines reaction direction: `->` is irreversible and `<->` is
|
|
163
|
+
reversible. `//` begins a comment, as it does elsewhere in ChemQL. The function
|
|
164
|
+
API is `balance_stoichiometry(reactants, products, reversible=None)`.
|
|
165
|
+
|
|
154
166
|
## Operators
|
|
155
167
|
|
|
156
168
|
ChemQL supports these comparison operators:
|
|
@@ -13,6 +13,9 @@ from prompt_toolkit.completion import Completer, Completion
|
|
|
13
13
|
import urllib.parse
|
|
14
14
|
import urllib.request
|
|
15
15
|
import urllib.error
|
|
16
|
+
from fractions import Fraction
|
|
17
|
+
from functools import reduce
|
|
18
|
+
from math import gcd
|
|
16
19
|
|
|
17
20
|
|
|
18
21
|
__all__ = [
|
|
@@ -22,6 +25,7 @@ __all__ = [
|
|
|
22
25
|
"Reaction",
|
|
23
26
|
"ReturnTable",
|
|
24
27
|
"Unknown",
|
|
28
|
+
"balance_stoichiometry",
|
|
25
29
|
"execute_query",
|
|
26
30
|
"execute_query_text",
|
|
27
31
|
]
|
|
@@ -918,7 +922,170 @@ reactionKeys = ("id", "name", "equation", "reaction_type", "reversible", "reacta
|
|
|
918
922
|
del elements
|
|
919
923
|
del reactions
|
|
920
924
|
del molecules
|
|
921
|
-
|
|
925
|
+
|
|
926
|
+
|
|
927
|
+
def _formula_counts(formula):
|
|
928
|
+
if not isinstance(formula, str) or not formula:
|
|
929
|
+
raise ValueError("Chemical formulas must be non-empty strings")
|
|
930
|
+
symbols = {element.symbol for element in ELEMENTS}
|
|
931
|
+
|
|
932
|
+
def parse_group(index, nested=False):
|
|
933
|
+
counts = {}
|
|
934
|
+
while index < len(formula):
|
|
935
|
+
char = formula[index]
|
|
936
|
+
if char == ")":
|
|
937
|
+
if not nested:
|
|
938
|
+
raise ValueError(f"Unmatched closing parenthesis in formula `{formula}`")
|
|
939
|
+
return counts, index
|
|
940
|
+
if char == "(":
|
|
941
|
+
group, index = parse_group(index + 1, True)
|
|
942
|
+
if index >= len(formula) or formula[index] != ")":
|
|
943
|
+
raise ValueError(f"Unclosed parenthesis in formula `{formula}`")
|
|
944
|
+
index += 1
|
|
945
|
+
match = re.match(r"\d+", formula[index:])
|
|
946
|
+
multiplier = int(match.group()) if match else 1
|
|
947
|
+
if multiplier <= 0:
|
|
948
|
+
raise ValueError(f"Invalid atom count in formula `{formula}`")
|
|
949
|
+
if match:
|
|
950
|
+
index += len(match.group())
|
|
951
|
+
for symbol, amount in group.items():
|
|
952
|
+
counts[symbol] = counts.get(symbol, 0) + amount * multiplier
|
|
953
|
+
continue
|
|
954
|
+
if not char.isupper():
|
|
955
|
+
raise ValueError(f"Invalid chemical formula `{formula}`")
|
|
956
|
+
end = index + 1
|
|
957
|
+
if end < len(formula) and formula[end].islower():
|
|
958
|
+
end += 1
|
|
959
|
+
symbol = formula[index:end]
|
|
960
|
+
if symbol not in symbols:
|
|
961
|
+
raise ValueError(f"Unknown element `{symbol}` in formula `{formula}`")
|
|
962
|
+
index = end
|
|
963
|
+
match = re.match(r"\d+", formula[index:])
|
|
964
|
+
amount = int(match.group()) if match else 1
|
|
965
|
+
if amount <= 0:
|
|
966
|
+
raise ValueError(f"Invalid atom count in formula `{formula}`")
|
|
967
|
+
if match:
|
|
968
|
+
index += len(match.group())
|
|
969
|
+
counts[symbol] = counts.get(symbol, 0) + amount
|
|
970
|
+
if nested:
|
|
971
|
+
raise ValueError(f"Unclosed parenthesis in formula `{formula}`")
|
|
972
|
+
return counts, index
|
|
973
|
+
|
|
974
|
+
counts, end = parse_group(0)
|
|
975
|
+
if end != len(formula) or not counts:
|
|
976
|
+
raise ValueError(f"Invalid chemical formula `{formula}`")
|
|
977
|
+
return counts
|
|
978
|
+
|
|
979
|
+
|
|
980
|
+
def _stoichiometric_species(side):
|
|
981
|
+
if isinstance(side, str):
|
|
982
|
+
species = [item.strip() for item in side.split("+")]
|
|
983
|
+
else:
|
|
984
|
+
species = [str(item).strip() for item in side]
|
|
985
|
+
if not species or any(not item for item in species):
|
|
986
|
+
raise ValueError("Both sides of a reaction must contain molecules")
|
|
987
|
+
return species
|
|
988
|
+
|
|
989
|
+
|
|
990
|
+
def balance_stoichiometry(reactants, products, reversible=None):
|
|
991
|
+
"""Balance a reaction and return it as a :class:`Reaction`."""
|
|
992
|
+
reactant_formulas = _stoichiometric_species(reactants)
|
|
993
|
+
product_formulas = _stoichiometric_species(products)
|
|
994
|
+
formulas = reactant_formulas + product_formulas
|
|
995
|
+
formulas_by_element = [_formula_counts(formula) for formula in formulas]
|
|
996
|
+
elements = list(dict.fromkeys(
|
|
997
|
+
element for counts in formulas_by_element for element in counts
|
|
998
|
+
))
|
|
999
|
+
matrix = [
|
|
1000
|
+
[
|
|
1001
|
+
Fraction(counts.get(element, 0) * (1 if index < len(reactant_formulas) else -1))
|
|
1002
|
+
for index, counts in enumerate(formulas_by_element)
|
|
1003
|
+
]
|
|
1004
|
+
for element in elements
|
|
1005
|
+
]
|
|
1006
|
+
|
|
1007
|
+
pivot_columns = []
|
|
1008
|
+
pivot_row = 0
|
|
1009
|
+
for column in range(len(formulas)):
|
|
1010
|
+
selected = next(
|
|
1011
|
+
(row for row in range(pivot_row, len(matrix)) if matrix[row][column]),
|
|
1012
|
+
None,
|
|
1013
|
+
)
|
|
1014
|
+
if selected is None:
|
|
1015
|
+
continue
|
|
1016
|
+
matrix[pivot_row], matrix[selected] = matrix[selected], matrix[pivot_row]
|
|
1017
|
+
divisor = matrix[pivot_row][column]
|
|
1018
|
+
matrix[pivot_row] = [value / divisor for value in matrix[pivot_row]]
|
|
1019
|
+
for row in range(len(matrix)):
|
|
1020
|
+
if row == pivot_row or not matrix[row][column]:
|
|
1021
|
+
continue
|
|
1022
|
+
factor = matrix[row][column]
|
|
1023
|
+
matrix[row] = [
|
|
1024
|
+
value - factor * pivot
|
|
1025
|
+
for value, pivot in zip(matrix[row], matrix[pivot_row])
|
|
1026
|
+
]
|
|
1027
|
+
pivot_columns.append(column)
|
|
1028
|
+
pivot_row += 1
|
|
1029
|
+
if pivot_row == len(matrix):
|
|
1030
|
+
break
|
|
1031
|
+
|
|
1032
|
+
free_columns = [
|
|
1033
|
+
column for column in range(len(formulas)) if column not in pivot_columns
|
|
1034
|
+
]
|
|
1035
|
+
if len(free_columns) != 1:
|
|
1036
|
+
raise ValueError("Reaction does not have a unique stoichiometric balance")
|
|
1037
|
+
coefficients = [Fraction(0) for _ in formulas]
|
|
1038
|
+
free_column = free_columns[0]
|
|
1039
|
+
coefficients[free_column] = Fraction(1)
|
|
1040
|
+
for row, column in enumerate(pivot_columns):
|
|
1041
|
+
coefficients[column] = -matrix[row][free_column]
|
|
1042
|
+
if all(value < 0 for value in coefficients):
|
|
1043
|
+
coefficients = [-value for value in coefficients]
|
|
1044
|
+
if any(value <= 0 for value in coefficients):
|
|
1045
|
+
raise ValueError("Reaction cannot be balanced with positive coefficients")
|
|
1046
|
+
|
|
1047
|
+
denominator = 1
|
|
1048
|
+
for coefficient in coefficients:
|
|
1049
|
+
denominator = denominator * coefficient.denominator // gcd(
|
|
1050
|
+
denominator, coefficient.denominator
|
|
1051
|
+
)
|
|
1052
|
+
integer_coefficients = [
|
|
1053
|
+
int(coefficient * denominator) for coefficient in coefficients
|
|
1054
|
+
]
|
|
1055
|
+
common_divisor = reduce(gcd, integer_coefficients)
|
|
1056
|
+
integer_coefficients = [value // common_divisor for value in integer_coefficients]
|
|
1057
|
+
participants = [
|
|
1058
|
+
{
|
|
1059
|
+
"molecule": formula,
|
|
1060
|
+
"stoichiometric_coefficient": coefficient,
|
|
1061
|
+
}
|
|
1062
|
+
for formula, coefficient in zip(formulas, integer_coefficients)
|
|
1063
|
+
]
|
|
1064
|
+
split_at = len(reactant_formulas)
|
|
1065
|
+
return Reaction(
|
|
1066
|
+
"BALANCED",
|
|
1067
|
+
"Balanced reaction",
|
|
1068
|
+
"balanced",
|
|
1069
|
+
reversible,
|
|
1070
|
+
participants[:split_at],
|
|
1071
|
+
participants[split_at:],
|
|
1072
|
+
{},
|
|
1073
|
+
"",
|
|
1074
|
+
)
|
|
1075
|
+
|
|
1076
|
+
|
|
1077
|
+
def _execute_balance_command(text):
|
|
1078
|
+
text = text.split("//", 1)[0].rstrip()
|
|
1079
|
+
match = re.fullmatch(
|
|
1080
|
+
r"\s*balance\s+(.+?)\s*(<->|->)\s*(.+?)\s*",
|
|
1081
|
+
text,
|
|
1082
|
+
re.IGNORECASE,
|
|
1083
|
+
)
|
|
1084
|
+
if not match:
|
|
1085
|
+
raise ValueError("Usage: balance <reactants> ->|<-> <products>")
|
|
1086
|
+
reactants, arrow, products = match.groups()
|
|
1087
|
+
reversible = arrow == "<->"
|
|
1088
|
+
return balance_stoichiometry(reactants, products, reversible)
|
|
922
1089
|
|
|
923
1090
|
|
|
924
1091
|
|
|
@@ -2134,6 +2301,14 @@ def execute(arguments, query_text=None, positions=None):
|
|
|
2134
2301
|
if not arguments:
|
|
2135
2302
|
return
|
|
2136
2303
|
|
|
2304
|
+
if arguments[0].lower() == "balance":
|
|
2305
|
+
try:
|
|
2306
|
+
return _execute_balance_command(
|
|
2307
|
+
query_text if query_text is not None else shlex.join(arguments)
|
|
2308
|
+
)
|
|
2309
|
+
except ValueError as exc:
|
|
2310
|
+
return format_error(str(exc), query_text)
|
|
2311
|
+
|
|
2137
2312
|
if arguments[0].lower() in {"help", "?"}:
|
|
2138
2313
|
return HELP
|
|
2139
2314
|
|
|
@@ -2562,11 +2737,15 @@ HELP = """\033[1;36mChemql - Python Chemistry Query Language\033[0m
|
|
|
2562
2737
|
set catalysts
|
|
2563
2738
|
conditions
|
|
2564
2739
|
react N2 3H2
|
|
2740
|
+
balance O2 + H2 -> H2O
|
|
2741
|
+
balance O2 + H2 <-> H2O
|
|
2565
2742
|
|
|
2566
2743
|
Temperature accepts C, F, or K. Pressure accepts Pa, N/m^2, Nm^-2, or bar.
|
|
2567
2744
|
`standard` means 0 C and 100000 Pa; `room` means 25 C and 101325 Pa.
|
|
2568
2745
|
Unset values appear as `Not Set` in `conditions`.
|
|
2569
2746
|
Reaction matching allows 5 K temperature and 5% pressure tolerance.
|
|
2747
|
+
`balance` returns a Reaction with the smallest positive integer coefficients.
|
|
2748
|
+
The `->` arrow is irreversible; `<->` is reversible. `//` starts a comment.
|
|
2570
2749
|
|
|
2571
2750
|
\033[1;33mRESULTS\033[0m
|
|
2572
2751
|
\033[1mfirst\033[0m
|
|
@@ -2718,7 +2897,7 @@ class ChemqlLexer(Lexer):
|
|
|
2718
2897
|
first_word = words[0] if words else ""
|
|
2719
2898
|
|
|
2720
2899
|
# search / source / list / return / clear / exit
|
|
2721
|
-
if first_word in {"search", "source", "list", "clear", "exit", "findel", "findmol", "findre", "set", "add", "remove", "react", "conditions", "view", "help", "dump"}:
|
|
2900
|
+
if first_word in {"search", "source", "list", "clear", "exit", "findel", "findmol", "findre", "set", "add", "remove", "react", "balance", "conditions", "view", "help", "dump"}:
|
|
2722
2901
|
command_start = line.find(first_word)
|
|
2723
2902
|
command_end = command_start + len(first_word)
|
|
2724
2903
|
|
|
@@ -2857,6 +3036,7 @@ class ChemqlLexer(Lexer):
|
|
|
2857
3036
|
"add",
|
|
2858
3037
|
"remove",
|
|
2859
3038
|
"react",
|
|
3039
|
+
"balance",
|
|
2860
3040
|
"conditions",
|
|
2861
3041
|
"view",
|
|
2862
3042
|
"help",
|
|
@@ -2936,7 +3116,7 @@ class ChemqlCompleter(Completer):
|
|
|
2936
3116
|
commands = (
|
|
2937
3117
|
"search", "source", "list", "clear", "return", "exit",
|
|
2938
3118
|
"findel", "findmol", "findre", "set", "add", "remove",
|
|
2939
|
-
"react", "conditions",
|
|
3119
|
+
"react", "balance", "conditions",
|
|
2940
3120
|
)
|
|
2941
3121
|
|
|
2942
3122
|
if not command_words:
|
|
@@ -3019,7 +3199,7 @@ def main():
|
|
|
3019
3199
|
"-v",
|
|
3020
3200
|
"--version",
|
|
3021
3201
|
action="version",
|
|
3022
|
-
version="0.1
|
|
3202
|
+
version="0.2.1",
|
|
3023
3203
|
)
|
|
3024
3204
|
|
|
3025
3205
|
args = parser.parse_args()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: chemql-python
|
|
3
|
-
Version: 0.1
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: A chemistry-focused query language and REPL
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -12,6 +12,8 @@ Dynamic: license-file
|
|
|
12
12
|
|
|
13
13
|
# ChemQL
|
|
14
14
|
|
|
15
|
+

|
|
16
|
+
|
|
15
17
|
ChemQL is a chemistry-focused query language and REPL for exploring element, molecule, and reaction datasets. It lets you search the built-in chemistry data, filter results with expressions, set reaction conditions, and combine queries with small Python snippets when needed.
|
|
16
18
|
|
|
17
19
|
## What ChemQL does
|
|
@@ -80,9 +82,11 @@ The Python package is `chemql`. Its query engine, chemistry types, and bundled
|
|
|
80
82
|
datasets can be imported directly:
|
|
81
83
|
|
|
82
84
|
```python
|
|
83
|
-
from chemql import execute_query_text
|
|
85
|
+
from chemql import balance_stoichiometry, execute_query_text
|
|
84
86
|
|
|
85
87
|
result = execute_query_text('search elements name = "Hydrogen"')
|
|
88
|
+
reaction = balance_stoichiometry(["O2", "H2"], ["H2O"], reversible=True)
|
|
89
|
+
print(reaction)
|
|
86
90
|
```
|
|
87
91
|
|
|
88
92
|
## Core query syntax
|
|
@@ -159,10 +163,18 @@ add catalyst "Vanadium oxide"
|
|
|
159
163
|
remove catalyst "Vanadium oxide"
|
|
160
164
|
conditions
|
|
161
165
|
react N2 3H2
|
|
166
|
+
balance O2 + H2 -> H2O
|
|
167
|
+
balance O2 + H2 <-> H2O
|
|
162
168
|
```
|
|
163
169
|
|
|
164
170
|
Temperature accepts `C`, `F`, or `K`; pressure accepts `Pa`, `N/m^2`, `Nm^-2`, `bar`, or `atm`. The keywords `standard` and `room` are also accepted.
|
|
165
171
|
|
|
172
|
+
`balance` accepts chemical formulas on each side, separated by `+`. It returns
|
|
173
|
+
a `Reaction` with the smallest positive integer stoichiometric coefficients.
|
|
174
|
+
The arrow determines reaction direction: `->` is irreversible and `<->` is
|
|
175
|
+
reversible. `//` begins a comment, as it does elsewhere in ChemQL. The function
|
|
176
|
+
API is `balance_stoichiometry(reactants, products, reversible=None)`.
|
|
177
|
+
|
|
166
178
|
## Operators
|
|
167
179
|
|
|
168
180
|
ChemQL supports these comparison operators:
|
|
@@ -10,6 +10,7 @@ def test_all_exports_only_public_query_api_and_result_types():
|
|
|
10
10
|
"Reaction",
|
|
11
11
|
"ReturnTable",
|
|
12
12
|
"Unknown",
|
|
13
|
+
"balance_stoichiometry",
|
|
13
14
|
"execute_query",
|
|
14
15
|
"execute_query_text",
|
|
15
16
|
}
|
|
@@ -24,4 +25,4 @@ def test_cli_version_option_prints_version(monkeypatch, capsys, option):
|
|
|
24
25
|
chemql.main()
|
|
25
26
|
|
|
26
27
|
assert exception.value.code == 0
|
|
27
|
-
assert capsys.readouterr().out == "0.1
|
|
28
|
+
assert capsys.readouterr().out == "0.2.1\n"
|
|
@@ -2,6 +2,45 @@ import chemql
|
|
|
2
2
|
from prompt_toolkit.document import Document
|
|
3
3
|
|
|
4
4
|
|
|
5
|
+
def test_balance_stoichiometry_returns_minimal_reaction_coefficients():
|
|
6
|
+
reaction = chemql.balance_stoichiometry(["O2", "H2"], ["H2O"], True)
|
|
7
|
+
|
|
8
|
+
assert isinstance(reaction, chemql.Reaction)
|
|
9
|
+
assert [part["stoichiometric_coefficient"] for part in reaction.reactants] == [1, 2]
|
|
10
|
+
assert [part["stoichiometric_coefficient"] for part in reaction.products] == [2]
|
|
11
|
+
assert reaction.reversible is True
|
|
12
|
+
|
|
13
|
+
grouped = chemql.balance_stoichiometry(
|
|
14
|
+
["Ca(OH)2", "H3PO4"],
|
|
15
|
+
["Ca3(PO4)2", "H2O"],
|
|
16
|
+
)
|
|
17
|
+
assert [
|
|
18
|
+
part["stoichiometric_coefficient"]
|
|
19
|
+
for part in grouped.reactants + grouped.products
|
|
20
|
+
] == [3, 2, 1, 6]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_balance_syntax_infers_direction_from_arrow_and_honors_comments(capsys):
|
|
24
|
+
chemql.process_lines(["balance O2 + H2 -> H2O //reversible"])
|
|
25
|
+
assert "1 O₂ + 2 H₂ → 2 H₂O" in capsys.readouterr().out
|
|
26
|
+
|
|
27
|
+
reaction = chemql.execute_query_text(
|
|
28
|
+
"balance O2 + H2 <-> H2O //irreversible"
|
|
29
|
+
)
|
|
30
|
+
assert isinstance(reaction, chemql.Reaction)
|
|
31
|
+
assert reaction.reversible is True
|
|
32
|
+
assert "1 O₂ + 2 H₂ ⇌ 2 H₂O" in str(reaction)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_balance_stoichiometry_rejects_invalid_formula():
|
|
36
|
+
try:
|
|
37
|
+
chemql.balance_stoichiometry(["Xx2"], ["H2"])
|
|
38
|
+
except ValueError as exc:
|
|
39
|
+
assert "Unknown element" in str(exc)
|
|
40
|
+
else:
|
|
41
|
+
raise AssertionError("Unknown elements must be rejected")
|
|
42
|
+
|
|
43
|
+
|
|
5
44
|
def test_findre_searches_reactions_by_id_and_projects_attributes():
|
|
6
45
|
result = chemql.execute([
|
|
7
46
|
"findre", "RHEA:10000", "return", "id", "name", "source"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|