Calculation of the pH Values of Aqueous Systems Containing Carbonic Acid and Significance for Natural Waters, Following (Near-)Exact and Approximated Solutions: The Importance of the Boundary Conditions
Abstract
1. Introduction
- (i).
- Reaction (1) is responsible for the long-term increase in Mg levels;
- (ii).
- Contrary to Mg, Ca levels rather underwent a long-term decrease. The most likely reason is that, differently from MgCO3, CaCO3 is very near saturation in Swiss rivers and the increase in water temperature has caused a decrease in Ca2+ solubility.
2. Results and Discussion
2.1. Calculation of the pH Value of a Solution of H2CO3*, Having Total Concentration cT
- (a)
- If cT is high enough, then
- (b)
- If cT → 0, then [H3O+] = (Kw)½ (typically, pH 7)

2.2. Calculation of the pH Value of a Solution Resulting from the Reaction Between CaCO3(s) and CO2(g)
2.3. The Most General Case: A H2CO3* Solution (cT) in the Presence of Alkalinity, [Alk]
3. Methods
- 1.
- The first script solves different simplified equilibrium equations for [H3O+] in carbonic acid systems. Numerical solutions were obtained with ‘fsolve’, and the corresponding pH values were compared across five approximations. The results are reported in Figure 2.
import numpy as np
import matplotlib.pyplot as plt
from scipy.optimize import fsolve
Ka1_star = 10**-6.3
Kw = 1e-14
cT = np.logspace(-8, -1, 500)
pH_eq1 = []
pH_eq2 = []
pH_eq3 = []
pH_eq4 = []
pH_eq5 = []
def eq1(H, cT_val):
return H - (Ka1_star * cT_val) / (H + Ka1_star) - Kw / H
def eq2(H, cT_val):
return H - cT_val - Kw / H
def eq4(H, cT_val):
return H - (Ka1_star * cT_val) / (H + Ka1_star)
for ct in cT:
H1 = fsolve(eq1, ct, args=(ct))[0]
pH_eq1.append(-np.log10(H1))
H2 = fsolve(eq2, ct, args=(ct))[0]
pH_eq2.append(-np.log10(H2))
H3 = np.sqrt(Ka1_star * ct + Kw)
pH_eq3.append(-np.log10(H3))
H4 = fsolve(eq4, ct, args=(ct))[0]
pH_eq4.append(-np.log10(H4))
H5 = np.sqrt(Ka1_star * ct)
pH_eq5.append(-np.log10(H5))
plt.figure(figsize=(10, 6))
plt.semilogx(cT, pH_eq1, '-', label='[H⁺] = (Ka1·cT)/(H⁺ + Ka1) + Kw/H⁺')
plt.semilogx(cT, pH_eq2, '--', label='[H⁺] = cT + Kw/H⁺')
plt.semilogx(cT, pH_eq3, ':', label='[H⁺] = √(Ka1·cT + Kw)')
plt.semilogx(cT, pH_eq4, '-.', label='[H⁺] = (Ka1·cT)/(H⁺ + Ka1)')
plt.semilogx(cT, pH_eq5, '-', label='[H⁺] = √(Ka1·cT)')
plt.xlabel('cT [mol/L]')
plt.ylabel('pH')
plt.title('Comparison of five approximations for [H⁺] in H₂CO₃')
plt.legend()
plt.grid(False)
plt.tight_layout()
plt.show()
- 2.
-
The second script derives and solves a cubic equation for [H3O+], representing an approximate description of the carbonate system. The cubic equation was solved numerically with ‘fsolve’, while an analytical approximation was also implemented. The two solutions (exact vs. approximate) were then compared across a range of cT values (Figure 3):
import numpy as np
import matplotlib.pyplot as plt
from scipy.optimize import fsolve
Ka1 = 10**-6.3
Ka2 = 10**-10.3
Kw = 1e-14
cT_values = np.logspace(-8, -1, 500)
pH_exact = []
pH_approx = []
def cubic_eq(H, cT_val):
return H**3 + (Ka1 + 2*cT_val)*H**2 - Kw*H - Ka1*(2*cT_val*Ka2 + Kw)
def approx_H(cT_val):
numerator = Ka1 * (2*cT_val*Ka2 + Kw)
denominator = 2*cT_val + Ka1
return np.sqrt(numerator / denominator)
for ct in cT_values:
H_root = fsolve(cubic_eq, 1e-7, args=(ct))[0]
H_approx = approx_H(ct)
pH_exact.append(-np.log10(H_root))
pH_approx.append(-np.log10(H_approx))
plt.figure(figsize=(10, 6))
plt.semilogx(cT_values, pH_exact, label=r'exact pH: $[H^+]^3 + (K_{a1}* + 2c_T)[H^+]^2 - K_w[H^+] - K_{a1}*(2c_T K_{a2} + K_w) = 0$')
plt.semilogx(cT_values, pH_approx, '--', label=r'approximate pH: $[H^+] = \sqrt{\frac{K_{a1}*(2c_T K_{a2} + K_w)}{2c_T + K_{a1}*}}$')
plt.xlabel('cT [mol/L]')
plt.ylabel('pH')
plt.title('pH of a bicarbonate solution')
plt.legend()
plt.grid(False)
plt.tight_layout()
plt.show()
- 3.
-
The third script computes the exact relationship between alkalinity ([Alk]) and pH for different values of the total inorganic carbon concentration (cT). The hydrogen ion concentration was obtained by solving the fourth-degree equation with the Brent’s root-finding method (‘brentq’). This allowed a robust identification of all valid physical roots. The results were plotted as pH vs. alkalinity curves, for different cT values (Figure 4).
import numpy as np
import matplotlib.pyplot as plt
from scipy.optimize import brentq
import pandas as pd
Ka1 = 10**-6.3
Ka2 = 10**-10.3
Kw = 1e-14
Alk_values = np.linspace(-0.01, 0.04, 400)
H_min, H_max = 1e-14, 1.0
N_scan = 400
xtol_brentq = 1e-15
rtol_brentq = 1e-12
cT_values = [1e-2, 0.007, 0.005, 0.003, 1e-3, 1e-4, 1e-5, 1e-6, 1e-7]
def fH(H, Alk, Ka1, Ka2, Kw, cT):
return (H**4
+ H**3*(Alk + Ka1)
+ H**2*(Alk*Ka1 + Ka1*Ka2 - Ka1*cT - Kw)
+ H*(Alk*Ka1*Ka2 - 2*Ka1*Ka2*cT - Ka1*Kw)
- Ka1*Ka2*Kw)
def find_sign_change_brackets(Alk, cT):
Hgrid = np.logspace(np.log10(H_min), np.log10(H_max), N_scan)
fvals = fH(Hgrid, Alk, Ka1, Ka2, Kw, cT)
brackets = []
for i in range(len(Hgrid) - 1):
f1, f2 = fvals[i], fvals[i+1]
if np.isnan(f1) or np.isnan(f2): continue
if abs(f1) < 1e-300: f1 = np.copysign(1e-300, f1 if f1 != 0 else 1.0)
if abs(f2) < 1e-300: f2 = np.copysign(1e-300, f2 if f2 != 0 else -1.0)
if f1 * f2 < 0.0:
brackets.append((Hgrid[i], Hgrid[i+1]))
return brackets
def roots_with_brentq(Alk, cT):
brackets = find_sign_change_brackets(Alk, cT)
roots = []
for (a, b) in brackets:
try:
r = brentq(fH, a, b, args=(Alk, Ka1, Ka2, Kw, cT),
xtol=xtol_brentq, rtol=rtol_brentq, maxiter=200)
if H_min <= r <= H_max:
roots.append(r)
except ValueError:
pass
return sorted(set(roots))
plt.figure(figsize=(8, 6))
all_results = {"Alk": Alk_values}
for cT in cT_values:
H_list, pH_list = [], []
prev_H = None
for i, Alk in enumerate(Alk_values):
roots = roots_with_brentq(Alk, cT)
if not roots:
H_list.append(np.nan)
pH_list.append(np.nan)
prev_H = None
continue
if i == 0:
h_choice = max(roots)
else:
log_prev = np.log(prev_H) if prev_H else 0
h_choice = min(roots, key=lambda h: abs(np.log(h) - log_prev))
prev_H = h_choice
H_list.append(h_choice)
pH_list.append(-np.log10(h_choice))
all_results[f"pH_cT={cT:g}"] = pH_list
plt.plot(Alk_values, pH_list, label=f"cT={cT:g}")
plt.xlabel(r"[Alk], eq L$^{-1}$")
plt.ylabel("pH")
plt.title("pH vs Alcalinity for Different Values of $c_T$ (M)")
plt.xlim(-0.01, 0.04)
plt.ylim(2, 14)
plt.grid(True, linestyle="-", alpha=0.6)
plt.legend()
plt.tight_layout()
plt.show()
df = pd.DataFrame(all_results)
df.to_excel("pH_vs_Alk.xlsx", index=False)
print("File Excel saved as pH_vs_Alk.xlsx")
- 4.
-
In addition to the equilibrium and alkalinity solvers, a dedicated Python script was developed to compute the first and second derivatives of the alkalinity function, with respect to both H3O+ concentration and pH. The code was implemented using ‘NumPy’ for efficient vectorized calculations and ‘Matplotlib’ for visualization (Figure 5). Symbolic expressions for the derivatives were derived analytically and then translated into Python functions. The script evaluates the alkalinity, expressed as a function of pH, its first derivative ∂[Alk]/∂pH, and the second derivative ∂2[Alk]/∂pH2 over a dense grid of pH values. Zeros of the second derivative, corresponding to inflection points of the alkalinity curve, were identified numerically and compared across different conditions (Table 1).
import numpy as np
import matplotlib.pyplot as plt
from scipy.optimize import brentq
Ka1 = 10**-6.3
Ka2 = 10**-10.3
Kw = 1e-14
cT = 1e-2
pH = np.linspace(2, 12, 1000)
H = 10**(-pH)
D = H**2 + Ka1*H + Ka1*Ka2
Alk = cT * ((Ka1*H)/D + (2*Ka1*Ka2)/D) + (Kw/H) - H
dAlk_dH = (
cT * (Ka1*D - (Ka1*H + 2*Ka1*Ka2)*(2*H + Ka1)) / D**2
- Kw/H**2 - 1)
z = -np.log(10) * H * dAlk_dH
d2Alk_dH2 = (2*Kw/H**3 - cT*Ka1 * ((2*H + 4*Ka2)*D - 2*(H**2 + 4*Ka2*H + Ka1*Ka2)*(2*H + Ka1)) / D**3)
y = -np.log(10) * (H * dAlk_dH + H**2 * d2Alk_dH2)
zeros = []
for i in range(len(pH)-1):
if y[i] == 0:
zeros.append(pH[i])
elif y[i]*y[i+1] < 0:
try:
root = brentq(lambda xx: np.interp(xx, pH, y), pH[i], pH[i+1])
zeros.append(root)
except ValueError:
pass
print("Zeros of the second derivative (pH values corresponding to the minima and maxima of the first derivative):")
print(zeros)
labels = [
r"$\mathrm{H_2CO_3^*}$",
r"$\mathrm{H_2CO_3^*/HCO_3^-}$",
r"$\mathrm{HCO_3^-}$",
r"$\mathrm{HCO_3^-/CO_3^{2-}}$",
r"$\mathrm{CO_3^{2-}}$"]
plt.figure(figsize=(7,10))
plt.plot(pH, Alk, label=r"$[\mathrm{Alk}]$")
plt.plot(pH, z, label=r"$\frac{\partial [\mathrm{Alk}]}{\partial \mathrm{pH}}$")
plt.plot(pH, y, label=r"$\frac{\partial^2 [\mathrm{Alk}]}{\partial \mathrm{pH}^2}$")
plt.axhline(0, color="k", linestyle="--", linewidth=0.8)
for i, root in enumerate(zeros):
z_val = np.interp(root, pH, z)
plt.plot(root, z_val, "ro") # punto rosso
label = labels[i] if i < len(labels) else labels[-1]
offset_x, offset_y = -0.7, 0.002
if i == len(zeros) - 1:
offset_x, offset_y = -0.7, -0.003 # sinistra e più in basso
if i == len(zeros) - 4:
offset_x, offset_y = -2, 0.002
if i == len(zeros) - 2:
offset_x, offset_y = -1, 0.002
plt.annotate(
f"{label}\n(pH={root:.2f})",
xy=(root, z_val),
xytext=(root + offset_x, z_val + offset_y),
arrowprops=dict(arrowstyle="->", color="red"),
fontsize=10,
bbox=dict(boxstyle="round,pad=0.3", fc="yellow", alpha=0.3))
plt.xlabel("pH")
plt.ylabel(r"$[\mathrm{Alk}] \; (\mathrm{eq/L})$")
plt.ylim(-0.005, 0.015)
plt.title(r"Alkalinity Function Analysis for $c_T = 0.01 \,\mathrm{M}$")
plt.legend(fontsize=13, loc="best", frameon=True, fancybox=True, shadow=True)
plt.grid(True)
plt.show()
4. Conclusions
Author Contributions
Funding
Institutional Review Board Statement
Informed Consent Statement
Data Availability Statement
Conflicts of Interest
References
- Zobrist, J.; Schoenenberger, U.; Figura, S.; Hug, S.J. Long-term trends in Swiss rivers sampled continuously over 39 years reflect changes in geochemical processes and pollution. Environ. Sci. Pollut. Res. 2018, 25, 16788–16809. [Google Scholar] [CrossRef] [Scilit] [PubMed]
- Mostofa, K.M.G.; Liu, C.-Q.; Zhai, W.; Minella, M.; Vione, D.; Gao, K.; Minakata, D.; Arakaki, T.; Yoshioka, T.; Hayakawa, K.; et al. Reviews and Syntheses: Ocean acidification and its potential impacts on marine ecosystems. Biogeosciences 2016, 13, 1767–1786. [Google Scholar] [CrossRef] [Scilit]
- Gustafsson, J. Visual MINTEQ, version 3.1; MINTEQ: Uppsala, Sweden, 2000.
- Robbins, L.L.; Hansen, M.E.; Kleypas, J.A.; Meylan, S.C. CO2calc: A User-Friendly Seawater Carbon Calculator for Windows, Mac OS X, and iOS (iPhone); U.S. Geological Survey: Reston, VA, USA, 2010. [Google Scholar] [CrossRef] [Scilit]
- Castellino, L.; Alladio, E.; Bertinetti, S.; Lando, G.; De Stefano, C.; Blasco, S.; García-España, E.; Gama, S.; Berto, S.; Milea, D. PyES–an open-source software for the computation of solution and precipitation equilibria. Chemom. Intell. Lab. Syst. 2023, 239, 104860. [Google Scholar] [CrossRef] [Scilit]
- Mostofa, K.M.G.; Liu, C.-Q.; Vione, D. Quantifying the possible short- and long-term impacts of superoxide redox chemistry on seawater pH. Can. Chem. Trans. 2015, 3, 285–290. [Google Scholar] [CrossRef] [Scilit]
- Christian, G.D.; Dasgupta, P.K.; Schug, K.A. Analytical Chemistry, 7th ed.; Wiley: New York, NY, USA, 2020. [Google Scholar]
- Python. Available online: https://www.python.org (accessed on 1 September 2025).
- Skoog, D.A.; West, D.M.; Holler, F.J.; Crouch, S.E. Analytical Chemistry: An Introduction; Brooks/Cole Pub Co.: Pacific Grove, CA, USA, 2000; 256p. [Google Scholar]
- National Institute of Standards and Technology. NIST Chemistry WebBook. Available online: https://webbook.nist.gov (accessed on 1 October 2025).
- Solution of Cubic Equations with the Python Code. Available online: https://docs.scipy.org/doc/scipy/reference/optimize.html (accessed on 1 October 2025).
- Sulpis, O.; Agrawal, P.; Wolthers, M.; Munhoven, G.; Walker, M.; Middelburg, J.J. Aragonite dissolution protects calcite at the seafloor. Nat. Commun. 2022, 13, 1104. [Google Scholar] [CrossRef] [Scilit] [PubMed]





