chemql-python 0.1.0__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {chemql_python-0.1.0/src/chemql_python.egg-info → chemql_python-0.2.0}/PKG-INFO +15 -2
- {chemql_python-0.1.0 → chemql_python-0.2.0}/README.md +14 -1
- {chemql_python-0.1.0 → chemql_python-0.2.0}/pyproject.toml +1 -1
- {chemql_python-0.1.0 → chemql_python-0.2.0}/src/chemql/__init__.py +198 -4
- {chemql_python-0.1.0 → chemql_python-0.2.0/src/chemql_python.egg-info}/PKG-INFO +15 -2
- {chemql_python-0.1.0 → chemql_python-0.2.0}/tests/test_public_api.py +2 -1
- {chemql_python-0.1.0 → chemql_python-0.2.0}/tests/test_reactions.py +39 -0
- {chemql_python-0.1.0 → chemql_python-0.2.0}/LICENSE +0 -0
- {chemql_python-0.1.0 → chemql_python-0.2.0}/setup.cfg +0 -0
- {chemql_python-0.1.0 → chemql_python-0.2.0}/src/chemql/__main__.py +0 -0
- {chemql_python-0.1.0 → chemql_python-0.2.0}/src/chemql/data/BondEnergies.json +0 -0
- {chemql_python-0.1.0 → chemql_python-0.2.0}/src/chemql/data/MoleculeBonds.json +0 -0
- {chemql_python-0.1.0 → chemql_python-0.2.0}/src/chemql/data/Molecules.json +0 -0
- {chemql_python-0.1.0 → chemql_python-0.2.0}/src/chemql/data/PeriodicTableJSON.json +0 -0
- {chemql_python-0.1.0 → chemql_python-0.2.0}/src/chemql/data/Reactions.json +0 -0
- {chemql_python-0.1.0 → chemql_python-0.2.0}/src/chemql/data/THIRD_PARTY_LICENSES/PeriodicTableJSON.txt +0 -0
- {chemql_python-0.1.0 → chemql_python-0.2.0}/src/chemql/data/THIRD_PARTY_LICENSES/RHEA.txt +0 -0
- {chemql_python-0.1.0 → chemql_python-0.2.0}/src/chemql/html/periodic_table.html +0 -0
- {chemql_python-0.1.0 → chemql_python-0.2.0}/src/chemql_python.egg-info/SOURCES.txt +0 -0
- {chemql_python-0.1.0 → chemql_python-0.2.0}/src/chemql_python.egg-info/dependency_links.txt +0 -0
- {chemql_python-0.1.0 → chemql_python-0.2.0}/src/chemql_python.egg-info/entry_points.txt +0 -0
- {chemql_python-0.1.0 → chemql_python-0.2.0}/src/chemql_python.egg-info/requires.txt +0 -0
- {chemql_python-0.1.0 → chemql_python-0.2.0}/src/chemql_python.egg-info/top_level.txt +0 -0
- {chemql_python-0.1.0 → chemql_python-0.2.0}/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.
|
|
3
|
+
Version: 0.2.0
|
|
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,19 @@ add catalyst "Vanadium oxide"
|
|
|
159
163
|
remove catalyst "Vanadium oxide"
|
|
160
164
|
conditions
|
|
161
165
|
react N2 3H2
|
|
166
|
+
balance O2 + H2 -> H2O //reversible
|
|
167
|
+
balance O2 + H2 <-> H2O //irreversible
|
|
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 optional `//reversible` and `//irreversible` annotations set the returned
|
|
175
|
+
reaction direction; without an annotation, `->` is irreversible and `<->` is
|
|
176
|
+
reversible. The function API is
|
|
177
|
+
`balance_stoichiometry(reactants, products, reversible=None)`.
|
|
178
|
+
|
|
166
179
|
## Operators
|
|
167
180
|
|
|
168
181
|
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,19 @@ add catalyst "Vanadium oxide"
|
|
|
147
151
|
remove catalyst "Vanadium oxide"
|
|
148
152
|
conditions
|
|
149
153
|
react N2 3H2
|
|
154
|
+
balance O2 + H2 -> H2O //reversible
|
|
155
|
+
balance O2 + H2 <-> H2O //irreversible
|
|
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 optional `//reversible` and `//irreversible` annotations set the returned
|
|
163
|
+
reaction direction; without an annotation, `->` is irreversible and `<->` is
|
|
164
|
+
reversible. The function API is
|
|
165
|
+
`balance_stoichiometry(reactants, products, reversible=None)`.
|
|
166
|
+
|
|
154
167
|
## Operators
|
|
155
168
|
|
|
156
169
|
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,177 @@ 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
|
+
match = re.fullmatch(
|
|
1079
|
+
r"\s*balance\s+(.+?)\s*(<->|->)\s*(.+?)"
|
|
1080
|
+
r"(?:\s*//\s*(reversible|irreversible)\s*)?",
|
|
1081
|
+
text,
|
|
1082
|
+
re.IGNORECASE,
|
|
1083
|
+
)
|
|
1084
|
+
if not match:
|
|
1085
|
+
raise ValueError(
|
|
1086
|
+
"Usage: balance <reactants> ->|<-> <products> "
|
|
1087
|
+
"[//reversible|//irreversible]"
|
|
1088
|
+
)
|
|
1089
|
+
reactants, arrow, products, direction = match.groups()
|
|
1090
|
+
reversible = (
|
|
1091
|
+
direction.lower() == "reversible"
|
|
1092
|
+
if direction
|
|
1093
|
+
else arrow == "<->"
|
|
1094
|
+
)
|
|
1095
|
+
return balance_stoichiometry(reactants, products, reversible)
|
|
922
1096
|
|
|
923
1097
|
|
|
924
1098
|
|
|
@@ -1549,6 +1723,13 @@ def find_endblock(lines, start):
|
|
|
1549
1723
|
raise SyntaxError("Missing `[endblock]`")
|
|
1550
1724
|
|
|
1551
1725
|
def remove_comments(line):
|
|
1726
|
+
if re.fullmatch(
|
|
1727
|
+
r"\s*balance\b.*//\s*(?:reversible|irreversible)\s*",
|
|
1728
|
+
line,
|
|
1729
|
+
re.IGNORECASE,
|
|
1730
|
+
):
|
|
1731
|
+
return line
|
|
1732
|
+
|
|
1552
1733
|
in_quote = None
|
|
1553
1734
|
escaped = False
|
|
1554
1735
|
|
|
@@ -2134,6 +2315,14 @@ def execute(arguments, query_text=None, positions=None):
|
|
|
2134
2315
|
if not arguments:
|
|
2135
2316
|
return
|
|
2136
2317
|
|
|
2318
|
+
if arguments[0].lower() == "balance":
|
|
2319
|
+
try:
|
|
2320
|
+
return _execute_balance_command(
|
|
2321
|
+
query_text if query_text is not None else shlex.join(arguments)
|
|
2322
|
+
)
|
|
2323
|
+
except ValueError as exc:
|
|
2324
|
+
return format_error(str(exc), query_text)
|
|
2325
|
+
|
|
2137
2326
|
if arguments[0].lower() in {"help", "?"}:
|
|
2138
2327
|
return HELP
|
|
2139
2328
|
|
|
@@ -2562,11 +2751,15 @@ HELP = """\033[1;36mChemql - Python Chemistry Query Language\033[0m
|
|
|
2562
2751
|
set catalysts
|
|
2563
2752
|
conditions
|
|
2564
2753
|
react N2 3H2
|
|
2754
|
+
balance O2 + H2 -> H2O //reversible
|
|
2755
|
+
balance O2 + H2 <-> H2O //irreversible
|
|
2565
2756
|
|
|
2566
2757
|
Temperature accepts C, F, or K. Pressure accepts Pa, N/m^2, Nm^-2, or bar.
|
|
2567
2758
|
`standard` means 0 C and 100000 Pa; `room` means 25 C and 101325 Pa.
|
|
2568
2759
|
Unset values appear as `Not Set` in `conditions`.
|
|
2569
2760
|
Reaction matching allows 5 K temperature and 5% pressure tolerance.
|
|
2761
|
+
`balance` returns a Reaction with the smallest positive integer coefficients.
|
|
2762
|
+
Direction annotations override the arrow: `//reversible` or `//irreversible`.
|
|
2570
2763
|
|
|
2571
2764
|
\033[1;33mRESULTS\033[0m
|
|
2572
2765
|
\033[1mfirst\033[0m
|
|
@@ -2718,7 +2911,7 @@ class ChemqlLexer(Lexer):
|
|
|
2718
2911
|
first_word = words[0] if words else ""
|
|
2719
2912
|
|
|
2720
2913
|
# 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"}:
|
|
2914
|
+
if first_word in {"search", "source", "list", "clear", "exit", "findel", "findmol", "findre", "set", "add", "remove", "react", "balance", "conditions", "view", "help", "dump"}:
|
|
2722
2915
|
command_start = line.find(first_word)
|
|
2723
2916
|
command_end = command_start + len(first_word)
|
|
2724
2917
|
|
|
@@ -2857,6 +3050,7 @@ class ChemqlLexer(Lexer):
|
|
|
2857
3050
|
"add",
|
|
2858
3051
|
"remove",
|
|
2859
3052
|
"react",
|
|
3053
|
+
"balance",
|
|
2860
3054
|
"conditions",
|
|
2861
3055
|
"view",
|
|
2862
3056
|
"help",
|
|
@@ -2936,7 +3130,7 @@ class ChemqlCompleter(Completer):
|
|
|
2936
3130
|
commands = (
|
|
2937
3131
|
"search", "source", "list", "clear", "return", "exit",
|
|
2938
3132
|
"findel", "findmol", "findre", "set", "add", "remove",
|
|
2939
|
-
"react", "conditions",
|
|
3133
|
+
"react", "balance", "conditions",
|
|
2940
3134
|
)
|
|
2941
3135
|
|
|
2942
3136
|
if not command_words:
|
|
@@ -3019,7 +3213,7 @@ def main():
|
|
|
3019
3213
|
"-v",
|
|
3020
3214
|
"--version",
|
|
3021
3215
|
action="version",
|
|
3022
|
-
version="0.
|
|
3216
|
+
version="0.2.0",
|
|
3023
3217
|
)
|
|
3024
3218
|
|
|
3025
3219
|
args = parser.parse_args()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: chemql-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
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,19 @@ add catalyst "Vanadium oxide"
|
|
|
159
163
|
remove catalyst "Vanadium oxide"
|
|
160
164
|
conditions
|
|
161
165
|
react N2 3H2
|
|
166
|
+
balance O2 + H2 -> H2O //reversible
|
|
167
|
+
balance O2 + H2 <-> H2O //irreversible
|
|
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 optional `//reversible` and `//irreversible` annotations set the returned
|
|
175
|
+
reaction direction; without an annotation, `->` is irreversible and `<->` is
|
|
176
|
+
reversible. The function API is
|
|
177
|
+
`balance_stoichiometry(reactants, products, reversible=None)`.
|
|
178
|
+
|
|
166
179
|
## Operators
|
|
167
180
|
|
|
168
181
|
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.
|
|
28
|
+
assert capsys.readouterr().out == "0.2.0\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_preserves_direction_annotation(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 False
|
|
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
|