Balance Reaction Equations¶
To follow along, either
Download
BalanceReactionEquations.py(run as$AMSBIN/amspython BalanceReactionEquations.py).Download
BalanceReactionEquations.ipynb(see also: how to install Jupyterlab in AMS)
Worked Example¶
Balance simple reactions¶
Here we present eleven different sets of reactants and products, not necessarily based on real reactions. The molecules can be passed as formulas, or as PLAMS Molecule objects.
from scm.plams.tools.reaction import ReactionEquation, balance
Example: Aspirin in water¶
reactants = ["C9H8O4", "H2O"]
products = ["C2H4O2", "C7H6O3"]
reaction = balance(reactants, products)
print(reaction)
1 C9H8O4 + 1 H2O => 1 C2H4O2 + 1 C7H6O3
reaction is of type ReactionEquation and has the following attributes:
print(f"{reaction.coeffs=}")
print(f"{reaction.message=}")
print(f"{reaction.reactants=}")
print(f"{reaction.products=}")
reaction.coeffs=array([1, 1, 1, 1])
reaction.message='Success'
reaction.reactants=['C9H8O4', 'H2O']
reaction.products=['C2H4O2', 'C7H6O3']
Specify reactants/products as Species¶
An alternative to the string representation of reactants and products is to use Species, which also allows you to set the molecular charges and minimum coefficients (see later examples):
from scm.plams.tools.reaction import Species as S
reactants = [S("C9H8O4", charge=0), S("H2O", charge=0)]
products = [S("C2H4O2", charge=0), S("C7H6O3", charge=0)]
reaction = balance(reactants, products)
print(reaction)
1 C9H8O4 + 1 H2O => 1 C2H4O2 + 1 C7H6O3
Initialize species from SMILES¶
from scm.plams.tools.reaction import Species as S
reactants = [S.from_smiles("C"), S.from_smiles("O=O")]
products = [S.from_smiles("O=C=O"), S.from_smiles("O")]
reaction = balance(reactants, products)
print(reaction)
1 CH4 + 2 O2 => 1 CO2 + 2 H2O
You can also format the reaction with the SMILES strings (requires that all reactants and products have the smiles attribute set):
print(f"{reaction:smiles}")
1 C + 2 O=O => 1 O=C=O + 2 O
Superfluous reactants and products (multiple possible solutions)¶
When there are multiple possible solutions, a single one is chosen.
reactants = ["C9H8O4", "H2O", "HO", "HO2"]
products = ["C2H4O2", "C7H6O3", "H2O2"]
reaction = balance(reactants, products)
print(reaction)
print("Some coefficients are zero:")
print(f"{reaction.coeffs=}")
1 C9H8O4 + 1 H2O => 1 C2H4O2 + 1 C7H6O3
Some coefficients are zero:
reaction.coeffs=array([1, 1, 0, 0, 1, 1, 0])
Exampe that is not easily balanced¶
reactants = ["C9O4H6", "OH"]
products = ["C2O2H3", "C7O3H2"]
reaction = balance(reactants, products)
print(reaction)
9 C9O4H6 + 25 OH => 23 C2O2H3 + 5 C7O3H2
Reaction that cannot be balanced¶
reactants = ["FeS2", "HNO3"]
products = ["Fe2S3O12", "NO", "H2SO4"]
reaction = balance(reactants, products)
print(reaction)
print(f"{reaction.coeffs=}")
print(f"{reaction.message=}")
Inconsistent system: No solution
reaction.coeffs=None
reaction.message='Empty nullspace'
Sympy method¶
By default, a native method is used to compute the nullspace. Optionally, this can be done with sympy. The result should be the same.
reactants = ["C9H8O4", "H2O"]
products = ["C2H4O2", "C7H6O3"]
reaction = balance(reactants, products, method="sympy")
print(reaction)
1 C9H8O4 + 1 H2O => 1 C2H4O2 + 1 C7H6O3
Balance reactions with charged species¶
Charges are printed within square brackets:
from scm.plams.tools.reaction import Species as S
reactants = [S("OH", charge=-1), S("H3O", charge=+1)]
products = [S("H2O", charge=0)]
reaction = balance(reactants, products)
print(reaction)
1 OH[-1] + 1 H3O[+1] => 2 H2O
If the charges cannot be balanced, no solution will be found:
from scm.plams.tools.reaction import Species as S
reactants = [S("OH", charge=-1), S("H3O", charge=0)] # "neutral" H3O!
products = [S("H2O", charge=0)]
reaction = balance(reactants, products)
print(reaction)
Inconsistent system: No solution
The charges are also inferred from the SMILES strings:
from scm.plams.tools.reaction import Species as S
reactants = [S.from_smiles("[H+]"), S.from_smiles("[OH-]")]
products = [S.from_smiles("O")]
reaction = balance(reactants, products)
print(reaction)
1 H[+1] + 1 HO[-1] => 1 H2O
print(f"{reaction:smiles}")
1 [H+] + 1 [OH-] => 1 O
Reaction with many possible products: Set minimum coefficients¶
You can set minimum values for the coefficients with the min_coeff attribute for a Species. This is useful if you have a list of many possible products, and want to find a balanced reaction that includes one or more specific products:
from scm.plams.tools.reaction import Species as S
reactant_smiles = ["O", "CC(=O)Oc1ccccc1C(=O)O"]
product_smiles = [
"CC(=O)O",
"O=C(O)c1ccccc1O",
"OO",
"O=C(O)O",
"O=C=O",
"CO",
"C",
"O=C(O)C1=CC(O)C=CC1=O",
]
reactants = [S.from_smiles(x, min_coeff=1) for x in reactant_smiles] # both reactants must appear
products = [S.from_smiles(x) for x in product_smiles]
products[1].min_coeff = 1 # the second product must appear
products[2].min_coeff = 3 # the third product must have a coefficient of at least 3
reaction = balance(reactants, products)
print(reaction)
6 H2O + 1 C9H8O4 => 1 C7H6O3 + 3 H2O2 + 1 CH4O + 1 CH4
print(f"{reaction:smiles}")
6 O + 1 CC(=O)Oc1ccccc1C(=O)O => 1 O=C(O)c1ccccc1O + 3 OO + 1 CO + 1 C
Direct usage of the ReactionEquation class¶
You can also directly use the ReactionEquation class. This can be useful and save some time if you want to loop over many possible values of min_coeffs, for example.
In this case, do not use Species but provide formulas or PLAMS Molecules directly, use the set_charges method to set charges, and set the min_coeffs array:
import numpy
reactant_smiles = ["O", "CC(=O)Oc1ccccc1C(=O)O"]
product_smiles = [
"CC(=O)O",
"O=C(O)c1ccccc1O",
"OO",
"O=C(O)O",
"O=C=O",
"CO",
"C",
"O=C(O)C1=CC(O)C=CC1=O",
"O=C(O)C1=CC(O)C(O)C=C1O",
"O=C(O)C(C=CO)=C(O)C=CO",
"O=C(CO)Oc1ccccc1",
"Oc1ccccc1",
"O=C1C=CC=CC1",
"OC1=CC(O)C=CC1",
"C=CC=CC=C=O",
"C1=C=CC=CC=1",
"O=C=CO",
"O=CO",
"O=C=C(O)O",
"O=C=C=O",
"O=C1OC1=O",
"O=COc1ccccc1",
"O=C1CC=CC(O)C1",
"C=CC(O)C(O)C=C=O",
"OC1=CC=CC(O)C1=C(O)O",
"O=C(O)C12C(=O)C1C=CC2O",
"O=C(O)CO",
"O=CC(=O)O",
"O=C1C=CC(O)=C(C1)C(=O)O",
"C=CC=CC(=O)O",
]
from scm.plams import from_smiles
reactants = [from_smiles(smiles) for smiles in reactant_smiles] # PLAMS Molecules
products = [from_smiles(smiles) for smiles in product_smiles] # PLAMS Molecules
# Create the Reaction object with all the molecules
reaction = ReactionEquation(reactants, products)
print("Starting loop over products..")
nmols = len(reactants) + len(products)
nreactants = len(reactants)
for iprod, _ in enumerate(products):
print(f"{product_smiles[iprod]:>25s}: ", end="")
min_coeffs = numpy.zeros(nmols)
min_coeffs[nreactants + iprod] = 1
reaction.balance(min_coeffs=min_coeffs)
print(f"{reaction:smiles}")
Starting loop over products..
CC(=O)O: 1 O + 1 CC(=O)OC1=CC=CC=C1C(=O)O => 1 CC(=O)O + 1 O=C(O)C1=CC=CC=C1O
O=C(O)c1ccccc1O: 1 O + 1 CC(=O)OC1=CC=CC=C1C(=O)O => 1 CC(=O)O + 1 O=C(O)C1=CC=CC=C1O
OO: 2 O + 1 CC(=O)OC1=CC=CC=C1C(=O)O => 1 CC(=O)O + 1 OO + 1 O=COC1C=CC=CC=1
O=C(O)O: 1 O + 1 CC(=O)OC1=CC=CC=C1C(=O)O => 1 CC(=O)O + 1 O=C(O)O + 1 C1=C=CC=CC=1
O=C=O: 1 CC(=O)OC1=CC=CC=C1C(=O)O => 1 CC(=O)O + 1 O=C=O + 1 C1=C=CC=CC=1
CO: 1 CC(=O)OC1=CC=CC=C1C(=O)O => 1 CO + 1 C1=C=CC=CC=1 + 1 O=C1OC1=O
C: 1 O + 1 CC(=O)OC1=CC=CC=C1C(=O)O => 1 O=C(O)C1=CC=CC=C1O + 1 O=C=O + 1 C
O=C(O)C1=CC(O)C=CC1=O: 2 O + 1 CC(=O)OC1=CC=CC=C1C(=O)O => 1 C + 1 O=C(O)C1=CC(O)C=CC1=O + 1 O=CO
O=C(O)C1=CC(O)C(O)C=C1O: 3 O + 1 CC(=O)OC1=CC=CC=C1C(=O)O => 1 C + 1 O=C(O)C1=CC(O)C(O)C=C1O + 1 O=CO
O=C(O)C(C=CO)=C(O)C=CO: 3 O + 1 CC(=O)OC1=CC=CC=C1C(=O)O => 1 C + 1 O=C(O)C(C=CO)=C(O)C=CO + 1 O=CO
O=C(CO)Oc1ccccc1: 1 O + 1 CC(=O)OC1=CC=CC=C1C(=O)O => 1 O=C(CO)OC1C=CC=CC=1 + 1 O=CO
Oc1ccccc1: 1 O + 1 CC(=O)OC1=CC=CC=C1C(=O)O => 1 CC(=O)O + 1 O=C=O + 1 OC1C=CC=CC=1
O=C1C=CC=CC1: 1 O + 1 CC(=O)OC1=CC=CC=C1C(=O)O => 1 CC(=O)O + 1 O=C=O + 1 O=C1C=CC=CC1
OC1=CC(O)C=CC1: 2 O + 1 CC(=O)OC1=CC=CC=C1C(=O)O => 1 CC(=O)O + 1 O=C=O + 1 OC1=CC(O)C=CC1
C=CC=CC=C=O: 1 O + 1 CC(=O)OC1=CC=CC=C1C(=O)O => 1 CC(=O)O + 1 O=C=O + 1 C=CC=CC=C=O
C1=C=CC=CC=1: 1 CC(=O)OC1=CC=CC=C1C(=O)O => 1 CC(=O)O + 1 O=C=O + 1 C1=C=CC=CC=1
O=C=CO: 1 CC(=O)OC1=CC=CC=C1C(=O)O => 1 O=C=CO + 1 O=COC1C=CC=CC=1
O=CO: 1 O + 1 CC(=O)OC1=CC=CC=C1C(=O)O => 1 O=C(CO)OC1C=CC=CC=1 + 1 O=CO
O=C=C(O)O: 1 O + 2 CC(=O)OC1=CC=CC=C1C(=O)O => 2 O=C(CO)OC1C=CC=CC=1 + 1 O=C=C(O)O
O=C=C=O: 2 CC(=O)OC1=CC=CC=C1C(=O)O => 2 O=C(CO)OC1C=CC=CC=1 + 1 O=C=C=O
O=C1OC1=O: 1 CC(=O)OC1=CC=CC=C1C(=O)O => 1 CO + 1 C1=C=CC=CC=1 + 1 O=C1OC1=O
O=COc1ccccc1: 1 CC(=O)OC1=CC=CC=C1C(=O)O => 1 O=C=CO + 1 O=COC1C=CC=CC=1
O=C1CC=CC(O)C1: 2 O + 1 CC(=O)OC1=CC=CC=C1C(=O)O => 1 CC(=O)O + 1 O=C=O + 1 O=C1CC=CC(O)C1
C=CC(O)C(O)C=C=O: 2 O + 1 CC(=O)OC1=CC=CC=C1C(=O)O => 1 CO + 1 O=C=C=O + 1 C=CC(O)C(O)C=C=O
OC1=CC=CC(O)C1=C(O)O: 2 O + 1 CC(=O)OC1=CC=CC=C1C(=O)O => 1 CC(=O)O + 1 OC1=CC=CC(O)C1=C(O)O
O=C(O)C12C(=O)C1C=CC2O: 2 O + 1 CC(=O)OC1=CC=CC=C1C(=O)O => 1 C + 1 O=CO + 1 O=C(O)C12C(=O)C1C=CC2O
O=C(O)CO: 1 O + 1 CC(=O)OC1=CC=CC=C1C(=O)O => 1 O=COC1C=CC=CC=1 + 1 O=C(O)CO
O=CC(=O)O: 1 O + 2 CC(=O)OC1=CC=CC=C1C(=O)O => 2 O=C(CO)OC1C=CC=CC=1 + 1 O=CC(=O)O
O=C1C=CC(O)=C(C1)C(=O)O: 2 O + 1 CC(=O)OC1=CC=CC=C1C(=O)O => 1 C + 1 O=CO + 1 O=C1C=CC(O)=C(C1)C(=O)O
C=CC=CC(=O)O: 2 O + 1 CC(=O)OC1=CC=CC=C1C(=O)O => 1 CC(=O)O + 1 O=C=CO + 1 C=CC=CC(=O)O
Complete Python code¶
#!/usr/bin/env amspython
# coding: utf-8
# ## Balance simple reactions
# Here we present eleven different sets of reactants and products, not necessarily based on real reactions.
# The molecules can be passed as formulas, or as PLAMS Molecule objects.
from scm.plams.tools.reaction import ReactionEquation, balance
# ### Example: Aspirin in water
reactants = ["C9H8O4", "H2O"]
products = ["C2H4O2", "C7H6O3"]
reaction = balance(reactants, products)
print(reaction)
# ``reaction`` is of type ``ReactionEquation`` and has the following attributes:
print(f"{reaction.coeffs=}")
print(f"{reaction.message=}")
print(f"{reaction.reactants=}")
print(f"{reaction.products=}")
# ### Specify reactants/products as Species
#
# An alternative to the string representation of reactants and products is to use Species, which also allows you to set the molecular charges and minimum coefficients (see later examples):
from scm.plams.tools.reaction import Species as S
reactants = [S("C9H8O4", charge=0), S("H2O", charge=0)]
products = [S("C2H4O2", charge=0), S("C7H6O3", charge=0)]
reaction = balance(reactants, products)
print(reaction)
# ### Initialize species from SMILES
from scm.plams.tools.reaction import Species as S
reactants = [S.from_smiles("C"), S.from_smiles("O=O")]
products = [S.from_smiles("O=C=O"), S.from_smiles("O")]
reaction = balance(reactants, products)
print(reaction)
# You can also format the reaction with the SMILES strings (requires that all reactants and products have the ``smiles`` attribute set):
print(f"{reaction:smiles}")
# ### Superfluous reactants and products (multiple possible solutions)
#
# When there are multiple possible solutions, a single one is chosen.
reactants = ["C9H8O4", "H2O", "HO", "HO2"]
products = ["C2H4O2", "C7H6O3", "H2O2"]
reaction = balance(reactants, products)
print(reaction)
print("Some coefficients are zero:")
print(f"{reaction.coeffs=}")
# ### Exampe that is not easily balanced
reactants = ["C9O4H6", "OH"]
products = ["C2O2H3", "C7O3H2"]
reaction = balance(reactants, products)
print(reaction)
# ### Reaction that cannot be balanced
reactants = ["FeS2", "HNO3"]
products = ["Fe2S3O12", "NO", "H2SO4"]
reaction = balance(reactants, products)
print(reaction)
print(f"{reaction.coeffs=}")
print(f"{reaction.message=}")
# ### Sympy method
#
# By default, a native method is used to compute the nullspace. Optionally, this can be done with sympy. The result should be the same.
reactants = ["C9H8O4", "H2O"]
products = ["C2H4O2", "C7H6O3"]
reaction = balance(reactants, products, method="sympy")
print(reaction)
# ## Balance reactions with charged species
#
# Charges are printed within square brackets:
from scm.plams.tools.reaction import Species as S
reactants = [S("OH", charge=-1), S("H3O", charge=+1)]
products = [S("H2O", charge=0)]
reaction = balance(reactants, products)
print(reaction)
# If the charges cannot be balanced, no solution will be found:
from scm.plams.tools.reaction import Species as S
reactants = [S("OH", charge=-1), S("H3O", charge=0)] # "neutral" H3O!
products = [S("H2O", charge=0)]
reaction = balance(reactants, products)
print(reaction)
# The charges are also inferred from the SMILES strings:
from scm.plams.tools.reaction import Species as S
reactants = [S.from_smiles("[H+]"), S.from_smiles("[OH-]")]
products = [S.from_smiles("O")]
reaction = balance(reactants, products)
print(reaction)
print(f"{reaction:smiles}")
# ## Reaction with many possible products: Set minimum coefficients
#
# You can set minimum values for the coefficients with the ``min_coeff`` attribute for a ``Species``. This is useful if you have a list of many possible products, and want to find a balanced reaction that includes one or more specific products:
from scm.plams.tools.reaction import Species as S
reactant_smiles = ["O", "CC(=O)Oc1ccccc1C(=O)O"]
product_smiles = [
"CC(=O)O",
"O=C(O)c1ccccc1O",
"OO",
"O=C(O)O",
"O=C=O",
"CO",
"C",
"O=C(O)C1=CC(O)C=CC1=O",
]
reactants = [S.from_smiles(x, min_coeff=1) for x in reactant_smiles] # both reactants must appear
products = [S.from_smiles(x) for x in product_smiles]
products[1].min_coeff = 1 # the second product must appear
products[2].min_coeff = 3 # the third product must have a coefficient of at least 3
reaction = balance(reactants, products)
print(reaction)
print(f"{reaction:smiles}")
# ## Direct usage of the ReactionEquation class
#
# You can also directly use the ReactionEquation class. This can be useful and save some time if you want to loop over many possible values of ``min_coeffs``, for example.
#
# In this case, do not use ``Species`` but provide formulas or PLAMS Molecules directly, use the ``set_charges`` method to set charges, and set the ``min_coeffs`` array:
import numpy
reactant_smiles = ["O", "CC(=O)Oc1ccccc1C(=O)O"]
product_smiles = [
"CC(=O)O",
"O=C(O)c1ccccc1O",
"OO",
"O=C(O)O",
"O=C=O",
"CO",
"C",
"O=C(O)C1=CC(O)C=CC1=O",
"O=C(O)C1=CC(O)C(O)C=C1O",
"O=C(O)C(C=CO)=C(O)C=CO",
"O=C(CO)Oc1ccccc1",
"Oc1ccccc1",
"O=C1C=CC=CC1",
"OC1=CC(O)C=CC1",
"C=CC=CC=C=O",
"C1=C=CC=CC=1",
"O=C=CO",
"O=CO",
"O=C=C(O)O",
"O=C=C=O",
"O=C1OC1=O",
"O=COc1ccccc1",
"O=C1CC=CC(O)C1",
"C=CC(O)C(O)C=C=O",
"OC1=CC=CC(O)C1=C(O)O",
"O=C(O)C12C(=O)C1C=CC2O",
"O=C(O)CO",
"O=CC(=O)O",
"O=C1C=CC(O)=C(C1)C(=O)O",
"C=CC=CC(=O)O",
]
from scm.plams import from_smiles
reactants = [from_smiles(smiles) for smiles in reactant_smiles] # PLAMS Molecules
products = [from_smiles(smiles) for smiles in product_smiles] # PLAMS Molecules
# Create the Reaction object with all the molecules
reaction = ReactionEquation(reactants, products)
print("Starting loop over products..")
nmols = len(reactants) + len(products)
nreactants = len(reactants)
for iprod, _ in enumerate(products):
print(f"{product_smiles[iprod]:>25s}: ", end="")
min_coeffs = numpy.zeros(nmols)
min_coeffs[nreactants + iprod] = 1
reaction.balance(min_coeffs=min_coeffs)
print(f"{reaction:smiles}")