pharma · pyodide + numpy · no libraries

Dose-Response Curve Fitter

Type in concentrations and % response; Python fits a 4-parameter Hill curve by hand-rolled Nelder-Mead and bootstraps the IC50 confidence interval — all in your browser.

teaching tool, not a lab tool It shows how curve fitting and its uncertainty behave. Do not use it for regulated or reportable results.
starting python runtime…0%
CPython + numpy compiled to WebAssembly. First load pulls the runtime once, then it is cached.

fitted curve8 points · log scale

hover the chart, or focus a row below, to inspect a point

readout—

IC50
—
95% CI —
Hill slope
—
95% CI —
Top
—
% response
Bottom
—
% response
R²
—
RMSE —

how it works
The model is the 4-parameter Hill (logistic) equation — the standard shape for a competitive dose-response:
response(x) = bottom + (top - bottom) / (1 + (x / IC50) ^ hill)
Fitting. Least squares on the four parameters, minimised by a Nelder-Mead simplex written from scratch in numpy — no scipy, no solver library. IC50 is fitted in log10 space (that is the scale the data lives on) and the model is evaluated as 1/(1+exp(hill·ln10·(log10 x − log10 IC50))) so it never overflows. Three starting guesses (hill = 0.5, 1, 2) are tried and the best kept.
Uncertainty. Residual bootstrap: the residuals from the best fit are resampled with replacement, added back onto the fitted curve, and the whole fit is re-run — a few hundred times. The 2.5th and 97.5th percentiles of those refits give the CI and the shaded band. Residual resampling (rather than resampling points) is used because an 8-point dilution series is a designed set — dropping a point at random would destroy the design.
Why it is fast. Running hundreds of simplexes one after another in Python takes seconds. Instead every resample gets its own simplex and they all march in lockstep: one reflection, one expansion and two contractions for the entire batch, as four numpy calls per iteration on a B × 5 × 4 array. Same algorithm, one pass, and the whole bootstrap lands in a fraction of a second.
Why it matters. A single IC50 number hides how much the data actually pins it down. Push the curve so its bottom plateau is off the edge of the tested range and watch the CI blow up while R² stays comfortably high.

datanM · % response

Editable assay data: concentration in nanomolar and percent response, with fit residual per point.
point# conc nM % resp resid remove

Edit any cell — the curve refits as you type.

datasets

bootstrap

Constraining the bottom plateau is what you do when the assay never reaches full inhibition — it trades a free parameter for a much tighter IC50. Try it on compound C.