chemistrykit.analytical#

chemistrykit.analytical: analytical chemistry.

Acid-base titration curves are provided by chemistrykit.solutions.systems.titration; this subpackage adds redox and complexometric (EDTA) titration-curve simulation with equivalence-point detection; chromatographic plate theory and the van Deemter equation (resolution, selectivity); linear-regression calibration curves with IUPAC-convention limits of detection/quantitation; and propagation-of-uncertainty formulas plus Dixon’s Q-test for outlier rejection. Also: Gran plots, Kovats retention indices, Purnell’s resolution equation, Student’s t confidence intervals, Grubbs’ outlier test, the Horwitz precision function, and Savitzky-Golay smoothing.

class chemistrykit.analytical.ConfidenceIntervalResult(mean, std, n, t_critical, half_width)[source]#

Bases: object

Result of a t_confidence_interval() call.

Parameters:
half_width: float#

\(t\,s/\sqrt n\), the interval’s half-width.

Type:

float

property lower: float#

Lower end of the interval.

Type:

float

mean: float#

Sample mean of the replicates.

Type:

float

n: int#

Number of replicates.

Type:

int

std: float#

Sample standard deviation s (ddof=1).

Type:

float

t_critical: float#

Two-sided Student’s t critical value for n - 1 degrees of freedom.

Type:

float

property upper: float#

Upper end of the interval.

Type:

float

class chemistrykit.analytical.EDTATitration(C_metal, V_metal, K_conditional, C_edta)[source]#

Bases: TitrationCurve

A complexometric titration of a metal ion M by EDTA, via the conditional formation constant.