| cT [M] | pH | Alk [eq/L] |
|---|---|---|
| 1.0 × 10−2 | 10.973 | 1.918 × 10−2 |
| 10.385 | 1.573 × 10−2 | |
| 8.295 | 1.000 × 10−2 | |
| 6.300 | 5.002 × 10−3 | |
| 4.156 | 1.518 × 10−6 | |
| 7.0 × 10−3 | 10.826 | 1.306 × 10−2 |
| 10.442 | 1.134 × 10−2 | |
| 8.293 | 6.999 × 10−3 | |
| 6.300 | 3.501 × 10−3 | |
| 4.235 | 1.521 × 10−6 | |
| 5.0 × 10−3 | 8.291 | 5.000 × 10−3 |
| 6.300 | 2.500 × 10−3 | |
| 4.309 | 1.524 × 10−6 | |
| 1.0 × 10−3 | 8.258 | 9.999 × 10−4 |
| 6.299 | 4.989 × 10−4 | |
| 4.670 | 1.551 × 10−6 | |
| 1.0 × 10−4 | 8.049 | 9.992 × 10−5 |
| 6.283 | 4.852 × 10−5 | |
| 5.221 | 1.685 × 10−6 | |
| 1.0 × 10−5 | 7.610 | 9.937 × 10−6 |
| 1.0 × 10−6 | 7.168 | 9.607 × 10−7 |
| 1.0 × 10−7 | 7.020 | 9.313 × 10−8 |
Disclaimer/Publisher’s Note: The statements, opinions and data contained in all publications are solely those of the individual author(s) and contributor(s) and not of MDPI and/or the editor(s). MDPI and/or the editor(s) disclaim responsibility for any injury to people or property resulting from any ideas, methods, instructions or products referred to in the content. |
© 2026 by the authors. Licensee MDPI, Basel, Switzerland. This article is an open access article distributed under the terms and conditions of the Creative Commons Attribution (CC BY) license.
Share and Cite
Rosso, A.; Vione, D. Calculation of the pH Values of Aqueous Systems Containing Carbonic Acid and Significance for Natural Waters, Following (Near-)Exact and Approximated Solutions: The Importance of the Boundary Conditions. Molecules 2026, 31, 292. https://doi.org/10.3390/molecules31020292
Rosso A, Vione D. Calculation of the pH Values of Aqueous Systems Containing Carbonic Acid and Significance for Natural Waters, Following (Near-)Exact and Approximated Solutions: The Importance of the Boundary Conditions. Molecules. 2026; 31(2):292. https://doi.org/10.3390/molecules31020292
Chicago/Turabian StyleRosso, Arianna, and Davide Vione. 2026. "Calculation of the pH Values of Aqueous Systems Containing Carbonic Acid and Significance for Natural Waters, Following (Near-)Exact and Approximated Solutions: The Importance of the Boundary Conditions" Molecules 31, no. 2: 292. https://doi.org/10.3390/molecules31020292
APA StyleRosso, A., & Vione, D. (2026). Calculation of the pH Values of Aqueous Systems Containing Carbonic Acid and Significance for Natural Waters, Following (Near-)Exact and Approximated Solutions: The Importance of the Boundary Conditions. Molecules, 31(2), 292. https://doi.org/10.3390/molecules31020292

