# Example 1 — Basic logistic fitting Fits a logistic growth model to synthetic plate-reader data, plots observed vs fitted curves, and compares two models by AICc. Full script: `examples/scripts/02_nl_fitting.py` ## Setup ```python import numpy as np import matplotlib.pyplot as plt import pykinbiont from pykinbiont import GrowthData, FitOptions, ModelSpec, MODEL_REGISTRY, fit pykinbiont.configure("/path/to/KinBiont.jl") # skip if using registry version ``` ## Generate synthetic data ```python def logistic(t, K=1.2, mu=0.5, N0=0.01): return K / (1 + ((K - N0) / N0) * np.exp(-mu * t)) rng = np.random.default_rng(1) times = np.linspace(0, 20, 80) curves = np.stack([ np.maximum( logistic(times, K=1.0 + 0.1 * i, mu=0.4 + 0.05 * i, N0=0.01) + rng.normal(0, 0.007, len(times)), 1e-4, # clip to avoid log(negative) errors ) for i in range(4) ]) data = GrowthData( curves=curves, times=times, labels=["W1", "W2", "W3", "W4"], ) ``` ```{admonition} Why clip to 1e-4? :class: note KinBiont computes `log(OD)` internally to estimate growth rates. Any OD ≤ 0 raises a `DomainError`. Clipping Gaussian noise at a small positive floor prevents this. ``` ## Fit with logistic model ```python logistic_model = MODEL_REGISTRY["NL_logistic"] spec = ModelSpec( models=[logistic_model], params=[[1.2, 0.01, 0.5]], # [N_max, N0, mu] lower=[[0.3, 1e-3, 0.05]], upper=[[3.0, 0.05, 3.0]], ) opts = FitOptions(smooth=True, smooth_method="rolling_avg", multistart=True, n_restart=20) results = fit(data, spec, opts) ``` ## Inspect results ```python df = results.to_dataframe() print(df[["label", "best_model", "aic", "param_1", "param_2", "param_3"]]) ``` | label | best_model | aic | param_1 (N_max) | param_2 (N0) | param_3 (mu) | |---|---|---|---|---|---| | W1 | NL_logistic | −142 | 1.007 | 0.010 | 0.401 | | W2 | NL_logistic | −138 | 1.103 | 0.010 | 0.449 | ## Plot ```python fig, axes = plt.subplots(2, 2, figsize=(9, 6)) for ax, r in zip(axes.flat, results): idx = data.labels.index(r.label) ax.plot(data.times, data.curves[idx], ".", markersize=3, alpha=0.5, label="data") ax.plot(r.times, r.fitted_curve, "-", lw=2, label="logistic fit") ax.set_title(r.label) ax.set_xlabel("Time (h)") ax.set_ylabel("OD") ax.legend(fontsize=7) plt.tight_layout() plt.savefig("logistic_fits.png", dpi=150) ``` ## Two-model comparison ```python gompertz_model = MODEL_REGISTRY["NL_gompertz"] spec_both = ModelSpec( models=[logistic_model, gompertz_model], params=[[1.2, 0.01, 0.5], [1.2, 0.5, 1.0]], lower=[[0.3, 1e-3, 0.05], [0.3, 0.0, 0.0]], upper=[[3.0, 0.05, 3.0], [3.0, 3.0, 10.0]], ) results_both = fit(data, spec_both, opts) print(results_both.to_dataframe()[["label", "best_model", "aic"]]) ``` The model with the lower AICc is selected automatically per curve.