The Problem with Manual Inspection
Every chemistry student encounters chemical equation balancing in their first semester. While small reactions can be solved by inspection, complex redox transformations—such as the permanganate oxidation of hydrochloric acid:
$\text{KMnO}_4 + \text{HCl} \rightarrow \text{KCl} + \text{MnCl}_2 + \text{H}_2\text{O} + \text{Cl}_2$
—defy quick guessing and often lead to frustrating arithmetic errors during exam or laboratory prep.
When developing Alomole — Chemistry Companion, our objective was clear: create an instantaneous, client-side chemical balancer capable of resolving any valid chemical reaction without requiring a server roundtrip.
1. The Mathematical Formulation
Chemical balance obeys the Law of Conservation of Mass: for every chemical element E_i, the total number of atoms on the reactant side must equal the total on the product side:
$\sum_{\text{reactants}} c_j \cdot n_{i,j} = \sum_{\text{products}} c_k \cdot n_{i,k}$
By adopting the convention of treating product coefficients as negative values, the conservation law simplifies to a homogeneous system of linear equations:
$\mathbf{A} \mathbf{c} = \mathbf{0}$
where:
- \mathbf{A} is an m \times n matrix with m distinct chemical elements and n chemical compounds.
- Entry A_{i,j} represents the count of element i in compound j (positive for reactants, negative for products).
- \mathbf{c} is the vector of stoichiometric coefficients (c_1, c_2, \dots, c_n)^T > 0.
Finding the balanced equation is mathematically equivalent to computing the kernel (null space) of matrix \mathbf{A} restricted to positive integer vectors.
2. Chemical Formula Parsing with Recursive Descent
Before constructing matrix \mathbf{A}, raw chemical strings must be parsed into an abstract syntax tree of elemental counts. We implemented a lightweight recursive-descent tokeniser:
- Tokens: Elements (
[A-Z][a-z]?), Numbers ([0-9]+), Parentheses/Brackets ((,),[,]). - Sub-group Expansion: Parenthesised radicals like
(NH4)2trigger a recursive evaluation, multiplying each child element count by the following integer coefficient. - Validation: Any unclosed bracket or invalid atomic symbol throws a descriptive syntax error before matrix construction.
3. Exact Rational Arithmetic vs. Floating-Point Instability
Standard Gaussian elimination implemented with standard IEEE 754 64-bit floating-point arithmetic fails on large stoichiometric matrices due to rounding truncation. For instance, 0.333333333333 does not cleanly scale to an integer 1/3, producing fractional coefficient artefacts.
To prevent this, we created an exact Rational class in Dart:
class Rational {
final BigInt numerator;
final BigInt denominator;
Rational(this.numerator, this.denominator) {
if (denominator == BigInt.zero) throw ArgumentError('Division by zero');
// Normalize sign and reduce via GCD
}
Rational operator +(Rational other) => ...;
Rational operator *(Rational other) => ...;
Rational operator -(Rational other) => ...;
Rational operator /(Rational other) => ...;
}By maintaining all pivot operations in exact fractions, the resulting null-space basis vector consists entirely of rational values r_1, r_2, \dots, r_n.
4. Scaling to Smallest Positive Integers
Once a basis vector \mathbf{v} = (r_1, r_2, \dots, r_n) is found:
- Let L be the Least Common Multiple (LCM) of all denominators in \mathbf{v}.
- Multiply each element r_j by L to produce integer vector \mathbf{z}.
- Divide each entry in \mathbf{z} by the Greatest Common Divisor (GCD) of all entries.
- Ensure all coefficients are strictly positive. If the null-space dimension is >1, the reaction represents multiple independent parallel pathways (e.g. incomplete combustion), which Alomole flags to the user.
5. Performance in the Browser
Because the algorithm runs in pure Dart compiled to WebAssembly/JavaScript, the entire pipeline:
- String parsing
- Matrix formulation
- Rational Gaussian-Jordan elimination
- Integer scaling
executes in under 3 milliseconds on mobile browsers. This gives Alomole its signature responsive feel: equations balance in real time as the user types.