chemistrykit.solutions#
chemistrykit.solutions: solution chemistry.
pH/pOH and weak acid/base equilibria with Henderson-Hasselbalch buffers,
Van Slyke buffer capacity, and Bjerrum polyprotic speciation; a full
titration-curve solver (strong/weak acid-base) with Gran-plot
equivalence-point estimation; solubility equilibria (Ksp, common-ion
effect); and Debye-Huckel limiting/extended and Davies
activity-coefficient laws. Colligative properties (freezing-point
depression, boiling-point elevation, osmotic pressure) live in
chemistrykit.thermo instead – see
chemistrykit.thermo.freezing_point_depression() and friends.
- class chemistrykit.solutions.Buffer(pKa, acid_conc, base_conc)[source]#
Bases:
objectAn acid/conjugate-base buffer, via the Henderson-Hasselbalch approximation.
- Parameters:
- classmethod from_target_ph(pKa, target_pH, total_conc)[source]#
Build a
Bufferwith a target pH from a fixed total buffer concentration.Inverting the Henderson-Hasselbalch equation for the base/acid ratio \(r=[A^-]/[HA] = 10^{pH-pKa}\) and distributing the fixed total concentration \(C=[HA]+[A^-]\) between the two forms in that ratio.
- Parameters:
- Return type:
- Returns:
Buffer
Examples
Recovering the target pH exactly:
>>> buf = Buffer.from_target_ph(pKa=4.76, target_pH=5.0, total_conc=0.20) >>> round(buf.pH(), 6) 5.0 >>> round(buf.acid_conc + buf.base_conc, 6) 0.2
- pH()[source]#
Return the buffer’s pH via
henderson_hasselbalch_ph().- Return type:
- Returns:
float
- class chemistrykit.solutions.GranPlotResult(Vb, G, slope, intercept, equivalence_volume)[source]#
Bases:
objectContainer for the output of
chemistrykit.solutions.systems.titration.gran_plot().
- class chemistrykit.solutions.StrongAcidStrongBaseTitration(Ca, Va, Cb, Kw=1e-14)[source]#
Bases:
TitrationTitrating a strong acid (in the flask) with a strong base (the titrant).
- Parameters:
Examples
The equivalence volume (where moles acid = moles base) is exactly
Ca*Va/Cb, and the numerically detected steepest-ascent point should match it closely:>>> import numpy as np >>> titration = StrongAcidStrongBaseTitration(Ca=0.100, Va=0.050, Cb=0.100) >>> Vb_equiv_exact = titration.equivalence_volume() >>> round(Vb_equiv_exact, 4) 0.05 >>> Vb = np.linspace(1e-6, 0.09, 20000) >>> Vb_equiv_numeric = titration.find_equivalence_point(Vb) >>> abs(Vb_equiv_numeric - Vb_equiv_exact) < 1e-3 True
- class chemistrykit.solutions.Titration[source]#
Bases:
ABCCommon base for a strong/weak acid-base titration-curve model.
Concrete subclasses implement
pH_at();curve()andfind_equivalence_point()are then available on every subclass for free, mirroring howchemistrykit.kinetics.core.base_system.ReactionNetwork.integrate()is shared machinery built once atop each subclass’srhs.- curve(Vb)[source]#
Compute the full titration curve over a range of titrant volumes.
- Parameters:
Vb (array-like of float) – Volumes of titrant added, in L.
- Return type:
- Returns:
TitrationResult
- find_equivalence_point(Vb)[source]#
Numerically locate the equivalence point as the point of steepest pH ascent.
The equivalence point is where \(d(\text{pH})/dV_b\) is largest in magnitude – the inflection point of the titration curve (Harris, Quantitative Chemical Analysis, 9th ed., Ch. 11). This is a generic numerical detector shared by every
Titrationsubclass; compare its result against a subclass’s closed-form equivalence volume (stoichiometricCa*Va/Cb) as a correctness check.
- class chemistrykit.solutions.TitrationResult(Vb, pH)[source]#
Bases:
objectContainer for the output of a
Titration.curve()call.
- class chemistrykit.solutions.WeakAcid(Ca, Ka, Kw=1e-14)[source]#
Bases:
WeakElectrolyteA weak monoprotic acid HA in water, solved exactly (see module docstring).
- Parameters:
Examples
0.100 M acetic acid (Ka = 1.8e-5): the exact solution is very close to the textbook approximation \([H^+] \approx \sqrt{K_a C_a}\) here, since \(K_aC_a \gg K_w\):
>>> acid = WeakAcid(Ca=0.100, Ka=1.8e-5) >>> round(acid.h_concentration(), 6) 0.001333 >>> round(float(np.sqrt(1.8e-5 * 0.100)), 6) 0.001342 >>> round(acid.pH(), 3) 2.875
- class chemistrykit.solutions.WeakAcidStrongBaseTitration(Ca, Va, Ka, Cb, Kw=1e-14)[source]#
Bases:
TitrationTitrating a weak acid HA (in the flask) with a strong base (the titrant).
- Parameters:
Examples
At the half-equivalence volume, exactly half the acid has been converted to its conjugate base, so by Henderson-Hasselbalch
pH = pKathere:>>> import numpy as np >>> titration = WeakAcidStrongBaseTitration(Ca=0.100, Va=0.050, Ka=1.8e-5, Cb=0.100) >>> Vb_half = titration.equivalence_volume() / 2.0 >>> pKa = float(-np.log10(1.8e-5)) >>> bool(abs(float(titration.pH_at(np.array([Vb_half]))[0]) - pKa) < 1e-3) True
- class chemistrykit.solutions.WeakBase(Cb, Kb, Kw=1e-14)[source]#
Bases:
WeakElectrolyteA weak base B in water, solved exactly (see module docstring).
- Parameters:
Examples
0.100 M ammonia (Kb = 1.8e-5), by the acid/base symmetry the same numbers as the
WeakAcidexample, but for pOH:>>> base = WeakBase(Cb=0.100, Kb=1.8e-5) >>> round(base.oh_concentration(), 6) 0.001333 >>> round(base.pH(), 3) 11.125
- oh_concentration()[source]#
Return the equilibrium \([OH^-]\), in mol/L.
- Return type:
- Returns:
float
- class chemistrykit.solutions.WeakBaseStrongAcidTitration(Cb, Vb0, Kb, Ca, Kw=1e-14)[source]#
Bases:
TitrationTitrating a weak base B (in the flask) with a strong acid (the titrant).
The mirror image of
WeakAcidStrongBaseTitration(swap \([H^+] \leftrightarrow [OH^-]\), \(K_a \leftrightarrow K_b\)).- Parameters:
Examples
At the half-equivalence volume,
pOH = pKb:>>> import numpy as np >>> titration = WeakBaseStrongAcidTitration(Cb=0.100, Vb0=0.050, Kb=1.8e-5, Ca=0.100) >>> Va_half = titration.equivalence_volume() / 2.0 >>> pKb = float(-np.log10(1.8e-5)) >>> pH_half = float(titration.pH_at(np.array([Va_half]))[0]) >>> pOH_half = 14.0 - pH_half # Kw = 1e-14 -> pKw = 14 >>> bool(abs(pOH_half - pKb) < 1e-3) True
- class chemistrykit.solutions.WeakElectrolyte[source]#
Bases:
ABCCommon interface for a single weak acid or weak base dissociation equilibrium.
A concrete subclass represents either a weak acid (in which case
_ion_concentration()returns \([H^+]\)) or a weak base (returning \([OH^-]\)) – never both – sopercent_dissociation()’s formula \(\alpha = ([\text{ion}] - K_w/[\text{ion}])/C\) applies unchanged to either case (for an acid, \([\text{ion}] - K_w/[\text{ion}] = [H^+] - [OH^-] = [A^-]\), the dissociated fraction’s concentration; for a base, symmetrically, \([OH^-] - [H^+] = [BH^+]\)).
- chemistrykit.solutions.activity_coefficient_davies(z, I, A=0.509, b=0.3)[source]#
Davies equation: \(\log_{10}\gamma = -A z^2\left(\frac{\sqrt{I}}{1+\sqrt{I}} - bI\right)\).
An empirical extension of the Guntelberg form (
activity_coefficient_debye_huckel_extended()withBa = 1) by a term linear in ionic strength, which makes \(\log\gamma\) pass through a minimum and turn back up at high ionic strength, as measured activity coefficients do; useful up to roughly \(I \approx 0.5\) mol/L with no ion-specific parameter at all. Davies originally used \(b = 0.2\) (C. W. Davies, J. Chem. Soc. 1938, 2093); the now-standard \(b = 0.3\) is from his later monograph (C. W. Davies, Ion Association, Butterworths, 1962).- Parameters:
- Return type:
- Returns:
float – Activity coefficient \(\gamma\).
Examples
With
b = 0it is exactly the Guntelberg (Ba = 1) extended law:>>> g_davies = activity_coefficient_davies(z=1, I=0.05, b=0.0) >>> g_ext = activity_coefficient_debye_huckel_extended(z=1, I=0.05, Ba=1.0) >>> round(g_davies - g_ext, 12) 0.0
The linear term raises \(\gamma\) again at high ionic strength:
>>> activity_coefficient_davies(z=1, I=1.0) > activity_coefficient_davies(z=1, I=0.4) True
- chemistrykit.solutions.activity_coefficient_debye_huckel_extended(z, I, A=0.509, Ba=1.0)[source]#
Extended Debye-Huckel law: \(\log_{10}\gamma = -A z^2 \sqrt{I}/(1+Ba\sqrt{I})\).
Extends the limiting law’s validity to moderate ionic strength (roughly \(I \lesssim 0.1\) mol/L) by accounting for the ion’s finite size via the dimensionless product Ba (the Debye-Huckel parameter B times an effective ion-size parameter a); Ba=1 is a commonly used simplification when a specific ion-size parameter isn’t available (Atkins & de Paula, Physical Chemistry, 11th ed., Ch. 5.9, eq. 5.70) – this is itself still an approximation, one step better than the limiting law, not an exact result.
- Parameters:
- Return type:
- Returns:
float – Activity coefficient \(\gamma\).
Examples
Reduces to the limiting law as \(I \to 0\):
>>> import numpy as np >>> I = 1e-8 >>> extended = activity_coefficient_debye_huckel_extended(z=1, I=I) >>> limiting = activity_coefficient_debye_huckel_limiting(z=1, I=I) >>> bool(np.isclose(extended, limiting, rtol=1e-3)) True
At higher ionic strength, the extended law predicts a less severe deviation from ideality than the (over-extrapolated) limiting law:
>>> extended_at_01 = activity_coefficient_debye_huckel_extended(z=1, I=0.1) >>> limiting_at_01 = activity_coefficient_debye_huckel_limiting(z=1, I=0.1) >>> extended_at_01 > limiting_at_01 True
- chemistrykit.solutions.activity_coefficient_debye_huckel_limiting(z, I, A=0.509)[source]#
Debye-Huckel limiting law: \(\log_{10}\gamma = -A z^2 \sqrt{I}\).
Valid only at low ionic strength (roughly \(I \lesssim 0.01\) mol/L), where ion-ion interactions are dominated by long-range electrostatics (Debye & Huckel, 1923; Atkins & de Paula, Physical Chemistry, 11th ed., Ch. 5.9, eq. 5.69).
- Parameters:
- Return type:
- Returns:
float – Activity coefficient \(\gamma\) (dimensionless, \(\leq 1\)).
Examples
At zero ionic strength the activity coefficient is exactly 1 (ideal, infinitely dilute behavior):
>>> round(activity_coefficient_debye_huckel_limiting(z=1, I=0.0), 6) 1.0
A more highly charged ion deviates further from ideality at the same ionic strength:
>>> gamma_1 = activity_coefficient_debye_huckel_limiting(z=1, I=0.01) >>> gamma_2 = activity_coefficient_debye_huckel_limiting(z=2, I=0.01) >>> gamma_2 < gamma_1 < 1.0 True
- chemistrykit.solutions.buffer_capacity(pH, C, Ka, Kw=1e-14)[source]#
Van Slyke’s buffer value \(\beta = dC_b/d\mathrm{pH}\) of a weak monoprotic acid.
The moles of strong base per litre needed to raise the pH of a solution containing a weak acid/conjugate base pair (total concentration \(C\)) by one unit, from differentiating the charge balance \(C_b = CK_a/(K_a+h) + K_w/h - h\):
\[\beta = \ln 10\left(h + \frac{K_w}{h} + \frac{CK_ah}{(K_a+h)^2}\right)\]The buffer term peaks at \(pH = pK_a\) with value \(\ln 10\,C/4\) (D. D. Van Slyke, J. Biol. Chem. 52, 525 (1922); Harris, Quantitative Chemical Analysis, 9th ed., Ch. 9-5).
- Parameters:
- Returns:
float or ndarray – Buffer capacity, in mol/L per pH unit.
Examples
A 0.10 M acetate buffer at \(pH = pK_a\) (water terms negligible):
>>> import numpy as np >>> round(float(buffer_capacity(4.76, C=0.10, Ka=10**-4.76)), 4) 0.0576 >>> round(float(np.log(10) * 0.10 / 4), 4) 0.0576
- chemistrykit.solutions.gran_plot(Vb, pH, Va)[source]#
Gran’s linearization of a strong-acid/strong-base titration curve.
Before the equivalence point of a strong acid (initial volume \(V_a\)) titrated with a strong base, the charge balance gives, once \([OH^-]\) is negligible,
\[(V_a + V_b)\,10^{-\mathrm{pH}} = C_aV_a - C_bV_b,\]a straight line in \(V_b\) whose x-intercept is exactly the equivalence volume \(V_e = C_aV_a/C_b\) – so \(V_e\) can be found by extrapolating pre-equivalence data, without having to resolve the steep inflection itself (G. Gran, Analyst 77, 661 (1952); Harris, Quantitative Chemical Analysis, 9th ed., Ch. 11-5). Neither \(C_a\) nor \(C_b\) needs to be known.
- Parameters:
- Return type:
- Returns:
GranPlotResult
Examples
Recovering the 50.00 mL equivalence volume of 50.00 mL of 0.100 M HCl titrated with 0.100 M NaOH from pre-equivalence points only:
>>> import numpy as np >>> titration = StrongAcidStrongBaseTitration(Ca=0.100, Va=0.050, Cb=0.100) >>> Vb = np.linspace(0.030, 0.045, 10) >>> result = gran_plot(Vb, titration.pH_at(Vb), Va=0.050) >>> round(result.equivalence_volume * 1000.0, 4) 50.0 >>> round(-result.slope, 6) # slope is -Cb 0.1
- chemistrykit.solutions.h_from_ph(pH)[source]#
Inverse of
ph_from_h(): \([H^+] = 10^{-pH}\).Examples
>>> round(float(h_from_ph(7.0)), 10) 1e-07
- chemistrykit.solutions.henderson_hasselbalch_ph(pKa, base_conc, acid_conc)[source]#
The Henderson-Hasselbalch equation: \(pH = pK_a + \log_{10}([A^-]/[HA])\).
A buffer approximation valid when both base_conc and acid_conc are large enough that dissociation/hydrolysis negligibly perturbs their equilibrium ratio (L. J. Henderson, Am. J. Physiol. 21, 173 (1908); K. A. Hasselbalch, Biochem. Z. 78, 112 (1917); see Atkins & de Paula, Physical Chemistry, 11th ed., Ch. 6.7).
- Parameters:
- Return type:
- Returns:
float
Examples
An equimolar acid/conjugate-base buffer has pH = pKa exactly:
>>> round(henderson_hasselbalch_ph(pKa=4.76, base_conc=0.10, acid_conc=0.10), 6) 4.76
- chemistrykit.solutions.ionic_strength(concentrations, charges)[source]#
Ionic strength \(I = \frac{1}{2}\sum_i c_i z_i^2\).
- Parameters:
- Return type:
- Returns:
float
Examples
A 0.10 M solution of a 1:1 electrolyte (e.g. NaCl) has ionic strength equal to its concentration:
>>> round(ionic_strength([0.10, 0.10], [1, -1]), 6) 0.1
A 0.10 M solution of a 1:2 electrolyte (e.g. CaCl2) has three times the concentration’s worth of ionic strength:
>>> round(ionic_strength([0.10, 0.20], [2, -1]), 6) 0.3
- chemistrykit.solutions.ksp_from_molar_solubility(s, cation_coeff, anion_coeff)[source]#
Compute \(K_{sp} = p^pq^q s^{p+q}\) from a measured molar solubility.
- Parameters:
- Return type:
- Returns:
float
Examples
AgCl (1:1 salt) with molar solubility 1.3e-5 mol/L:
>>> round(ksp_from_molar_solubility(s=1.3e-5, cation_coeff=1, anion_coeff=1), 12) 1.69e-10
CaF2 (1:2 salt), Ksp = [Ca2+][F-]^2 = s*(2s)^2 = 4s^3:
>>> round(ksp_from_molar_solubility(s=2.1e-4, cation_coeff=1, anion_coeff=2), 12) 3.7e-11
- chemistrykit.solutions.molar_solubility_from_ksp(Ksp, cation_coeff, anion_coeff)[source]#
Invert
ksp_from_molar_solubility(): \(s = (K_{sp}/(p^pq^q))^{1/(p+q)}\).- Parameters:
- Return type:
- Returns:
float – Molar solubility, in mol/L.
Examples
Round-trips with
ksp_from_molar_solubility():>>> Ksp = ksp_from_molar_solubility(s=1.3e-5, cation_coeff=1, anion_coeff=1) >>> round(molar_solubility_from_ksp(Ksp, cation_coeff=1, anion_coeff=1), 12) 1.3e-05
- chemistrykit.solutions.molar_solubility_with_common_ion(Ksp, cation_coeff, anion_coeff, common_ion_conc, common_ion='cation')[source]#
Molar solubility of \(M_pX_q\) in the presence of an added common ion.
Le Chatelier’s principle predicts that adding a common ion (independently, e.g. from a soluble salt sharing that ion) suppresses the target salt’s solubility relative to
molar_solubility_from_ksp()(Atkins & de Paula, Physical Chemistry, 11th ed., Ch. 6.8b). This solves the exact polynomial\[K_{sp} = (ps + C_0)^p (qs)^q \quad\text{(common cation, added at concentration } C_0\text{)}\]or the mirror-image equation for a common anion, via
chemistrykit.solutions.utils.rootfinding.find_positive_root()(exact for any p, q – not just the textbook 1:1-salt quadratic special case).- Parameters:
Ksp (
float) – Solubility product of the salt whose solubility is being found.cation_coeff (
int) – Stoichiometric coefficient of the cation, p.anion_coeff (
int) – Stoichiometric coefficient of the anion, q.common_ion_conc (
float) – Concentration of the common ion contributed by an independent (fully dissociated) source, in mol/L.common_ion (
str) – Which ion is shared with the independent source.
- Return type:
- Returns:
float – Molar solubility of the salt, in mol/L.
Examples
AgCl (Ksp = 1.8e-10) is much less soluble in 0.10 M NaCl (common Cl- ion) than in pure water:
>>> s_pure = molar_solubility_from_ksp(1.8e-10, cation_coeff=1, anion_coeff=1) >>> s_common = molar_solubility_with_common_ion(1.8e-10, cation_coeff=1, anion_coeff=1, common_ion_conc=0.10, common_ion="anion") >>> s_common < s_pure True >>> round(s_common, 12) # dominated by the 0.10 M Cl- already present: s ~= Ksp/0.10 1.8e-09
- chemistrykit.solutions.oh_from_poh(pOH)[source]#
Inverse of
poh_from_oh(): \([OH^-] = 10^{-pOH}\).
- chemistrykit.solutions.ph_from_h(h_conc)[source]#
pH \(= -\log_{10}[H^+]\).
- Parameters:
h_conc (float or array-like of float) – Hydrogen ion concentration, in mol/L.
- Return type:
- Returns:
float or ndarray
Examples
>>> round(float(ph_from_h(1.0e-7)), 6) 7.0
- chemistrykit.solutions.poh_from_oh(oh_conc)[source]#
pOH \(= -\log_{10}[OH^-]\).
Examples
>>> round(float(poh_from_oh(1.0e-7)), 6) 7.0
- chemistrykit.solutions.polyprotic_fractions(pH, Ka)[source]#
Bjerrum’s species-distribution fractions of a polyprotic acid \(H_nA\).
With stepwise dissociation constants \(K_1, \dots, K_n\) and \(h = [H^+]\), the fraction of the total acid present as \(H_{n-j}A^{j-}\) is
\[\alpha_j = \frac{h^{n-j}\prod_{i=1}^{j} K_i}{\sum_{k=0}^{n} h^{n-k}\prod_{i=1}^{k} K_i},\]which depends only on pH, not on the total concentration (N. Bjerrum, Die Theorie der alkalimetrischen und azidimetrischen Titrierungen, Stuttgart: Enke, 1914; Harris, Quantitative Chemical Analysis, 9th ed., Ch. 11-7).
- Parameters:
- Return type:
- Returns:
ndarray – Shape
(n + 1, len(pH)): rowjis \(\alpha_j\), the fraction that has lostjprotons. The rows sum to 1.
Examples
Carbonic acid (\(pK_1 = 6.35\), \(pK_2 = 10.33\)): at \(pH = pK_1\) the fully protonated form and bicarbonate are (almost exactly) equally abundant:
>>> alpha = polyprotic_fractions(6.35, [10**-6.35, 10**-10.33]) >>> [round(float(a), 3) for a in alpha[:, 0]] [0.5, 0.5, 0.0]