Chemical Kineticsï
kinetics.py module
This module contains two classes and one standalone function:
kbasicâ a minimal, self-contained, pedagogical tool: fit a single rate constant \(k\) for one chosen reaction order. See 3.kbasic.KORD(Kinetic Order of Reaction Determination) â the full tool: fit \(k\), \(Y_0\), \(Y_\infty\) (and \(b_\infty\)) for reaction orders 0, 1 and 2, and decide which order the data actually support. See 4.KORD.superpose_plots()â the moduleâs only standalone function (not a method of either class): overlays the fitted curves of severalKORDstudies on one plot, e.g. to compare the same data fitted freely vs. withfix_Y0/fix_Yinf. See 4.4.1 Comparing several fits.
Table of Contentsï
1. Finding a Mathematical Model That Fits the Dataï
Curve fitting, in general, is the search for the free parameters â generically \(a, b, c, \ldots\) (not to be confused with this moduleâs own \(a_0\), \(\alpha\), \(\beta\)) â of a mathematical model \(Y_\mathrm{THEO}(t;\, a, b, c, \ldots)\) that make it track the measured data \(Y_\mathrm{EXP}(t)\) as closely as possible. âAs closely as possibleâ needs a single number to minimize, and that number is the Root-Mean-Square Deviation between the measured values and the model evaluated at its best-fit parameters, \(\hat{Y}_\mathrm{THEO}(t)\) (the hat marks a prediction, exactly as in the usual machine-learning notation), over the experimental time points \(t_k\):
(You may see this quantity called RMSD â Root Mean Square Deviation â
elsewhere, e.g. in crystallography or molecular dynamics; RMSE â Root Mean
Square Error, the name used throughout this module and by
scikit-learnâs root_mean_squared_error â is the same formula, just the name
preferred in statistics/ML.)
Both kbasic.fit() and KORD.fit() work exactly this way: they search â via
scipy.optimize.minimize (kbasic) or scipy.optimize.curve_fit (KORD) â
for the parameter values that make the RMSE as small as possible.
The RMSE should tend toward 0 as the fit improves, and it will, provided
the chosen mathematical model is the right one for the data: a correct
model, once its parameters are optimized, reproduces the measurements almost
exactly. A wrong model (the wrong reaction order, say) cannot be pushed to a
near-zero RMSE no matter how its parameters are tuned, simply because its
shape doesnât match the dataâs. KORD builds on this further â see
4.2.4 Model Selection â to compare reaction orders
against each other, not just to fit one orderâs parameters.
2. Integrated rate laws: \(a(t)\), \(b(t)\) and half-lives for orders 0, 1, 2ï
Consider a single-reactant, single-product transformation \(\alpha A \rightarrow \beta B\), under the usual simplifying assumptions used throughout this module:
Single-component reaction: a unique reactant \(A\) transforms into a unique product \(B\).
Total reaction: the reaction goes to completion (the extent of reaction is 100%).
Closed system: no exchange of matter occurs between the system and its environment; only energy exchanges are possible.
Homogeneous system: the concentration of any compound \(C_i\) is uniform throughout the entire system.
Isochoric system: the volume of the system remains constant throughout the reaction.
The rate law is defined as:
This section gives the integrated form \(a(t)=[A](t)\), the corresponding
product expression \(b(t)=[B](t)\), and the resulting half-life, for
\(n \in \{0,1,2\}\). kbasic (section 3) and KORD (section 4) both build on
these â kbasic as the special case \(\alpha=1\), \(Y_\infty=0\) (see
3.4 Theory), KORD directly (see
4.3 Theoretical Model).
2.1 Reactant Expression \(a(t)\)ï
Order 0:
Order 1:
Order 2:
2.2 Product Expression \(b(t)\)ï
Derived from mass balance (\(M_{A} a(t) + M_{B} b(t) = M_{A} a_{0} = M_{B} b_{\infty}\)):
Order 0:
Order 1:
Order 2:
2.3 Half-life \(t_{1/2}\)ï
The half-life is the time at which \(a(t)\) has fallen to half its initial value, \(a(t_{1/2}) = a_{0}/2\). Inverting the \(a(t)\) expressions above gives, for each order:
Order 0: \(t_{1/2} = \dfrac{a_{0}}{2\alpha k}\) â depends on \(a_0\).
Order 1: \(t_{1/2} = \dfrac{\ln 2}{\alpha k}\) â independent of \(a_0\).
Order 2: \(t_{1/2} = \dfrac{1}{\alpha k a_{0}}\) â inversely proportional to \(a_0\).
KORD.halflife(order, binf, k) implements exactly these three formulas
(recovering \(a_0\) from \(b_\infty\), \(\alpha\), \(\beta\) internally), and
fit()/get_best_order() store the result in study.results[order]['t_half'].
3. kbasic: a minimal, pedagogical rate-constant fitï
3.1 Purpose and limitationsï
kbasic is a deliberately minimal teaching tool. Given a single time series
\((t, Y)\), it finds the one rate constant \(k\) that minimizes the RMSE (see
1. Finding a Mathematical Model That Fits the Data above), for a
reaction order you choose yourself (0, 1 or 2). Its point is to make the
fitting procedure itself â build a model, define an RMSE, call
scipy.optimize.minimize â easy to read and follow end to end in a first
course, before moving on to KORDâs richer (and more opaque) model.
To stay that transparent, kbasic makes simplifying assumptions KORD does
not, and these are real limitations, not just simplifications:
No stoichiometry: it assumes \(\alpha = 1\) and reports an apparent \(k\); it has no \(\beta\), \(a_0\) or \(b_\infty\) at all, and cannot separate a true rate constant from the stoichiometric coefficient.
No baseline/plateau: \(Y_\infty\) is implicitly 0 â the three models in
_modeledecay from \(Y_0\) toward 0 (the order-0 case is floored at exactly 0 bynp.maximum). There is no adjustable \(Y_\infty\). Do not usekbasicon data whose measured quantity levels off at a non-zero value â useKORDinstead.A single free parameter: only \(k\) is optimized by
fit(); \(Y_0\) is always taken from the data (first point) or supplied by the user, never fitted âkbasicalready does, permanently, whatKORD.fit(fix_Y0=True)does on request.One order at a time, no model comparison: unlike
KORD.get_best_order(), there is no AIC/BIC-based verdict. You pick the order yourself, andkbasiconly reports the RMSE for that one choice.Order 2 gives an apparent \(k\), not the true rate constant: see 3.4 Theory below for why, and how to recover the true value if you need it.
In short: reach for kbasic for a quick, from-scratch look at a single
order on data that genuinely decays to 0 at large \(t\). Reach for KORD as
soon as \(Y_\infty \ne 0\), stoichiometry matters, or you want the reaction
order itself determined for you rather than assumed.
3.2 Data formatï
kbasic.read() expects a plain two-column spreadsheet: time in the first
column, the measured quantity \(Y\) in the second â nothing else. An optional
header row (text) sets the axis labels for plot(); without one, the
default labels âTempsâ/âYâ are used.
Temps / s |
Abs |
|---|---|
0 |
1.40 |
7.59 |
1.33 |
15.18 |
1.27 |
⊠|
⊠|
There are no parameter rows here (unlike KORDâs format, see
4.2.3 Input below) â kbasic has no \(\alpha\), \(\beta\) or \(a_0\) to
read from the file.
3.3 Quick Startï
from pyphyschemtools import kbasic # lazy-loaded from the package root
kb = kbasic()
kb.read("measurements.xlsx") # 2-column file: time, Y
kb.fit(ordre=1) # try order 1 (exponential decay to 0)
kb.plot() # data vs. model, and the RMSE landscape
fit() estimates its own starting value for \(k\) directly from the data: it
inverts the chosen orderâs \(Y(t)\) equation (see 3.4 Theory
below) at one reference point, rather than always starting from an arbitrary
constant. You can still impose your own starting value with k_depart=...
if needed.
3.4 Theoryï
kbasicâs three models are the special case \(\alpha=1\), \(Y_\infty=0\) of the
general \(a(t)\) expressions in 2.1 above, with \(a_0\) renamed
\(Y_0\):
Order 0: \(Y(t)=\max(Y_{0}-kt,\ 0)\)
Order 1: \(Y(t)=Y_{0}\exp(-kt)\)
Order 2: \(Y(t)=\dfrac{Y_{0}}{1+kt}\)
and correspondingly, the half-life (special case \(\alpha=1\), \(a_0=Y_0\) of 2.3):
Order 0: \(t_{1/2} = \dfrac{Y_{0}}{2k}\)
Order 1: \(t_{1/2} = \dfrac{\ln 2}{k}\)
Order 2: \(t_{1/2} = \dfrac{1}{k}\)
fit() computes and prints this \(t_{1/2}\) alongside \(k\) and the RMSE, and
stores it in self.t_half.
Order 2 caveat. The true integrated second-order rate law is
\(a(t) = a_{0}/(1+a_{0}kt)\) â note the extra \(a_0\) multiplying \(kt\) in the
denominator (consistent with 2.1 for \(\alpha=1\)). kbasicâs
order-2 model, \(Y(t)=Y_{0}/(1+kt)\), omits that factor. As a result, the \(k\)
kbasic reports for order 2 is an apparent constant,
\(k_{\mathrm{app}} = k_{\mathrm{true}} \times Y_{0}\), not the true molecular
rate constant â and it is not comparable between two experiments run at
different \(Y_0\). If you need the true \(k\) for order 2, divide kbasicâs
result by \(Y_0\), or use KORD instead, which tracks \(a_0\) explicitly.
4. KORD: Kinetic Order of Reaction Determinationï
4.1 Quick Startï
4.1.1 Basic useï
from pyphyschemtools import KORD, get_ppct_data
# Load a bundled example file (experiment 1) and build the KORD instance in one call:
file = get_ppct_data("Kinetics/Test_Examples_VP.ods")
study = KORD.from_spreadsheet(file, exp_number=1)
study.get_best_order() # fits orders 0, 1, 2 and reports the best one
study.plot_all_fits() # visual comparison of the three fits
plot_all_fits() fits (or reuses, from study.results) all three orders and
plots them together. An order already cached in study.results from an
earlier fit() call is not re-fitted here, even if the fix_Y0/fix_Yinf
passed to plot_all_fits() differ from the ones that produced that cached
fit â your earlier, explicit choice is kept rather than silently overwritten.
If that creates a mismatch (some orders fitted under different
fix_Y0/fix_Yinf than the others), a warning table is printed, since
comparing orders fitted under different constraints can be misleading. The
same rule, and the same warning, apply to
superpose_plots() below.
4.1.2 Specific fitï
You can also fix \(Y_0\) (or \(Y_\infty\)) at a chosen value instead of letting the optimizer adjust it freely â fully explained in 4.2 Principle below:
# A better starting point than the raw first data point (e.g. because it's
# visibly noisy), used here as a hard constraint rather than a free guess:
study = KORD.from_spreadsheet(file, exp_number=1, Y0_guess=1.40)
study.simulate_plot() # the three guesses (order 0, 1, 2) vs. the data
study.fit(order=1, fix_Y0=True) # Y0 is held fixed at 1.40
# Changing the guess afterwards and fitting order 1 again works the same way,
# without re-creating study:
study.Y0_guess = 1.35
study.fit(order=1, fix_Y0=True) # now fixed at 1.35
study.simulate_plot(order=1) # only the order-1 guess vs. the data
simulate_plot(order=None, show_experimental=True, save_img=None) plots the
current guess curve(s) â before or without fitting â against the measured
data (shown by default; pass show_experimental=False to hide it). Use
order=None (the default) to compare all three ordersâ guesses at once, or
order=0/1/2 to see just one.
Note that fit() itself never plots anything â it only computes and stores
numbers in study.results[order]; plot_all_fits() (above) or
simulate_plot() (just for the initial guesses, before any fit) are what
draw a figure.
4.2 Principleï
Scope and limitations. KORD only models the transformation of a single followed species, \(\alpha A \rightarrow \beta B\), with rate law \(v=k[A]^n\) (\(n \in \{0,1,2\}\)). This covers:
a genuine unimolecular transformation of A (most order-1 cases);
a genuine bimolecular reaction of A with itself, \(2A \rightarrow \text{products}\) (\(n=2\)), since there is still only one concentration, \([A]\), varying;
a mixed bimolecular reaction between two different species, \(A+B\rightarrow\ldots\), only in the pseudo-order approximation: one reactant held in large excess so its concentration stays essentially constant (the âisolation methodâ/Ostwald method, âfloodingâ, or dĂ©gĂ©nĂ©rescence de lâordre in French). The apparent order and \(k\) obtained then describe the minority reactant only.
KORD does not correctly describe a true mixed second-order reaction where both \([A]_0\) and \([B]_0\) are comparable and both vary independently â that needs the (different, more complex) two-species integrated rate law, which this module does not implement.
\(\alpha\) and \(\beta\) are plain stoichiometric coefficients of \(\alpha A \rightarrow \beta B\), not âassociationâ vs. âdissociationâ: \(\alpha=2\) means 2 molecules of A are consumed per reaction event (e.g. \(2A\rightarrow P\)), \(\beta=2\) means each event produces 2 molecules of B (e.g. \(A\rightarrow 2B\), a fragmentation).
4.2.1 Experimental Measurements (\(Y_\mathrm{EXP}\))ï
In chemical kinetics, we track the evolution of molar concentrations over time: \(C_{i}(t)\). Experimentally, we measure a physical quantity proportional to these concentrations: \(Y_\mathrm{EXP}(t)\).
Spectrophotometry: \(Y_\mathrm{EXP} = A = \sum_{i} \epsilon_{i} \cdot l \cdot C_{i}\), where \(A\) is the absorbance
Polarimetry: \(Y_\mathrm{EXP} = \alpha = \sum_{i} [\alpha]_{i} \cdot l \cdot C_{i}^{w}\)
Conductivity: \(Y_\mathrm{EXP} = \sigma = \sum_{i} \lambda_{i} \cdot C_{i}\)
General Form: \(Y_\mathrm{EXP}(t) = \sum_{i} \eta_{i} \cdot C_{i}(t)\)
4.2.2 Theoretical Model (\(Y_\mathrm{THEO}\))ï
The model has three different expressions (see Theoretical model section), depending on the order of the reaction:
Standard Formula for Order 0:
Standard Formula for Order 1:
Standard Formula for Order 2:
\(Y_\mathrm{THEO}\) is defined by two types of values: fixed parameters (input by the user) and adjustable variables (optimized by the algorithm).
Fixed Parameters:
Reaction Order: \(n \in \{0, 1, 2\}\) (The user selects the order to test).
Stoichiometry: \(\alpha\) and \(\beta\) are known constants, provided by the user.
\(\alpha\) and \(\beta\) must be the smallest possible positive integers
Initial Concentration: \(a_{0}\) (Note: For Order 1, \(\hat{Y}_\mathrm{THEO}\) is independent of \(a_{0}\)). The concentration must also be provided by the user
The final concentration of B, \(b_{\infty}\) is related to \(a_0\) by the relation:
\[\frac{a_0}{\alpha} = \frac{b_{\infty}}{\beta}\]
Adjustable Variables: By default, the model fits the experimental data by adjusting the following:
Rate Constant: \(k\)
Final Value: \(Y_{\infty}\)
Initial Value: \(Y_{0}\) (While \(Y_{0}\) is measured, the algorithm also adjusts it by default to ensure the best fit starting point).
The optimization is performed for a specific reaction order at a time to determine which model best describes the experimental data.
By default, KORD chooses the first and last \(Y_\mathrm{EXP}\) values as \(Y_{0}\) and \(Y_{\infty}\). And a default \(k\) value is also setup by KORD. If you need to change that because of a convergence issue, ensure your starting values for \(k\), \(Y_{0}\) and \(Y_{\infty}\) are realistic to help the algorithm converge.
Letting the optimizer adjust \(Y_0\) freely can let it drift away from the actually
measured starting point just to minimize the RMSE â which defeats the purpose of
measuring it in the first place. Conversely, some experiments have a \(Y_\infty\) that
is known on physical/chemical grounds (e.g. a non-absorbing product, \(Y_\infty = 0\)),
and leaving it free can make the optimizer report a spurious non-zero plateau.
fit() therefore accepts two independent boolean switches, fix_Y0 and
fix_Yinf, to hold either one at its guess value instead of optimizing it
(only \(k\) remains free when both are True â see the note on \(b_\infty\)
below, which is never a free fit parameter for any order). BIC is computed
using the actual number of free parameters, so
model comparison across orders stays meaningful whichever switches are used
(see 4.2.4 Model Selection below â fit() also stores an
AIC for reference in study.results[order]['AIC'], but the printed verdict
is based on BIC only).
Unless you pass Y0_guess/Yinf_guess yourself when creating the KORD
instance, \(Y_0\)âs guess defaults to Y_exp[0] (the first measured point) and
\(Y_\infty\)âs to Y_exp[-1] (the last). The guess and the fix switch are
independent: giving your own Y0_guess does not force fix_Y0 one way or
the other â it only changes which value is used, whether it ends up fixed
or merely as the optimizerâs starting point.
fit(order=1, fix_Y0=False, fix_Yinf=False) never takes k_guess/Y0_guess/
Yinf_guess/binf_guess as arguments: it always reads the current
self.k_guess, self.Y0_guess, self.Yinf_guess, self.binf_guess â
set once at KORD(...)/from_spreadsheet(...) creation time, or changed
later by assigning the attribute directly. See 4.1.2
above for an example.
There is no fix_binf switch, because \(b_\infty\) is never a free fit
parameter in the first place. It is always derived from mass balance,
\(b_\infty = a_0\,\beta/\alpha\), for every order â the same relation Order 1
already relied on. This is not a simplification: for Orders 0 and 2, if \(k\)
and \(b_\infty\) were both left free, the model would depend on them only
through a single combined ratio (\(\beta k/b_\infty\) for order 0,
\(\beta/(b_\infty\alpha^2 k)\) for order 2). Infinitely many \((k, b_\infty)\)
pairs then trace the exact same curve and give the exact same RMSE, so
curve_fit would converge to an arbitrary point on that family â whichever
one happens to sit closest to binf_guess â rather than to a unique,
physically meaningful value. Deriving \(b_\infty\) removes that degeneracy:
binf_guess still exists (used only to draw the guess curve in
simulate_plot() before any fit is run), but
fit() itself ignores it and always uses the mass-balance value, so k is
the only quantity the data actually has to say about \(b_\infty\)âs combined
effect â and it reports a binf value you can trust.
This buys physical consistency, but it also removes the fitâs ability to absorb noise or systematic error on that one point (e.g. mixing dead-time on the first point, or a \(Y_\infty\) assumed from âknownâ chemistry that is in fact slightly off): a wrong fixed value does not just affect that one parameter, it biases \(k\) (and whichever of \(Y_0\)/\(Y_\infty\)/\(b_\infty\) is still free) as the optimizer tries to compensate for it elsewhere.
One case is fixed by necessity rather than by choice: for order 1,
\(\hat{Y}^{[1]}(t)=Y_{\infty}+\exp(-\alpha kt)(Y_{0}-Y_{\infty})\) does not
depend on \(b_\infty\) at all, so there is nothing in the order-1 curve shape
that could identify it â fit() always derives \(b_\infty\) from \(a_0\),
\(\alpha\), \(\beta\) instead of optimizing it, for order 1 only.
4.2.3 Inputï
Data input is performed through a structured spreadsheet file (.xlsx, .ods,
âŠ), read by KORD.load_from_spreadsheet(file_path, exp_number, sheet_name=0)
â or, more conveniently, by KORD.from_spreadsheet(...), which calls it for
you (see 4.1 Quick Start). Within the chosen sheet, each
experiment occupies a pair of columns (t, Y), laid out like this:
Row |
Column \(2k{-}1\) (t) |
Column \(2k\) (Y) |
|---|---|---|
1 |
header for \(t\) |
header for \(Y\) |
2 |
[A]0 |
\([A]_0\) value |
3 |
alpha |
\(\alpha\) value |
4 |
beta |
\(\beta\) value |
5+ |
\(t\) data |
\(Y_\mathrm{EXP}\) data |
where \(k\) is the experiment number (exp_number, 1-indexed). In the
Column \(2k{-}1\) (t), [A]0, alpha and beta are unused labels, written in
the table purely for clarity â only the Column \(2k\) (Y) cells in rows 2-4 are
actually read. Several experiments can sit side by side in the same sheet
this way, each in its own column pair; load_from_spreadsheet returns, for
one experiment: t, Y, (label_t, label_Y), (a0, alpha, beta).
As a sanity check, load_from_spreadsheet inspects rows 2-4 of the t
column (which should be blank or a text label, never real data at this
stage): if it finds a genuine number there instead, it prints a warning
reminding you of the expected format, since this usually means the data
doesnât follow the layout above (e.g. actual \((t,Y)\) data starting at row 2
instead of row 5).
4.2.4 Model Selection: get_best_order() and \(\Delta\)BICï
get_best_order() fits orders 0, 1 and 2 (any order not already in
study.results) and prints a conclusion, in two separate steps:
Best model: the order with the lowest RMSE (plain goodness of fit).
Final Statistical Verdict: a confidence label on how clearly the data favor one order over the others, based on the BIC (Bayesian Information Criterion) rather than the RMSE. BIC trades off goodness of fit against the number of free parameters \(p\) actually optimized (fewer free parameters is rewarded â this matters more once you can fix \(Y_0\)/\(Y_\infty\) with
fix_Y0/fix_Yinf, since that changes \(p\)):\[BIC = n\ln(RMSE^{2}) + p\ln(n)\]where \(n\) is the number of experimental points (the same for every order, since itâs the same dataset) and \(p\) is the number of parameters actually optimized for that order â normally 3 for order 1 (\(k\), \(Y_0\), \(Y_\infty\); \(b_\infty\) is fixed) and 4 for orders 0 and 2 (\(k\), \(Y_0\), \(Y_\infty\), \(b_\infty\)), one less for each of \(Y_0\)/\(Y_\infty\) held fixed via
fix_Y0/fix_Yinf.fit()âs printed GUESS/OPTIM block also shows an AIC (Akaike Information Criterion) value alongside BIC, for reference:\[AIC = n\ln(RMSE^{2}) + 2p\]Same \(n\) and \(p\) as BIC above; the only difference is the penalty for extra parameters (\(2p\) instead of \(p\ln(n)\)), which is milder â AIC penalizes model complexity less harshly than BIC does once \(n\) is more than a handful of points.
study.results[order]['AIC']stores it, but the printed verdict below is based on BIC only â AIC is kept for reference, not used inget_best_order()âs conclusion.The three orders (0, 1, 2) are ranked by their BIC (lowest = best), and
\[\Delta BIC = BIC(\text{2}^{\text{nd}}\text{ best}) - BIC(\text{best})\]is the strength of evidence for the best-BIC order over its closest rival. The verdict label follows the standard scale from Kass, R. E., & Raftery, A. E. (1995), âBayes Factorsâ, J. Am. Stat. Assoc., 90(430), 773â795 â see also the Bayesian information criterion and Bayes factor Wikipedia pages (ÎBIC approximates twice the log Bayes factor between the two models):
\(\Delta\)BIC
Verdict
< 2
Weak / Inconclusive
2â6
Positive
6â10
Strong
> 10
Decisive
Example:
Model Selection Confidence: Decisive,ÎBIC (Best [order 1] vs 2nd Best [order 2]): 359.15means order 1 beats order 2 by 359 BIC points â an overwhelming margin, far past the âdecisiveâ threshold of 10: the data leave essentially no doubt that order 1 is the right kinetic order.Note: the âBest modelâ (step 1, by RMSE) and the two orders compared in the verdict (step 2, by BIC) are usually the same, but are determined independently and need not coincide exactly, especially when parameters are fixed asymmetrically across orders.
4.3 Theoretical Modelï
\(Y_\mathrm{THEO}(t)\) is built directly from the integrated rate laws of section 2: it is a linear combination of the reactant expression \(a(t)\) and the product expression \(b(t)\) introduced there, under the same assumptions (single-component, total, closed, homogeneous, isochoric reaction):
Compte tenu des équations de \(a(t)\) et \(b(t)\) pour les ordres 0, 1 et 2, il vient :
Standard Formula for Order 0:
Warning: This mathematical model for Order 0 is a linear equation. Unlike Order 1 or 2, this linear model does not naturally plateau. Depending on the values of \(k\) and \(t\), the model may predict non-physical values (e.g., negative absorbance or negative concentration) if the time \(t\) exceeds the theoretical completion time \(t_{\mathrm{end}} = \frac{a_{0}}{\alpha k}=\frac{b_{\infty}}{\beta k}\). These values are mathematical artifacts and should be ignored.
Standard Formula for Order 1:
Standard Formula for Order 2:
4.4 Advanced useï
4.4.1 Comparing several fitsï
superpose_plots(studies, order=1, show_experimental=True, labels=None, fix_Y0=False, fix_Yinf=False, save_img=None) is a standalone function (not
a KORD method, since it takes a list of instances) that overlays the
fitted curve of one chosen order â and, by default, each studyâs data â
across several KORD studies on the same graph. Typical use: the same data
fitted under different fix_Y0/fix_Yinf settings, to see directly how
that choice moves the fitted curve.
Being standalone, it is called as superpose_plots(...), imported
separately (from pyphyschemtools.kinetics import superpose_plots) â
not as KORD.superpose_plots(...), which raises an AttributeError
since KORD has no such method.
study.copy() returns an independent copy â data, guesses and cached
results are duplicated, not shared, so fitting one does not affect the
other â which is the natural way to get two variants of the same study to
compare:
from pyphyschemtools.kinetics import superpose_plots
study_free = KORD.from_spreadsheet(file, exp_number=1)
study_fixed = study_free.copy()
study_free.fit(order=1, fix_Y0=False) # Y0 optimized
study_fixed.fit(order=1, fix_Y0=True) # Y0 held fixed at its guess value
superpose_plots([study_free, study_fixed], order=1)
Each studyâs legend entry is tagged automatically (free fit, Y0 fixed,
Yinf fixed, or both), based on the actual fit found in study.results.
If an order isnât fitted yet for a given study, it is fitted now with
fix_Y0/fix_Yinf; if itâs already cached under different settings,
the cache is kept as-is (same rule as plot_all_fits(), see
4.1.1 above) and a warning is printed for that
study.
4.4.2 from_spreadsheet vs load_from_spreadsheetï
from_spreadsheet(file_path, exp_number, sheet_name=0, show_data=True, colab=False, **kwargs)
is a convenience constructor: it reads the experiment with
load_from_spreadsheet (below) and forwards headers, a0, alpha, beta
to KORD() for you â any extra keyword (k_guess, Y0_guess, verbose,
âŠ) is passed straight through to KORD.__init__. The returned study is a
perfectly ordinary KORD instance: all its attributes (study.k_guess,
study.Y0_guess, study.a0, âŠ) are set exactly as they would be from a
direct KORD(...) call. file_path can be any local path ("data/exp.ods",
get_ppct_data(...) is just one way to point at a bundled example file â
your own files work the same way), or, on Google Colab, pass colab=True
and omit file_path: an upload button appears and the chosen file is read
instead (same mechanism as kbasic.read(colab=True)). show_data=True
(the default) also prints the usual summary table.
from_spreadsheet is what you should reach for by default â itâs what every
example in this doc uses. The lower-level load_from_spreadsheet (which it
calls internally) stays available separately, and is worth using directly
when you need to inspect, filter or correct the loaded data or parameters
before creating the KORD instance â e.g. overriding a stoichiometry
value read from the file, dropping an outlier point, or feeding the same
raw \((t, Y)\) into kbasic instead:
t, Y, headers, parameters = KORD.load_from_spreadsheet(file, exp_number=1)
# ... inspect / fix t, Y, parameters here if needed ...
study = KORD(t, Y, headers, a0=parameters[0], alpha=parameters[1], beta=parameters[2])
4.4.3 Reading a different sheet / experimentï
load_from_spreadsheet(file_path, exp_number, sheet_name=0, show_data=True)
(formerly named load_from_excel â same method; it reads with
pandas.read_excel, so it already worked on .ods files as well as .xlsx,
and the old, Excel-specific name was misleading. load_from_excel still
works, as a deprecated alias.) reads one sheet (sheet_name, by index or
by name â the first sheet by default) and, within that sheet, picks the
column pair identified by exp_number (1 for the first pair of columns, 2
for the next pair, etc. â see the data format in
4.2.3 Input above). from_spreadsheet takes the exact same
sheet_name/exp_number arguments and forwards them:
If every experiment lives in its own pair of columns within the same sheet, just change
exp_number(exp_number=2for the second experiment).If each experiment instead lives on its own sheet/tab of the workbook, pass
sheet_nameexplicitly, by index or by name, e.g.:study = KORD.from_spreadsheet(file, exp_number=1, sheet_name=1) # or, by sheet name: study = KORD.from_spreadsheet(file, exp_number=1, sheet_name="Feuille2")
API referenceï
- class pyphyschemtools.kinetics.KORD(t_exp=None, Y_exp=None, headers=('Time / s', 'Y'), a0=1.0, alpha=1.0, beta=1.0, k_guess=None, Y0_guess=None, Yinf_guess=None, binf_guess=None, t_simul_max=15, verbose=True)[source]ï
Bases:
objectInitialize the kinetic study with experimental data. Reaction: alpha A = beta B
- Parameters:
t_exp (-) â Time values.
Y_exp (-) â Measured physical quantity (Absorbance, Conductivity, etc.).
- Fixed Parameters:
alpha (float): Stoichiometric coefficient for A reactant (smallest positive integer). Default is 1.0.
beta (float): Stoichiometric coefficient for B product (smallest positive integer). Default is 1.0.
a0 (float): Initial concentration. Note: Y_theo is independent of a0 for Order 1. Default is 1.0.
- Adjustable Variables (Initial Guesses):
k_guess (float, optional): Initial estimate for the rate constant. Default is 0.01.
Y0_guess (float, optional): Initial estimate for the initial measured value (Y at t=0). if None, it will be initialized as Y_exp[0]
Yinf_guess (float, optional): Initial estimate for the final measured value (Y at infinity). if None, it will be initialized as Y_exp[-1]
binf_guess (float, optional): Initial estimate for the final concentration of B ([B] at infinity). if None, it will be initialized as [A](t=0), i.e. a0
- Other Args:
t_simul_max (float): the maximum time duration for theoretical simulations. It defines the range of the time vector [0, t_simul_max}] used to simulate and plot kinetic curves with simulate_plot
verbose (bool): If True defaulk), enables detailed optimization logs (recommended).
headers (tuple of strings): headers of the t_exp and Y_exp arrays read in the excel file
- concentrations(order, t, binf, k)[source]ï
Calculate the time-dependent concentrations of reactant A and product B.
This method derives the initial concentration (a0) from the fitted final product concentration (binf) using the stoichiometric ratio alpha/beta. It then computes the concentration profiles based on the integrated rate laws for the specified kinetic order.
- Parameters:
order (int) â Kinetic order (0, 1, or 2).
t (array-like) â Time vector for the calculation.
binf (float) â Final concentration of product B at t=infinity.
k (float) â Rate constant.
- Returns:
- (a, b) where âaâ is the concentration array of reactant A
and âbâ is the concentration array of product B.
- Return type:
tuple
Notes
Order 0: Linear decay/formation until reactant exhaustion.
Order 1: Exponential decay/formation.
Order 2: Hyperbolic progression based on the 1/[A] linear relationship.
- copy()[source]ï
FR â Retourne une copie indĂ©pendante de cette Ă©tude KORD : les donnĂ©es, les guesses et les rĂ©sultats dĂ©jĂ en cache (self.results) sont dupliquĂ©s, pas partagĂ©s. Un fit() ultĂ©rieur sur la copie (ou sur lâoriginal) ne modifie pas lâautre.
Utile pour comparer, par exemple, un ajustement avec Y0 figĂ© et un ajustement libre sur les mĂȘmes donnĂ©es, sans que lâun Ă©crase le cache de lâautre (voir superpose_plots()) :
>>> study_free = KORD.from_spreadsheet(file, exp_number=1) >>> study_fixed = study_free.copy() >>> study_fixed.fit(order=1, fix_Y0=True) >>> superpose_plots([study_free, study_fixed], order=1)
EN â Returns an independent copy of this KORD study: the data, the guesses, and any results already cached (self.results) are duplicated, not shared. A later fit() on the copy (or on the original) does not affect the other.
Useful to compare, e.g., a fit with Y0 frozen against a free fit of the same data, without either one overwriting the otherâs cache (see superpose_plots()):
>>> study_free = KORD.from_spreadsheet(file, exp_number=1) >>> study_fixed = study_free.copy() >>> study_fixed.fit(order=1, fix_Y0=True) >>> superpose_plots([study_free, study_fixed], order=1)
- fit(order=1, fix_Y0=False, fix_Yinf=False)[source]ï
Fits the selected kinetic model (Order 0, 1, or 2) to the experimental data.
This method uses non-linear least squares (scipy.optimize.curve_fit) to optimize the rate constant (k), and - unless fixed, see below - the initial and final physical property values (Y0, Yinf). The final product concentration (binf) is never a free fit parameter: it is always derived from mass balance as binf = a0*beta/alpha (see the Notes below for why). Physical constraints are applied via parameter bounds to ensure k remains positive.
It includes automated âSmart Guessingâ for k, based on initial rates and computes information criteria (AIC/BIC) for model selection.
The starting guesses (k_guess, Y0_guess, Yinf_guess) are NOT passed here: they are read from the attributes set when the KORD instance was created (self.k_guess, self.Y0_guess, self.Yinf_guess). To change a guess, set it on the instance (or pass it to KORD(âŠ) / KORD.from_spreadsheet(âŠ) in the first place) before calling fit(), e.g.
study.Y0_guess = 1.40thenstudy.fit(order=1, fix_Y0=True).- Parameters:
order (int, optional) â Kinetic order of the reaction (0, 1, or 2). Defaults to 1.
fix_Y0 (bool, optional) â If True, Y0 is held fixed at Y0_guess (normally the first measured value) instead of being optimized. Use this when you trust the measured starting point and want to stop the fit from drifting it away to improve the apparent fit. Defaults to False (Y0 is optimized, as before).
fix_Yinf (bool, optional) â If True, Yinf is held fixed at Yinf_guess instead of being optimized. Use this when the final value is known on physical/chemical grounds (e.g. a non-absorbing product, Yinf = 0) and you donât want the optimizer to report a spurious non-zero plateau. Defaults to False (Yinf is optimized, as before).
- Returns:
- A dictionary containing the optimized parameters (âkâ, âY0â,
âYinfâ), the derived final product concentration (âbinfâ), and statistical metrics (âRMSEâ, âR2â, ât_halfâ). Returns None if optimization fails to converge.
- Return type:
dict
Notes
binf is never fitted: it is always derived as binf = a0*beta/alpha, for every order, the same formula Order 1 already relied on. This is not an approximation to simplify the model â it is required for k and binf to be individually meaningful. For Orders 0 and 2, if k AND binf were both left free, the modelâs shape would depend on them only through one combined ratio (beta*k/binf for order 0, beta/(binf*alpha**2*k) for order 2): infinitely many (k, binf) pairs then give an identical curve and an identical RMSE, so curve_fit would converge to an arbitrary point on that family (whichever one is closest to binf_guess) rather than to a unique, physically meaningful answer. Deriving binf from mass balance removes that degeneracy.
fix_Y0 / fix_Yinf let you turn off the optimization of Y0 and/or Yinf independently of each other, for any order. AIC/BIC account for the resulting (smaller) number of free parameters, so model comparison across orders remains fair even when parameters are fixed.
- classmethod from_spreadsheet(file_path=None, exp_number=1, sheet_name=0, show_data=True, colab=False, **kwargs)[source]ï
Convenience constructor: reads one experiment from a spreadsheet with load_from_spreadsheet(), then builds and returns a KORD instance from it, in a single call.
Equivalent to:
t, Y, headers, (a0, alpha, beta) = KORD.load_from_spreadsheet( file_path, exp_number, sheet_name, show_data, colab) study = KORD(t, Y, headers, a0=a0, alpha=alpha, beta=beta, **kwargs)
Any extra keyword argument (k_guess, Y0_guess, Yinf_guess, binf_guess, t_simul_max, verbose, âŠ) is forwarded to KORD.__init__. colab=True opens the Google Colab upload widget instead of reading file_path (same as in load_from_spreadsheet / kbasic.read).
Use load_from_spreadsheet() directly instead when you need to inspect or modify the loaded data/parameters before creating the KORD instance.
- get_best_order(verbose=True)[source]ï
Determines and prints the best model based on the lowest RMSE.
- halflife(order, binf, k)[source]ï
Calculate the half-life (t1/2) for the reaction based on the kinetic order.
The half-life represents the time required for the reactant concentration to reach half of its initial value (a0/2).
- Parameters:
order (int) â Kinetic order (0, 1, or 2).
k (float) â The optimized or guessed rate constant.
- Returns:
The calculated half-life value.
- Return type:
float
Notes
Order 0: t1/2 = a0 / (2 * alpha * k) -> Dependent on a0.
Order 1: t1/2 = ln(2) / (alpha * k) -> Independent of a0.
Order 2: t1/2 = 1 / (alpha * k * a0) -> Inversely proportional to a0.
- static load_from_excel(file_path=None, exp_number=1, sheet_name=0, show_data=True, colab=False)[source]ï
Deprecated alias for
load_from_spreadsheet()(kept so existing scripts/notebooks that callload_from_excelkeep working â the method always read any spreadsheet format pandas.read_excel supports, including .ods, so the old name was misleading). Preferload_from_spreadsheetin new code.
- static load_from_spreadsheet(file_path=None, exp_number=1, sheet_name=0, show_data=True, colab=False)[source]ï
Static method to extract data from a spreadsheet file (.xlsx, .xls, .ods, ⊠anything pandas.read_excel can open â a local path, or any path/URL pandas.read_excel accepts). Selects the pair of columns (t, Y) corresponding to the experiment number. Also loads parameters (a0, alpha, beta)
- Format:
Row 1: Headers for t and Y
Row 2: [A]0 value (in the Y column)
Row 3: alpha value (in the Y column)
Row 4: beta value (in the Y column)
Row 5+: [t, Y] data points
- Parameters:
file_path â path (or URL) of the spreadsheet to read. Not needed if colab=True (the file is picked via the Colab upload widget).
colab â if True, opens the Google Colab upload button and reads the chosen file instead of file_path. Default False.
- plot_all_fits(save_img=None, fix_Y0=False, fix_Yinf=False)[source]ï
Plots experimental data and all three kinetic models for visual comparison. If save_img is provided, saves the plot (png, svg, jpg, pdf according to the extension). Vectorial svg is recommended.
fix_Y0 / fix_Yinf are forwarded to fit() for any order not already present in self.results, so you can get all three comparison plots fitted with Y0 and/or Yinf held fixed (see fit()âs docstring).
An order already present in self.results (from an earlier fit() call) is NOT re-fitted here, even if fix_Y0/fix_Yinf passed to this plot differ from the ones that produced the cached fit â your earlier, explicit choice is respected rather than silently overwritten. If that creates a mismatch (some orders fitted under different fix_Y0/fix_Yinf settings than the others), a warning table is printed, since comparing orders fitted under different constraints can be misleading.
- simulate_plot(order=None, show_experimental=True, save_img=None)[source]ï
Plots the initial guesses for one or all three kinetic orders (0, 1, 2), for example to visualize the starting point of a fit before calling fit().
- Parameters:
order (int or None, optional) â which orderâs guess to plot. None (the default) plots all three (0, 1, 2), as before. Pass 0, 1 or 2 to show only that one orderâs guess curve.
show_experimental (bool, optional) â if True (the default), overlays the measured data (self.t_exp, self.Y_exp) as scatter points, so the guess can be checked visually against the actual data instead of only against the other guesses. Set to False to recover the old guesses-only behaviour.
If save_img is provided, saves the plot (png, svg, jpg, pdf according to the extension). Vectorial svg is recommended
- class pyphyschemtools.kinetics.kbasic[source]ï
Bases:
objectFR â Recherche pĂ©dagogique de la constante de vitesse k.
Utilisation typique
kb = kbasic() kb.read("mesures.xlsx") # 1) lire les données kb.fit(ordre=1) # 2) chercher le meilleur k kb.plot() # 3) tracer le résultat
La grandeur mesurĂ©e Y dĂ©croĂźt avec le temps depuis sa valeur initiale Y0. La forme de Y(t) dĂ©pend de lâordre de la rĂ©action :
ordre 0 : dĂ©croissance LINĂAIRE
ordre 1 : décroissance EXPONENTIELLE (le cas le plus courant)
ordre 2 : décroissance HYPERBOLIQUE
EN â Pedagogical search for the rate constant k.
Typical usage
kb = kbasic() kb.read("measurements.xlsx") # 1) read the data kb.fit(order=1) # 2) look for the best k kb.plot() # 3) plot the result
The measured quantity Y decreases with time from its initial value Y0. The shape of Y(t) depends on the reaction order:
order 0: LINEAR decay
order 1: EXPONENTIAL decay (the most common case)
order 2: HYPERBOLIC decay
- fit(ordre=1, Y0=None, k_depart=None)[source]ï
FR â Trouve la valeur de k qui minimise le RMSD pour lâordre choisi.
Si Y0 nâest pas donnĂ©, on le prend Ă©gal Ă la premiĂšre valeur mesurĂ©e. Sinon, on utilise la valeur fournie par lâutilisateur.
Si k_depart nâest pas donnĂ© (dĂ©faut), il est estimĂ© automatiquement Ă partir des donnĂ©es par _k_depart_auto() (voir cette mĂ©thode) au lieu dâune constante arbitraire â utile si k est trĂšs diffĂ©rent de 0.1 en ordre de grandeur, ce qui pouvait auparavant ralentir ou faire Ă©chouer la convergence. Vous pouvez toujours imposer votre propre valeur de dĂ©part en passant k_depart explicitement.
Calcule aussi le temps de demi-rĂ©action t_1/2 (stockĂ© dans self.t_half et affichĂ©), selon la formule propre Ă lâordre choisi (voir section 3.4 âTheoryâ de Kinetics.md) :
ordre 0 : t_1/2 = Y0 / (2*k)
ordre 1 : t_1/2 = ln(2) / k
ordre 2 : t_1/2 = 1 / k
Retourne : (k_optimal, rmsd_au_minimum)
EN â Finds the value of k that minimizes the RMSD for the chosen order.
If Y0 is not given, it is taken equal to the first measured value. Otherwise, the user-provided value is used.
If k_depart is not given (default), it is estimated automatically from the data by _k_depart_auto() instead of an arbitrary constant â useful when k is far from 0.1 in order of magnitude, which could previously slow down or fail convergence. You can still force your own starting value by passing k_depart explicitly.
Also computes the half-life t_1/2 (stored in self.t_half and printed), using the order-specific formula (see section 3.4 âTheoryâ in Kinetics.md):
order 0: t_1/2 = Y0 / (2*k)
order 1: t_1/2 = ln(2) / k
order 2: t_1/2 = 1 / k
Returns: (k_optimal, rmsd_at_minimum)
- plot()[source]ï
FR â Trace la comparaison donnĂ©es / modĂšle et le paysage du RMSD. EN â Plots the data/model comparison and the RMSD landscape.
- read(fichier=None, colab=False)[source]ï
FR â Lit un fichier Excel Ă deux colonnes (temps, Y) et affiche un rĂ©capitulatif sous forme de DataFrame pandas.
DĂ©tecte automatiquement si la premiĂšre ligne est un en-tĂȘte. Si oui, les noms de colonnes serviront de labels pour les axes du tracĂ©.
Retourne le DataFrame des données lues.
- Arguments :
- fichierchemin (ou URL) du fichier Excel Ă lire.
Inutile si colab=True (on choisit le fichier Ă lâupload).
- colabsi True, ouvre le bouton dâupload de Google Colab
et lit le fichier choisi. Défaut False.
EN â Reads a two-column Excel file (time, Y) and displays a summary as a pandas DataFrame.
Automatically detects whether the first row is a header. If so, the column names are used as axis labels for the plot.
Returns the DataFrame of the data read.
- Parameters:
fichier â path (or URL) of the Excel file to read. Unused if colab=True (the file is picked via the upload widget).
colab â if True, opens the Google Colab upload button and reads the chosen file. Default False.
- pyphyschemtools.kinetics.superpose_plots(studies, order=1, show_experimental=True, labels=None, fix_Y0=False, fix_Yinf=False, save_img=None)[source]ï
FR â Superpose, sur un mĂȘme graphe, la courbe ajustĂ©e (fit, pas guess) dâun seul ordre pour plusieurs instances KORD â typiquement la mĂȘme expĂ©rience fittĂ©e dans des conditions diffĂ©rentes (p.ex. une Ă©tude avec Y0 figĂ©, une autre oĂč Y0 reste libre), pour voir directement lâeffet de ce choix sur la courbe ajustĂ©e.
Pour chaque Ă©tude, si order nâest pas dĂ©jĂ prĂ©sent dans study.results, lâajustement est fait maintenant avec fix_Y0/fix_Yinf (mĂȘmes rĂ©glages que plot_all_fits()). Si order est dĂ©jĂ en cache (study.results) avec des rĂ©glages fix_Y0/fix_Yinf diffĂ©rents de ceux demandĂ©s ici, le cache nâest PAS Ă©crasĂ© â exactement le comportement de plot_all_fits() â mais un avertissement est affichĂ© pour cette Ă©tude. La lĂ©gende indique automatiquement, pour chaque Ă©tude, si Y0 et/ou Yinf ont Ă©tĂ© figĂ©s dans lâajustement effectivement tracĂ©.
- Args :
- studies (list[KORD])les instances KORD Ă comparer (peuvent ĂȘtre
des fits de la mĂȘme donnĂ©e, sous des rĂ©glages diffĂ©rents).
- order (int, optionnel)lâordre Ă tracer pour toutes les Ă©tudes
(0, 1 ou 2). Par défaut 1.
- show_experimental (bool, optionnel)si True (par défaut), superpose
aussi les données mesurées de chaque étude.
- labels (list[str] ou None, optionnel)légendes de base pour chaque
Ă©tude ; par dĂ©faut âStudy 1â, âStudy 2â, ⊠(le statut fix_Y0/fix_Yinf est ajoutĂ© automatiquement).
- fix_Y0, fix_Yinf (bool, optionnel)transmis Ă fit() pour toute
Ă©tude dont order nâest pas dĂ©jĂ dans son .results.
save_img : comme pour simulate_plot()/plot_all_fits().
EN â Overlays, on a single graph, the fitted (not guess) curve of one chosen order for several KORD instances â typically the same experiment fitted under different conditions (e.g. one study with Y0 frozen, another with Y0 left free), to see directly how that choice changes the fitted curve.
For each study, if order is not already present in study.results, it is fitted now with fix_Y0/fix_Yinf (same settings as plot_all_fits()). If order is already cached in study.results under different fix_Y0/fix_Yinf settings than requested here, the cache is NOT overwritten â exactly like plot_all_fits() â but a warning is printed for that study. The legend automatically shows, for each study, whether Y0 and/or Yinf were held fixed in the fit actually plotted.
- Parameters:
studies (list[KORD]) â the KORD instances to compare (can be fits of the very same data, under different settings).
order (int, optional) â the order to plot for every study. Default 1.
show_experimental (bool, optional) â if True (the default), also overlay each studyâs measured data.
labels (list[str] or None, optional) â base legend labels; default âStudy 1â, âStudy 2â, ⊠(the fix_Y0/fix_Yinf status is added automatically).
fix_Y0 (bool, optional) â forwarded to fit() for any study whose order isnât already in its .results.
fix_Yinf (bool, optional) â forwarded to fit() for any study whose order isnât already in its .results.
save_img â as in simulate_plot()/plot_all_fits().