Note
Go to the end to download the full example code.
Guldberg and Waage’s law of mass action: a constant equilibrium quotient#
The law of mass action says that at equilibrium the concentrations of products and reactants, each raised to its stoichiometric power, combine into a quotient that is fixed at a given temperature. For \(HA \rightleftharpoons H^+ + A^-\):
Below, the equilibrium concentrations of three weak acids are solved
exactly with WeakAcid
over six decades of total concentration. The individual concentrations
change by orders of magnitude, but the mass-action quotient stays fixed
at \(K_a\), while a quotient that is not the mass-action form,
such as \([H^+]/[HA]\), does not.
import matplotlib.pyplot as plt
import numpy as np
from chemistrykit.solutions.systems.acid_base import WeakAcid
acids = {"formic acid": 1.8e-4, "acetic acid": 1.8e-5, "hypochlorous acid": 3.0e-8}
Ca_values = np.logspace(-5, 0, 60)
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(11, 4.5))
for (name, Ka), color in zip(acids.items(), ["steelblue", "darkorange", "seagreen"]):
Q_mass_action, Q_other = [], []
for Ca in Ca_values:
acid = WeakAcid(Ca=Ca, Ka=Ka)
h = acid.h_concentration()
A_minus = Ka * Ca / (Ka + h) # from the mass balance and Ka
HA = Ca - A_minus
Q_mass_action.append(h * A_minus / HA)
Q_other.append(h / HA)
ax1.loglog(Ca_values, Q_mass_action, color=color, label=name)
ax1.axhline(Ka, color=color, linestyle=":", alpha=0.6)
ax2.loglog(Ca_values, Q_other, color=color, label=name)
ax1.set_xlabel("total acid concentration $C_a$ (mol/L)")
ax1.set_ylabel(r"$[H^+][A^-]/[HA]$")
ax1.set_title("Mass-action quotient: constant = $K_a$")
ax1.legend(fontsize=8)
ax2.set_xlabel("total acid concentration $C_a$ (mol/L)")
ax2.set_ylabel(r"$[H^+]/[HA]$")
ax2.set_title("A non-mass-action ratio: not constant")
fig.tight_layout()

The exact solver never imposes the quotient directly – it solves a cubic from mass balance, charge balance, and water autoionization – yet the recovered quotient matches \(K_a\) to rounding error:
[H+][A-]/[HA] = 1.800000e-05 (Ka = 1.800000e-05)
Total running time of the script: (0 minutes 0.164 seconds)