Models the 1:1 complexation \(M+Y\rightleftharpoons MY\) exactly (no large-K approximation needed, unlike RedoxTitration), using the conditional formation constant \(K_f'\) – the effective (pH- and side-reaction-corrected) formation constant at the titration’s fixed pH (Harris, Quantitative Chemical Analysis, 9th ed., Ch. 12.2; folding pH-dependence into a single conditional constant, rather than modeling EDTA’s stepwise protonation equilibria explicitly, is itself the standard simplification this class makes, flagged here as an approximation).

Mass balance on total metal \(C_M\) and total EDTA added \(C_Y\) (both diluted by the growing total volume), with \(x=[MY]\), \([M]=C_M-x\), \([Y]=C_Y-x\), and \(K_f'=x/[(C_M-x)(C_Y-x)]\), gives the exact quadratic

\[K_f'x^2-\left[K_f'(C_M+C_Y)+1\right]x+K_f'C_MC_Y=0\]

solved here in closed form for the physical root \(0\le x\le \min(C_M,C_Y)\), then \(pM=-\log_{10}[M]\).

Parameters:
  • C_metal (float) – Initial metal-ion concentration, in mol/L.

  • V_metal (float) – Initial metal-ion solution volume, in L.

  • K_conditional (float) – Conditional formation constant \(K_f'\) at the titration pH.

  • C_edta (float) – Titrant (EDTA) concentration, in mol/L.

Examples

At the equivalence volume (\(C_M=C_Y\)), pM approaches the textbook large-\(K_f'\) approximation \(pM\approx \frac12\log_{10}(K_f'/C_{M,eq})\) (Harris, Quantitative Chemical Analysis, 9th ed., eq. 12.2) as \(K_f'\) grows – verified here rather than assumed, since the approximation formula is itself only a limit of the exact quadratic solved above:

>>> import numpy as np
>>> titration = EDTATitration(C_metal=0.010, V_metal=0.050, K_conditional=1e12, C_edta=0.010)
>>> V_eq = titration.equivalence_volume()
>>> pM_exact = float(titration.response_at(np.array([V_eq]))[0])
>>> C_M_eq = titration.C_metal * titration.V_metal / (titration.V_metal + V_eq)
>>> pM_approx = 0.5 * np.log10(1e12 / C_M_eq)
>>> bool(abs(pM_exact - pM_approx) < 0.01)
True
equivalence_volume()[source]#

Return the exact (stoichiometric, 1:1) equivalence volume.

Return type:

float

Returns:

float

response_at(V)[source]#

Return the titration’s response variable at each titrant volume in V.

Parameters:

V (ndarray) – Volume(s) of titrant added, in L.

Returns:

ndarray

class chemistrykit.analytical.GranPlotResult(V, gran_function, slope, intercept)[source]#

Bases: object

Result of a gran_plot() call.

Parameters:
property Ka: float#

The acid dissociation constant implied by the slope, -slope.

Type:

float

V: ndarray#

Titrant volumes used in the fit, in L.

Type:

ndarray

property equivalence_volume: float#

The extrapolated x-intercept, -intercept / slope: the equivalence volume.

Type:

float

gran_function: ndarray#

The Gran function \(V_b\,10^{-pH}\) at each volume in V.

Type:

ndarray

intercept: float#

Least-squares intercept of the Gran function vs. V.

Type:

float

slope: float#

Least-squares slope of the Gran function vs. V (equal to \(-K_a\)).

Type:

float

class chemistrykit.analytical.GrubbsTestResult(suspect_value, G_statistic, G_critical, reject)[source]#

Bases: object

Result of a grubbs_test() call.

Parameters:
G_critical: float#

The critical value from grubbs_critical_value().

Type:

float

G_statistic: float#

\(G=|x_{suspect}-\bar x|/s\).

Type:

float

reject: bool#

Whether the suspect value is rejected as an outlier (G_statistic > G_critical).

Type:

bool

suspect_value: float#

The data point tested (the one farthest from the mean).

Type:

float

class chemistrykit.analytical.LinearCalibration(slope, intercept, r_squared, residual_std_error)[source]#

Bases: object

A fitted instrument-response-vs-concentration calibration curve, signal = slope*conc + intercept.

Parameters:
intercept: float#

Signal at zero concentration (ideally the blank signal).

Type:

float

lod()[source]#

Limit of detection, \(\text{LOD}=3.3\,s_{y/x}/|m|\) (ICH Q2 convention).

Return type:

float

Returns:

float

loq()[source]#

Limit of quantitation, \(\text{LOQ}=10\,s_{y/x}/|m|\) (ICH Q2 convention).

Return type:

float

Returns:

float

predict_concentration(signal)[source]#

Invert the calibration to estimate concentration from a measured signal.

\[\hat c = (y_{measured} - b)/m\]
Parameters:

signal (float or array-like of float)

Returns:

float or ndarray

predict_signal(concentration)[source]#

Predict the instrument signal at given concentration(s).

Parameters:

concentration (float or array-like of float)

Returns:

float or ndarray

r_squared: float#

Coefficient of determination of the fit.

Type:

float

residual_std_error: float#

\(s_{y/x}\), the residual standard error about the fit (Harris, Quantitative Chemical Analysis, 9th ed., Ch. 4.5).

Type:

float

slope: float#

Sensitivity, signal units per concentration unit.

Type:

float

class chemistrykit.analytical.QTestResult(suspect_value, Q_statistic, Q_critical, reject)[source]#

Bases: object

Result of a dixon_q_test() call.

Parameters:
Q_critical: float#

The critical value looked up from Q_CRITICAL_TABLE.

Type:

float

Q_statistic: float#

The computed Q statistic, gap/range.

Type:

float

reject: bool#

Whether the suspect value is rejected as an outlier (Q_statistic > Q_critical).

Type:

bool

suspect_value: float#

The data point tested (the minimum or maximum of the set).

Type:

float

class chemistrykit.analytical.RedoxTitration(E1_standard, n1, E2_standard, n2, C_analyte, V_analyte, C_titrant, T=298.15)[source]#

Bases: TitrationCurve

A potentiometric redox titration: a reduced analyte titrated by an oxidizing titrant.

Models the general reaction \(n_2\,\text{Red}_1 + n_1\,\text{Ox}_2 \rightarrow n_2\,\text{Ox}_1 + n_1\,\text{Red}_2\) in the standard large-equilibrium-constant (“quantitative reaction”) approximation (Harris, Quantitative Chemical Analysis, 9th ed., Ch. 16), under which electron-balance stoichiometry alone (not a numerical equilibrium solve) fixes the composition at every titrant volume:

  • Before the equivalence volume \(V_{eq}=n_1C_{analyte}V_{analyte}/(n_2C_{titrant})\), the analyte couple sets the potential via the Nernst equation, using the fraction of analyte already oxidized (fixed by the moles of titrant added, via the electron balance \(n_1x=n_2C_{titrant}V\)):

    \[E = E^\circ_1 - \frac{RT}{n_1F}\ln\frac{[\text{Red}_1]}{[\text{Ox}_1]}\]
  • At the equivalence point, the classical weighted-average result (from adding the two Nernst equations):

    \[E_{eq} = \frac{n_1E^\circ_1+n_2E^\circ_2}{n_1+n_2}\]
  • Past the equivalence volume, the titrant couple sets the potential from the (now excess) unreacted titrant:

    \[E = E^\circ_2 - \frac{RT}{n_2F}\ln\frac{[\text{Red}_2]}{[\text{Ox}_2]}\]

For the common case \(n_1=n_2\) (e.g. Fe2+/Ce4+, both 1-electron couples) this reduces to the textbook symmetric titration curve with \(E_{eq}=(E^\circ_1+E^\circ_2)/2\).

Parameters:
  • E1_standard (float) – Standard reduction potential of the analyte couple (Ox1/Red1), in V.

  • n1 (int) – Electrons transferred in the analyte half-reaction.

  • E2_standard (float) – Standard reduction potential of the titrant couple (Ox2/Red2), in V.

  • n2 (int) – Electrons transferred in the titrant half-reaction.

  • C_analyte (float) – Initial analyte (Red1) concentration, in mol/L.

  • V_analyte (float) – Initial analyte volume, in L.

  • C_titrant (float) – Titrant (Ox2) concentration, in mol/L.

  • T (float) – Absolute temperature, in K.

Examples

Fe2+ (E=0.771 V) titrated by Ce4+ (E=1.72 V), both 1-electron couples: at the equivalence volume, E is exactly the arithmetic mean of the two standard potentials – the textbook symmetric-curve result:

>>> titration = RedoxTitration(E1_standard=0.771, n1=1, E2_standard=1.72, n2=1, C_analyte=0.10, V_analyte=0.050, C_titrant=0.10)
>>> V_eq = titration.equivalence_volume()
>>> round(float(titration.response_at(np.array([V_eq]))[0]), 4) == round((0.771 + 1.72) / 2.0, 4)
True
equivalence_volume()[source]#

Return the exact (stoichiometric, electron-balance) equivalence volume.

Return type:

float

Returns:

float

response_at(V)[source]#

Return the titration’s response variable at each titrant volume in V.

Parameters:

V (ndarray) – Volume(s) of titrant added, in L.

Returns:

ndarray

class chemistrykit.analytical.TitrationCurve[source]#

Bases: ABC

Common base for a potentiometric titration-curve model whose response is not pH.

Concrete subclasses implement response_at(); curve() and find_equivalence_point() are then available for free, mirroring chemistrykit.solutions.core.base_system.Titration.curve()/ find_equivalence_point().

curve(V)[source]#

Compute the full titration curve over a range of titrant volumes.

Parameters:

V (array-like of float) – Volumes of titrant added, in L.

Return type:

TitrationCurveResult

Returns:

TitrationCurveResult

find_equivalence_point(V)[source]#

Numerically locate the equivalence point as the point of steepest response change.

Parameters:

V (array-like of float) – A sufficiently fine grid of titrant volumes spanning the equivalence point, in L.

Return type:

float

Returns:

float

abstractmethod response_at(V)[source]#

Return the titration’s response variable at each titrant volume in V.

Parameters:

V (ndarray) – Volume(s) of titrant added, in L.

Return type:

ndarray

Returns:

ndarray

class chemistrykit.analytical.TitrationCurveResult(V, response)[source]#

Bases: object

Container for the output of a TitrationCurve.curve() call.

Parameters:
V: ndarray#

Volume(s) of titrant added, in L (or any consistent volume unit).

Type:

ndarray

response: ndarray#

The titration’s response variable at each volume in V (e.g. electrode potential E in volts, or pM).

Type:

ndarray

chemistrykit.analytical.dixon_q_test(data, confidence=0.95, suspect='auto')[source]#

Test whether the smallest or largest value in a small data set is an outlier.

\[Q = \frac{\text{gap}}{\text{range}}\]

where “gap” is the absolute difference between the suspect value and its nearest neighbor, and “range” is the difference between the largest and smallest values in the set (Harris, Quantitative Chemical Analysis, 9th ed., Ch. 4.6). The suspect value is rejected as an outlier if \(Q>Q_{crit}\) for the sample size and confidence level chosen.

Parameters:
  • data (array-like of float) – The data set, 3 to 10 points.

  • confidence (float) – Confidence level for the critical value.

  • suspect (str) – Which point to test. "auto" tests whichever of the minimum or maximum is farther (in absolute value) from the sample mean.

Return type:

QTestResult

Returns:

QTestResult

Raises:

ValueError – If data has fewer than 3 or more than 10 points, or confidence is not one of the tabulated levels.

Examples

A clear outlier in a small data set is correctly rejected at 95% confidence:

>>> result = dixon_q_test([10.0, 10.1, 10.2, 10.3, 15.0], confidence=0.95)
>>> result.suspect_value
15.0
>>> result.reject
True

A borderline value that is not extreme enough is retained:

>>> result = dixon_q_test([10.0, 10.1, 10.2, 10.3, 10.9], confidence=0.99)
>>> result.reject
False
chemistrykit.analytical.fit_calibration(concentration, signal)[source]#

Fit a linear calibration curve (signal vs. concentration) by ordinary least squares.

Parameters:
  • concentration (array-like of float) – Known standard concentrations (at least 3 distinct values, so the residual standard error is defined).

  • signal (array-like of float) – Corresponding measured instrument signals.

Return type:

LinearCalibration

Returns:

LinearCalibration

Examples

LOQ is always exactly \(10/3.3\) times LOD, by definition, regardless of the data:

>>> conc = [0.0, 1.0, 2.0, 3.0, 4.0, 5.0]
>>> signal = [0.02, 1.05, 1.98, 3.10, 3.95, 5.08]
>>> cal = fit_calibration(conc, signal)
>>> round(cal.loq() / cal.lod(), 6) == round(10.0 / 3.3, 6)
True

A perfect (noiseless) calibration has zero residual error and hence zero LOD/LOQ:

>>> cal_perfect = fit_calibration([0.0, 1.0, 2.0, 3.0], [1.0, 3.0, 5.0, 7.0])
>>> cal_perfect.lod() < 1e-9
True
chemistrykit.analytical.gran_plot(V, pH)[source]#

Gran plot for a weak acid titrated by a strong base: locate the equivalence point by linear extrapolation.

Before the equivalence volume \(V_e\), the buffer region obeys \([H^+]=K_a\,n_{HA}/n_{A^-}=K_a(V_e-V_b)/V_b\), so the Gran function

\[V_b\,10^{-pH} = K_a\,(V_e - V_b)\]

is a straight line in \(V_b\) whose x-intercept is \(V_e\) and whose slope is \(-K_a\) (G. Gran, Analyst 77, 661 (1952)). This turns the equivalence point into a linear extrapolation of data taken well before it, rather than a search for the steepest point of the sigmoidal curve (activity coefficients are neglected here).

Parameters:
  • V (array-like of float) – Titrant volumes in the buffer region (before the equivalence point), in L.

  • pH (array-like of float) – Measured pH at each volume.

Return type:

GranPlotResult

Returns:

GranPlotResult

Examples

Ideal buffer-region data built from the Gran relation itself recover \(V_e\) and \(K_a\) exactly:

>>> import numpy as np
>>> Ka, Ve = 1.8e-5, 0.050
>>> V = np.linspace(0.010, 0.045, 8)
>>> pH = -np.log10(Ka * (Ve - V) / V)
>>> result = gran_plot(V, pH)
>>> round(result.equivalence_volume, 9), round(result.Ka / Ka, 9)
(0.05, 1.0)
chemistrykit.analytical.grubbs_critical_value(n, alpha=0.05)[source]#

Two-sided critical value of Grubbs’ statistic for a sample of size n.

\[G_{crit} = \frac{n-1}{\sqrt n}\sqrt{\frac{t^2}{n-2+t^2}}\]

with t the upper \(\alpha/(2n)\) critical value of Student’s distribution with \(n-2\) degrees of freedom (F. E. Grubbs, Technometrics 11, 1 (1969)).

Parameters:
  • n (int) – Sample size (at least 3).

  • alpha (float) – Significance level.

Return type:

float

Returns:

float

Examples

Matches the tabulated two-sided 5% value for ten observations:

>>> round(grubbs_critical_value(10, alpha=0.05), 3)
2.29
chemistrykit.analytical.grubbs_test(data, alpha=0.05)[source]#

Grubbs’ test for a single outlier: the most extreme value, in units of the sample standard deviation.

\[G = \frac{\max_i|x_i-\bar x|}{s}\]

compared against grubbs_critical_value() (F. E. Grubbs, Ann. Math. Statist. 21, 27 (1950)). Unlike Dixon’s gap-over-range dixon_q_test(), G uses every point through the mean and standard deviation, and its critical value comes from a formula rather than a finite table, so any n >= 3 works.

Parameters:
  • data (array-like of float) – The data set, at least 3 points.

  • alpha (float) – Significance level of the two-sided test.

Return type:

GrubbsTestResult

Returns:

GrubbsTestResult

Examples

>>> result = grubbs_test([10.0, 10.1, 10.2, 10.3, 15.0])
>>> result.suspect_value, result.reject
(15.0, True)
>>> grubbs_test([10.0, 10.1, 10.2, 10.3, 10.4]).reject
False
chemistrykit.analytical.horrat(observed_rsd, mass_fraction)[source]#

The HorRat ratio: an observed between-laboratory RSD divided by horwitz_rsd().

Values roughly between 0.5 and 2 are conventionally taken to indicate a method with normal, acceptable interlaboratory precision.

Parameters:
  • observed_rsd (float or array-like of float) – Observed reproducibility RSD, in percent.

  • mass_fraction (float or array-like of float) – Analyte concentration as a mass fraction.

Returns:

float or ndarray

Examples

>>> round(float(horrat(8.0, 1e-6)), 6)
0.5
chemistrykit.analytical.horwitz_rsd(mass_fraction)[source]#

Horwitz’s predicted between-laboratory relative standard deviation, in percent.

\[\text{RSD}_R(\%) = 2^{\,1-0.5\log_{10}C} = 2\,C^{-0.1505}\]

with C the analyte concentration as a dimensionless mass fraction (1 for a pure substance, \(10^{-6}\) for 1 ppm) – the empirical “Horwitz trumpet” fitted to thousands of collaborative-study results (W. Horwitz, L. R. Kamps, K. W. Boyer, J. Assoc. Off. Anal. Chem. 63, 1344 (1980)): each 100-fold drop in concentration doubles the expected RSD.

Parameters:

mass_fraction (float or array-like of float) – Analyte concentration as a mass fraction, in (0, 1].

Returns:

float or ndarray

Examples

2% for a pure substance, 16% at 1 ppm:

>>> round(float(horwitz_rsd(1.0)), 6), round(float(horwitz_rsd(1e-6)), 6)
(2.0, 16.0)
chemistrykit.analytical.kovats_retention_index(t_x, t_n, t_N, n, N=None, dead_time=0.0)[source]#

Kováts retention index of an analyte, bracketed by two n-alkane standards.

Under isothermal conditions the logarithm of an n-alkane’s adjusted retention time \(t'=t_R-t_0\) grows linearly with its carbon number, so an analyte eluting between the n-alkanes with n and N carbons is assigned the logarithmically interpolated index (E. Kováts, Helv. Chim. Acta 41, 1915 (1958)):

\[I = 100\left[n + (N-n)\,\frac{\log t'_x-\log t'_n}{\log t'_N-\log t'_n}\right]\]

With the usual choice \(N=n+1\) this is Kováts’ original definition; every n-alkane itself has, by construction, \(I=100n\).

Parameters:
  • t_x (float or array-like of float) – Retention time(s) of the analyte.

  • t_n (float) – Retention times of the smaller (n carbons) and larger (N carbons) bracketing n-alkanes.

  • t_N (float) – Retention times of the smaller (n carbons) and larger (N carbons) bracketing n-alkanes.

  • n (int) – Carbon number of the earlier-eluting n-alkane.

  • N (int | None) – Carbon number of the later-eluting n-alkane; defaults to n + 1.

  • dead_time (float) – Column dead time \(t_0\), subtracted from every retention time to form the adjusted retention times \(t'\).

Returns:

float or ndarray

Examples

An analyte eluting at the geometric mean of the adjusted retention times of n-octane and n-nonane sits exactly halfway, at I = 850:

>>> import numpy as np
>>> t0, t8, t9 = 1.0, 5.0, 9.0
>>> t_x = t0 + np.sqrt((t8 - t0) * (t9 - t0))
>>> round(float(kovats_retention_index(t_x, t8, t9, n=8, dead_time=t0)), 6)
850.0
chemistrykit.analytical.minimum_plate_height(A, B, C)[source]#

The minimum plate height \(H_{min}=A+2\sqrt{BC}\), at the optimum flow velocity.

Substituting optimum_flow_velocity() into van_deemter_H() (Harris, Quantitative Chemical Analysis, 9th ed., Ch. 23.4).

Parameters:
Return type:

float

Returns:

float

Examples

Matches direct evaluation of van_deemter_H() at the optimum velocity from optimum_flow_velocity():

>>> A, B, C = 1.0, 2.0, 0.05
>>> u_opt = optimum_flow_velocity(B, C)
>>> bool(round(minimum_plate_height(A, B, C), 6) == round(float(van_deemter_H(u_opt, A, B, C)), 6))
True
chemistrykit.analytical.optimum_flow_velocity(B, C)[source]#

The flow velocity \(u_{opt}=\sqrt{B/C}\) minimizing the van Deemter equation.

Found by \(dH/du=-B/u^2+C=0\) (Harris, Quantitative Chemical Analysis, 9th ed., Ch. 23.4).

Parameters:
  • B (float) – Van Deemter longitudinal-diffusion and mass-transfer coefficients.

  • C (float) – Van Deemter longitudinal-diffusion and mass-transfer coefficients.

Return type:

float

Returns:

float

Examples

>>> round(optimum_flow_velocity(B=2.0, C=0.05), 6)
6.324555
chemistrykit.analytical.plate_height(column_length, N)[source]#

Plate height (HETP) \(H=L/N\), the column length per theoretical plate.

Parameters:
  • column_length (float) – Column length L, e.g. in cm.

  • N (float or array-like of float) – Number of theoretical plates.

Returns:

float or ndarray

Examples

>>> round(float(plate_height(column_length=25.0, N=6400.0)), 6)
0.003906
chemistrykit.analytical.propagate_power(value, sigma, exponent)[source]#

Absolute uncertainty of \(y=x^n\), \(\sigma_y=|n|\,|x|^{n-1}\sigma_x\).

Equivalently \(\sigma_y/|y|=|n|\,\sigma_x/|x|\) (Harris, Quantitative Chemical Analysis, 9th ed., Ch. 3.2).

Parameters:
  • value (float) – The base x.

  • sigma (float) – Absolute uncertainty of x.

  • exponent (float) – The exponent n (need not be an integer).

Return type:

float

Returns:

float

Examples

\(y=x^2\) with \(x=3.0\pm0.1\): relative uncertainty doubles:

>>> round(propagate_power(3.0, 0.1, 2.0), 6)
0.6
chemistrykit.analytical.propagate_product(values, sigmas)[source]#

Absolute uncertainty of a product or quotient of independent quantities.

Relative uncertainties add in quadrature (Harris, Quantitative Chemical Analysis, 9th ed., Ch. 3.2):

\[\frac{\sigma_y}{|y|}=\sqrt{\sum_i\left(\frac{\sigma_i}{x_i}\right)^2}, \qquad y=\prod_i x_i^{\pm1}\]
Parameters:
  • values (array-like of float) – The value of each factor (as it appears in the product/quotient, e.g. use 1/x for a quantity in the denominator – see the Examples).

  • sigmas (array-like of float) – Absolute uncertainty of each factor, same order as values.

Return type:

float

Returns:

float – Absolute uncertainty of the product \(y=\prod_i\) values[i].

Examples

\(y=a\times b\) with \(a=2.0\pm0.1\), \(b=3.0\pm0.2\):

>>> y = 2.0 * 3.0
>>> sigma_y = propagate_product([2.0, 3.0], [0.1, 0.2])
>>> round(sigma_y, 6)
0.5

A quotient \(y=a/b\) is a product with b replaced by \(1/b\) (relative uncertainty of \(1/b\) equals that of b):

>>> a, sigma_a = 10.0, 0.5
>>> b, sigma_b = 4.0, 0.2
>>> y = a / b
>>> sigma_y = propagate_product([a, 1.0 / b], [sigma_a, sigma_b / b**2])
>>> round(sigma_y, 6)
0.176777
chemistrykit.analytical.propagate_sum(sigmas)[source]#

Absolute uncertainty of a sum or difference of independent quantities, \(\sigma=\sqrt{\sum_i\sigma_i^2}\).

Applies identically whether the terms are added or subtracted (Harris, Quantitative Chemical Analysis, 9th ed., Ch. 3.2) – only the absolute uncertainties of each term matter, not their signs.

Parameters:

sigmas (array-like of float) – Absolute uncertainty of each term.

Return type:

float

Returns:

float

Examples

Two equal uncertainties combine to \(\sigma\sqrt2\), not \(2\sigma\):

>>> round(propagate_sum([0.02, 0.02]), 6)
0.028284
chemistrykit.analytical.propagate_uncertainty(func, values, sigmas, h=1e-06)[source]#

General first-order uncertainty propagation for an arbitrary differentiable function.

Evaluates \(\sigma_y=\sqrt{\sum_i(\partial f/\partial x_i)^2\sigma_i^2}\) (Harris, Quantitative Chemical Analysis, 9th ed., Ch. 3.2, eq. 3.2) with each partial derivative estimated by a central finite difference – useful for a function with no simple closed-form propagation rule (e.g. a pH from a Nernst equation, or a rate constant from an Arrhenius fit), at the cost of losing the exact closed-form results propagate_sum()/propagate_product()/ propagate_power() give for their specific operations.

Parameters:
  • func (callable) – func(*values) -> float.

  • values (array-like of float) – The value of each independent variable, in the order func expects.

  • sigmas (array-like of float) – Absolute uncertainty of each variable, same order as values.

  • h (float) – Relative step size for the central finite difference.

Return type:

float

Returns:

float

Examples

Reproduces propagate_product() for a simple product, since both implement the same general formula:

>>> from chemistrykit.analytical.systems.uncertainty import propagate_product
>>> sigma_general = propagate_uncertainty(lambda a, b: a * b, [2.0, 3.0], [0.1, 0.2])
>>> sigma_closed_form = propagate_product([2.0, 3.0], [0.1, 0.2])
>>> bool(abs(sigma_general - sigma_closed_form) < 1e-6)
True
chemistrykit.analytical.purnell_resolution(N, alpha, k2)[source]#

Purnell’s resolution equation: resolution in terms of efficiency, selectivity, and retention.

\[R_s = \frac{\sqrt N}{4}\,\frac{\alpha-1}{\alpha}\,\frac{k_2}{1+k_2}\]

(J. H. Purnell, J. Chem. Soc. 1960, 1268), with N the plate count, \(\alpha=k_2/k_1\) the selectivity factor, and \(k_2\) the retention factor of the later-eluting peak. It is exact when both peaks have the base width \(4t_{R,2}/\sqrt N\) of the second peak, and separates the three independent levers a chromatographer can pull: efficiency (\(\sqrt N\)), selectivity, and retention.

Parameters:
  • N (float or array-like of float) – Number of theoretical plates.

  • alpha (float) – Selectivity factor \(\alpha\ge1\).

  • k2 (float) – Retention factor of the later-eluting peak.

Returns:

float or ndarray

Examples

Agrees with the direct resolution() formula for two peaks of equal base width \(4t_{R,2}/\sqrt N\):

>>> import numpy as np
>>> N, t0, k1, k2 = 10000.0, 1.0, 4.0, 4.4
>>> tR1, tR2 = t0 * (1 + k1), t0 * (1 + k2)
>>> w = 4.0 * tR2 / np.sqrt(N)
>>> bool(abs(purnell_resolution(N, alpha=k2 / k1, k2=k2) - resolution(tR1, tR2, w, w)) < 1e-12)
True

Resolution grows only as \(\sqrt N\): quadrupling the plate count doubles it.

>>> round(float(purnell_resolution(40000.0, 1.1, 4.4) / purnell_resolution(10000.0, 1.1, 4.4)), 6)
2.0
chemistrykit.analytical.resolution(tR1, tR2, w1, w2)[source]#

Chromatographic resolution \(R_s=2(t_{R,2}-t_{R,1})/(w_1+w_2)\) between two adjacent peaks.

\(R_s\ge1.5\) is the conventional criterion for baseline separation (Harris, Quantitative Chemical Analysis, 9th ed., eq. 23.21).

Parameters:
  • tR1 (float) – Retention times of the earlier- and later-eluting peaks.

  • tR2 (float) – Retention times of the earlier- and later-eluting peaks.

  • w1 (float) – Base widths of the two peaks, same time units.

  • w2 (float) – Base widths of the two peaks, same time units.

Return type:

float

Returns:

float

Examples

>>> round(resolution(tR1=9.0, tR2=10.0, w1=0.5, w2=0.5), 6)
2.0
chemistrykit.analytical.retention_factor(retention_time, dead_time)[source]#

The retention (capacity) factor \(k=(t_R-t_0)/t_0\).

Parameters:
  • retention_time (float) – Analyte retention time \(t_R\).

  • dead_time (float) – Column dead time (retention time of an unretained species) \(t_0\).

Returns:

float or ndarray

Examples

>>> round(float(retention_factor(retention_time=12.0, dead_time=2.0)), 6)
5.0
chemistrykit.analytical.savitzky_golay(y, window_length, polyorder, deriv=0, delta=1.0)[source]#

Smooth (or differentiate) a signal with a Savitzky-Golay filter.

Interior points use the fixed convolution weights of savitzky_golay_coefficients(); the first and last m points, which have no full centered window, are taken from the polynomial fitted to the first and last full window (the same edge treatment as scipy.signal.savgol_filter(..., mode="interp")).

Parameters:
Return type:

ndarray

Returns:

ndarray, same shape as y

Examples

A filter of polynomial order p passes any polynomial of degree <= p through unchanged, edges included:

>>> import numpy as np
>>> x = np.linspace(0.0, 1.0, 21)
>>> y = 3.0 * x**2 - x + 0.5
>>> bool(np.allclose(savitzky_golay(y, 7, 2), y))
True
chemistrykit.analytical.savitzky_golay_coefficients(window_length, polyorder, deriv=0, delta=1.0)[source]#

Convolution weights of a Savitzky-Golay filter.

With window offsets \(z=-m,\ldots,m\) and the design matrix \(J_{ij}=z_i^{\,j}\) (\(j=0,\ldots,p\)), the least-squares polynomial coefficients are \((J^TJ)^{-1}J^T y\); row d of that matrix, times \(d!/\Delta^d\), gives the weights for the d-th derivative at the window center.

Parameters:
  • window_length (int) – Odd number of points in each window, \(2m+1\).

  • polyorder (int) – Degree p of the local polynomial, less than window_length.

  • deriv (int) – Derivative order (0 = smoothing).

  • delta (float) – Sample spacing, used to scale derivatives.

Return type:

ndarray

Returns:

ndarray, shape (window_length,) – Weights \(c_i\) such that the filtered value at point k is \(\sum_i c_i\,y_{k+i}\), i running from -m to m.

Examples

The classic 5-point quadratic smoothing weights from Savitzky and Golay’s Table I, \((-3, 12, 17, 12, -3)/35\):

>>> import numpy as np
>>> np.round(savitzky_golay_coefficients(5, 2) * 35, 10)
array([-3., 12., 17., 12., -3.])
chemistrykit.analytical.selectivity_factor(k1, k2)[source]#

The selectivity (relative retention) factor \(\alpha=k_2/k_1\) (\(k_2\ge k_1\) by convention).

Parameters:
  • k1 (float) – Retention factors of the earlier- and later-eluting peaks.

  • k2 (float) – Retention factors of the earlier- and later-eluting peaks.

Return type:

float

Returns:

float

Examples

>>> round(selectivity_factor(k1=2.0, k2=5.0), 6)
2.5
chemistrykit.analytical.simulate_chromatogram(t, centers, retention_times=None, N=10000.0, amplitudes=None)[source]#

Simulate a chromatogram as a sum of Gaussian elution peaks of plate-count-consistent width.

Reuses chemistrykit.spectro.utils.lineshapes.gaussian() (each elution peak is, to a good approximation, Gaussian – the same plate-theory result that underlies theoretical_plates()), with each peak’s FWHM set by \(w_{1/2}=t_R\sqrt{8\ln2/N}\) (inverting theoretical_plates()’s half-height form, \(N=5.545(t_R/w_{1/2})^2\) with \(5.545\approx8\ln2\)).

Parameters:
  • t (array-like of float) – Time grid to evaluate the chromatogram on.

  • centers (array-like of float) – Retention time of each peak (alias for retention_times, kept for a natural call signature with chemistrykit.spectro.utils.lineshapes.broaden_stick_spectrum()).

  • retention_times (array-like of float, optional) – If given, overrides centers.

  • N (float) – Number of theoretical plates (assumed equal for every peak, the common simplifying assumption for a single column/method).

  • amplitudes (array-like of float, optional) – Relative peak heights; defaults to 1.0 for every peak.

Returns:

ndarray, shape matching t

Examples

A single simulated peak’s apparent plate count (recovered from its numerically-measured FWHM) matches the N it was built from:

>>> import numpy as np
>>> t = np.linspace(8.0, 12.0, 200001)
>>> N_true = 10000.0
>>> chrom = simulate_chromatogram(t, centers=[10.0], N=N_true)
>>> half_max = chrom.max() / 2.0
>>> above = t[chrom >= half_max]
>>> fwhm = above.max() - above.min()
>>> N_recovered = theoretical_plates(10.0, fwhm, width_type="half_height")
>>> bool(abs(N_recovered - N_true) / N_true < 0.01)
True
chemistrykit.analytical.t_confidence_interval(data, confidence=0.95)[source]#

Student’s t confidence interval for the true mean of a few replicate measurements.

\[\mu = \bar x \pm \frac{t\,s}{\sqrt n}\]

with s the sample standard deviation and t the two-sided critical value of Student’s distribution with \(n-1\) degrees of freedom (“Student” [W. S. Gosset], Biometrika 6, 1 (1908)). For small n, t is much larger than the normal-distribution value 1.96, widening the interval to account for s itself being an uncertain estimate of the true standard deviation.

Parameters:
  • data (array-like of float) – Replicate measurements (at least 2).

  • confidence (float) – Two-sided confidence level.

Return type:

ConfidenceIntervalResult

Returns:

ConfidenceIntervalResult

Examples

For five replicates, the 95% two-sided t value is the tabulated 2.776:

>>> ci = t_confidence_interval([10.1, 10.3, 9.9, 10.2, 10.0])
>>> round(ci.t_critical, 3), round(ci.mean, 6)
(2.776, 10.1)
>>> round(ci.half_width, 4)
0.1963
chemistrykit.analytical.theoretical_plates(retention_time, peak_width, width_type='base')[source]#

Number of theoretical plates N from a peak’s retention time and width.

For a Gaussian elution peak (Harris, Quantitative Chemical Analysis, 9th ed., eq. 23.19-23.20):

\[N = 16\left(\frac{t_R}{w_{base}}\right)^2 = 5.545\left(\frac{t_R}{w_{1/2}}\right)^2\]

where \(w_{base}\) is the width at the peak base (tangents to the inflection points) and \(w_{1/2}\) is the full width at half maximum – the numeric prefactor differs only because a Gaussian’s base width (4 standard deviations) and FWHM (\(2\sqrt{2\ln2}\,\sigma\)) are different multiples of sigma.

Parameters:
  • retention_time (float) – Peak retention time \(t_R\).

  • peak_width (float) – Peak width, in the same units as retention_time, of the type selected by width_type.

  • width_type (str)

Returns:

float or ndarray

Examples

A peak eluting at 10.0 min with a 0.5 min base width:

>>> round(float(theoretical_plates(10.0, 0.5, width_type="base")), 1)
6400.0

The two width conventions must agree for a Gaussian peak of consistent shape (base width = 4 sigma, FWHM = \(2\sqrt{2\ln2}\,\sigma\approx2.3548\sigma\)):

>>> sigma = 0.125
>>> N_base = theoretical_plates(10.0, 4.0 * sigma, width_type="base")
>>> N_half = theoretical_plates(10.0, 2.3548 * sigma, width_type="half_height")
>>> bool(abs(N_base - N_half) / N_base < 1e-3)
True
chemistrykit.analytical.van_deemter_H(u, A, B, C)[source]#

The van Deemter equation: plate height H as a function of mobile-phase linear velocity u.

\[H = A + \frac{B}{u} + Cu\]

with A the eddy-diffusion term (velocity-independent band broadening from unequal flow paths), B the longitudinal-molecular- diffusion term (dominant at low u), and C the mass-transfer- resistance term (dominant at high u) (J. J. van Deemter, F. J. Zuiderweg, A. Klinkenberg, Chem. Eng. Sci. 5, 271 (1956); Harris, Quantitative Chemical Analysis, 9th ed., Ch. 23.4).

Parameters:
  • u (float or array-like of float) – Mobile-phase linear velocity.

  • A (float) – Van Deemter coefficients (all non-negative for a physical column).

  • B (float) – Van Deemter coefficients (all non-negative for a physical column).

  • C (float) – Van Deemter coefficients (all non-negative for a physical column).

Returns:

float or ndarray

Examples

>>> round(float(van_deemter_H(u=2.0, A=1.0, B=2.0, C=0.05)), 4)
2.1