Executable scientific examples¶
Eleven seeded workflows model concentration decay and Michaelis–Menten kinetics. They execute with only the package, standard library, NumPy and SciPy. No external data or plotting dependency is required.
Run one or run them all¶
From an installed checkout:
python examples/workflows.py
python examples/workflows.py profile
From a source checkout in the locked documentation environment:
uv run --project requirements/docs --locked python docs/_tools/validate.py --examples-only
| Command name | Scientific question | Further reading |
|---|---|---|
confidence |
How uncertain is a fitted decay curve? | Quickstart |
intervals |
How do mean and new-measurement intervals differ? | Fit to uncertainty |
diagnostics |
What do Gaussian diagnostics and AIC/BIC measure? | Likelihood conventions |
summary |
How should a nonlinear parameter table be read? | Wald limitations |
profile |
Are kinetic parameter errors asymmetric? | Kinetics tutorial |
bootstrap |
What covariance do empirical residual refits give? | Bootstrap assumptions |
collinearity |
Do unsaturated measurements separate Vmax and Km? | Local identifiability |
bounds |
What uncertainty remains with an active baseline bound? | Conditional covariance |
propagation |
How uncertain is a half-life or concentration ratio? | Derived quantities |
global |
Can two experiments share one decay rate? | Global tutorial |
eiv |
What if both time and concentration have error? | EIV tutorial |
Reproducibility and warnings¶
Each experiment controls its random generator. Seeds reproduce stochastic experiments within an environment; optimizers and numerical backends can still differ across platforms. Assertions and reference tests use scale-aware and sampling-aware tolerances rather than cross-platform byte equality.
The correlation and active-bound examples deliberately emit interpretation warnings. EIV emits a compatibility warning on SciPy 1.17–1.18. The validation script permits these specific messages and treats unexpected warnings as errors. Failed fits must not be interpreted as successful uncertainty estimates.
Complete, maintained source¶
The code below is included directly from the repository's executable example file at build time; it is not a separately maintained copy.
All eleven workflows
"""Run eleven reproducible scientific workflows with NumPy and SciPy only.
Execute ``PYTHONPATH=src python examples/workflows.py`` from the checkout.
Pass one workflow name, for example ``profile``, to run just that example.
No plotting dependency or external data is required. Printed bands describe
pointwise uncertainty; output is deliberately labelled by statistical meaning.
"""
from __future__ import annotations
import argparse
from collections.abc import Callable
import numpy as np
from numpy.typing import NDArray
from scipy.optimize import curve_fit
import curvefit_stats as cs
_Array = NDArray[np.float64]
def decay(time: _Array, amplitude: float, rate: float) -> _Array:
"""Evaluate first-order decay, C(t) = amplitude * exp(-rate * t).
Parameters
----------
time : ndarray
Time in hours.
amplitude : float
Initial concentration in milligrams per litre.
rate : float
First-order rate constant in inverse hours.
Returns
-------
ndarray
Concentration in milligrams per litre, with the shape of time.
Notes
-----
The examples fit a positive rate; this expression itself is unconstrained.
Examples
--------
>>> decay(np.array([0.0]), 3.0, 0.5)
array([3.])
"""
return amplitude * np.exp(-rate * time)
def kinetics(substrate: _Array, vmax: float, km: float) -> _Array:
"""Evaluate Michaelis-Menten kinetics, v = vmax * S / (km + S).
Parameters
----------
substrate : ndarray
Substrate concentration in millimolar units.
vmax : float
Saturating reaction velocity in micromoles per minute.
km : float
Half-saturation concentration in millimolar units.
Returns
-------
ndarray
Reaction velocity, with the shape of substrate.
Notes
-----
The examples use positive substrate and km, avoiding a zero denominator.
Examples
--------
>>> kinetics(np.array([1.0]), 3.0, 1.0)
array([1.5])
"""
return vmax * substrate / (km + substrate)
def _decay_fit() -> tuple[_Array, _Array, _Array, _Array]:
"""Create seeded homoscedastic data and an unknown-scale interior fit."""
time = np.linspace(0, 5, 45)
concentration = decay(time, 3.0, 0.7) + np.random.default_rng(2026).normal(
0, 0.08, time.size
)
popt, pcov = curve_fit(decay, time, concentration, p0=[2.5, 0.5])
return time, concentration, popt, pcov
def confidence() -> None:
"""Print fitted decay parameters and pointwise mean-concentration bands.
Returns
-------
None
Print estimates, parameter standard errors, and three band rows.
Notes
-----
Unknown homoscedastic noise uses the approximate Student-t reference.
The evaluation grid does not determine the residual degrees of freedom.
"""
time, concentration, popt, pcov = _decay_fit()
grid = np.array([0.0, 2.5, 5.0])
lower, upper = cs.generate_error_bands(
decay, grid, popt, pcov, concentration, x_fit=time
)
print("parameters [mg/L, 1/h]:", popt)
print("parameter standard errors:", np.sqrt(np.diag(pcov)))
print(
"time, mean, lower, upper:\n",
np.column_stack((grid, decay(grid, *popt), lower, upper)),
)
def intervals() -> None:
"""Contrast uncertainty in the fitted mean with a future noisy observation.
Returns
-------
None
Print confidence and prediction widths at the same times.
Notes
-----
Prediction adds RSS/(N-p), assuming independent homoscedastic future noise.
Neither band provides simultaneous coverage of the entire curve.
"""
time, concentration, popt, pcov = _decay_fit()
grid = np.array([0.0, 2.5, 5.0])
ci = cs.generate_error_bands(decay, grid, popt, pcov, concentration, x_fit=time)
pi = cs.generate_error_bands(
decay, grid, popt, pcov, concentration, x_fit=time, interval_type="prediction"
)
print(
"time, confidence width, prediction width:\n",
np.column_stack((grid, ci[1] - ci[0], pi[1] - pi[0])),
)
assert np.all(pi[1] - pi[0] > ci[1] - ci[0])
def diagnostics() -> None:
"""Audit known-noise decay fits and compare a nested constant-background model.
Returns
-------
None
Print normalized Gaussian AIC/BIC and goodness-of-fit statistics.
Notes
-----
Both models use the same observations and known sigma=0.08 mg/L. Their
likelihoods therefore share a normalization and can be compared.
"""
time, concentration, _, _ = _decay_fit()
def background(t: _Array, amplitude: float, rate: float, baseline: float) -> _Array:
"""Add a concentration baseline to the decay model."""
return decay(t, amplitude, rate) + baseline
beta, _ = curve_fit(
decay, time, concentration, [2.5, 0.5], sigma=0.08, absolute_sigma=True
)
extended, _ = curve_fit(
background,
time,
concentration,
[2.5, 0.5, 0.0],
sigma=0.08,
absolute_sigma=True,
)
print(
"decay:",
cs.audit_fit(decay, time, concentration, beta, sigma=0.08, absolute_sigma=True),
)
print(
"decay + background:",
cs.audit_fit(
background, time, concentration, extended, sigma=0.08, absolute_sigma=True
),
)
def summary() -> None:
"""Print an R-inspired approximate nonlinear Student-t Wald summary.
Returns
-------
None
Print estimate, SE, t, two-sided p-value, and confidence endpoints.
Notes
-----
Tests concern each parameter equalling zero. A significant rate does not
establish model adequacy. This example uses unknown-scale interior fitting.
"""
time, concentration, popt, pcov = _decay_fit()
print(cs.fit_summary(decay, time, concentration, popt, pcov))
def profile() -> None:
"""Print asymmetric Michaelis-Menten profile-likelihood endpoints.
Returns
-------
None
Print signed deviations and absolute endpoints for both parameters.
Notes
-----
Known measurement noise and sigma_level=1 give a likelihood-ratio threshold
of one, with nominal regular-model coverage about 68.27%, not 95%.
Nuisance parameters are reoptimized at every profile evaluation.
"""
substrate = np.linspace(0.1, 2.0, 30)
velocity = kinetics(substrate, 3.0, 1.2) + np.random.default_rng(34).normal(
0, 0.06, substrate.size
)
bounds = ([0.01, 0.01], [20.0, 20.0])
popt, pcov = curve_fit(
kinetics,
substrate,
velocity,
[2.5, 1.0],
sigma=0.06,
absolute_sigma=True,
bounds=bounds,
)
errors = cs.profile_likelihood_errors(
kinetics,
substrate,
velocity,
popt,
sigma=0.06,
absolute_sigma=True,
pcov=pcov,
bounds=bounds,
)
for name, estimate, error in zip(("vmax", "km"), popt, errors, strict=True):
print(
name,
"estimate:",
estimate,
"signed errors:",
error,
"endpoints:",
(estimate + error["down"], estimate + error["up"]),
)
def bootstrap() -> None:
"""Estimate decay covariance with centered residual bootstrap refits.
Returns
-------
None
Print bootstrap mean, covariance, success count, and percentile interval.
Notes
-----
The residuals here are independent and exchangeable. Percentile intervals
come directly from the samples, without imposing a Student-t distribution.
"""
time, concentration, popt, _ = _decay_fit()
mean, covariance, samples = cs.bootstrap_curve_fit(
decay, time, concentration, popt, n_resamples=500, random_state=701
)
print("bootstrap mean:", mean, "covariance:\n", covariance)
print("successful fits / attempts:", samples.shape[0], "/ 500")
print("rate percentile interval [1/h]:", np.quantile(samples[:, 1], [0.025, 0.975]))
def collinearity() -> None:
"""Diagnose local parameter dependence for unsaturated enzyme measurements.
Returns
-------
None
Print correlation, VIF-like values, rank, and flagged parameter pairs.
Notes
-----
Measurements well below saturation can confound vmax and km. The expected
correlation warning concerns local precision, not structural identifiability.
"""
substrate = np.linspace(0.05, 0.8, 50)
velocity = kinetics(substrate, 3.0, 2.0) + np.random.default_rng(97).normal(
0, 0.003, substrate.size
)
_, covariance = curve_fit(
kinetics,
substrate,
velocity,
[2.5, 1.5],
sigma=0.003,
absolute_sigma=True,
bounds=(0.001, np.inf),
)
print(cs.collinearity_diagnostics(covariance))
print("Rank and condition here describe the parameter correlation matrix.")
def bounds() -> None:
"""Show conditional covariance when a fitted decay baseline hits zero.
Returns
-------
None
Print active-parameter NaNs and finite conditional bands for free parameters.
Notes
-----
This deliberately conflicting synthetic baseline drives the lower bound
active. Holding it fixed is a conditional analysis, not full uncertainty.
"""
def background(t: _Array, amplitude: float, rate: float, baseline: float) -> _Array:
"""Evaluate decay with a nonnegative fitted baseline."""
return decay(t, amplitude, rate) + baseline
time = np.linspace(0, 5, 50)
concentration = (
decay(time, 3, 0.7)
- 0.15
+ np.random.default_rng(33).normal(0, 0.02, time.size)
)
limits = ([0, 0, 0], [np.inf, np.inf, np.inf])
popt, _ = curve_fit(
background, time, concentration, [2.5, 0.5, 0.05], bounds=limits
)
covariance = cs.robust_bounded_pcov(background, time, concentration, popt, limits)
assert np.isnan(covariance[2]).all()
def conditional(t: _Array, amplitude: float, rate: float) -> _Array:
"""Hold the active baseline fixed at its fitted value."""
return background(t, amplitude, rate, float(popt[2]))
lower, upper = cs.generate_error_bands(
conditional, time, popt[:2], covariance[:2, :2], dof=time.size - 2
)
print("parameters and conditional covariance:\n", popt, "\n", covariance)
print("conditional band at final time:", lower[-1], upper[-1])
def propagation() -> None:
"""Propagate Gaussian parameter uncertainty to concentration and half-life.
Returns
-------
None
Print vector empirical summaries; units are mg/L and hours respectively.
Notes
-----
Half-life is log(2)/rate. A normal rate can approach zero or become negative;
finite sample summaries do not imply that population ratio moments exist.
"""
_, _, popt, covariance = _decay_fit()
result = cs.propagate_uncertainty(
lambda amplitude, rate: np.array(
[amplitude * np.exp(-2 * rate), np.log(2) / rate]
),
popt,
covariance,
n_samples=20000,
random_state=712,
)
print("[concentration at 2 h, half-life] Gaussian propagation:", result)
def global_fit() -> None:
"""Fit two decay experiments with one shared rate and separate amplitudes.
Returns
-------
None
Print global parameter mapping, solver diagnostics, and per-dataset bands.
Raises
------
RuntimeError
The global solver fails to converge or covariance is undefined.
Notes
-----
Known observation uncertainties use normal-reference bands. Projection
retains shared degrees of freedom; per-dataset y must not redefine them.
"""
rng = np.random.default_rng(480)
datasets = {}
for name, n, end, amplitude in [
("control", 35, 4.0, 3.0),
("treated", 52, 6.0, 4.5),
]:
time = np.linspace(0, end, n)
sigma = 0.05 * (1 + time / 6)
datasets[name] = {
"x": time,
"y": decay(time, amplitude, 0.7) + rng.normal(size=n) * sigma,
"sigma": sigma,
}
group = cs.GlobalFitGroup(decay, datasets, shared_parameters={"rate"})
print("global order:", group.parameter_names)
result = group.fit(
{"rate": 0.5, "control.amplitude": 2.5, "treated.amplitude": 4.0},
bounds=(0.001, np.inf),
absolute_sigma=True,
)
if not result.success or not np.isfinite(result.pcov).all():
raise RuntimeError("Global fit failed or uncertainty is undefined.")
print("global estimates:", result.popt, "diagnostics:", result.diagnostics)
for name, dataset in datasets.items():
indices = group.parameter_indices[name]
local_covariance = result.pcov[np.ix_(indices, indices)]
lower, upper = cs.generate_error_bands(
decay,
dataset["x"],
result.popt[indices],
local_covariance,
reference="normal",
)
print(
name,
"local parameters:",
result.datasets[name].parameters,
"final pointwise interval:",
lower[-1],
upper[-1],
)
def eiv() -> None:
"""Fit decay measurements with uncertainty in time and concentration.
Returns
-------
None
Print vertical least-squares and EIV estimates, ODR status, and mean bands.
Raises
------
RuntimeError
ODR fails to converge, has undefined covariance, or its backend is unavailable.
Notes
-----
A fitted ODR covariance is not a vertical-residual likelihood covariance.
Mean bands here evaluate at exact times and exclude new predictor error.
SciPy 1.17+ emits the documented ODR FutureWarning; 1.19+ is unsupported.
"""
rng = np.random.default_rng(301)
exact_time = np.linspace(0.2, 4, 65)
measured_time = exact_time + rng.normal(0, 0.15, exact_time.size)
concentration = decay(exact_time, 3, 0.7) + rng.normal(0, 0.06, exact_time.size)
ordinary, _ = curve_fit(decay, measured_time, concentration, [2.5, 0.5])
popt, covariance, diagnostic = cs.eiv_curve_fit(
decay,
measured_time,
concentration,
[2.5, 0.5],
sx=0.15,
sy=0.06,
absolute_sigma=True,
full_output=True,
)
if not diagnostic.success or not np.isfinite(covariance).all():
raise RuntimeError("ODR failed or uncertainty is undefined.")
lower, upper = cs.generate_error_bands(
decay, np.array([1.0, 3.0]), popt, covariance, reference="normal"
)
print("vertical LS:", ordinary, "EIV:", popt)
print(
"ODR termination:",
diagnostic.stopreason,
"weighted SSE:",
diagnostic.weighted_sse,
)
print("exact-time mean bands:", lower, upper)
WORKFLOWS: dict[str, Callable[[], None]] = {
"confidence": confidence,
"intervals": intervals,
"diagnostics": diagnostics,
"summary": summary,
"profile": profile,
"bootstrap": bootstrap,
"collinearity": collinearity,
"bounds": bounds,
"propagation": propagation,
"global": global_fit,
"eiv": eiv,
}
def main() -> None:
"""Select one workflow or execute all eleven in documented order.
Returns
-------
None
Print scientific results to standard output; expected warnings remain visible.
"""
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"workflow", nargs="?", choices=["all", *WORKFLOWS], default="all"
)
selection = parser.parse_args().workflow
chosen = (
WORKFLOWS.items() if selection == "all" else [(selection, WORKFLOWS[selection])]
)
for name, workflow in chosen:
print(f"\n{name.upper()}")
workflow()
if __name__ == "__main__":
main()