Example 2 — Custom Python growth model#

Demonstrates defining a growth model as a Python function and fitting it via NLModel.

Full script: examples/scripts/03_custom_model.py

Define a Gompertz model#

import numpy as np
from pykinbiont import NLModel

def gompertz(p, t):
    """
    Modified Gompertz function.

    Parameters
    ----------
    p : array-like, shape (3,)
        [A, mu, lam]  —  carrying capacity, max growth rate, lag time
    t : float or ndarray
        Time point(s)
    """
    A, mu, lam = p[0], p[1], p[2]
    return A * np.exp(-np.exp(mu * np.e / A * (lam - t) + 1))

custom_gompertz = NLModel(
    name="gompertz",
    func=gompertz,
    param_names=["A", "mu", "lam"],
)

Synthetic data with a visible lag phase#

import pykinbiont
from pykinbiont import GrowthData

pykinbiont.configure("/path/to/KinBiont.jl")

def true_gompertz(t, A=1.1, mu=0.5, lam=2.0):
    return A * np.exp(-np.exp(mu * np.e / A * (lam - t) + 1))

rng   = np.random.default_rng(42)
times = np.linspace(0, 18, 60)

curves = np.stack([
    np.maximum(
        true_gompertz(times, A=1.0 + 0.1 * i, mu=0.4 + 0.05 * i, lam=2.0 + 0.3 * i)
        + rng.normal(0, 0.007, len(times)),
        1e-4,
    )
    for i in range(3)
])

data = GrowthData(curves=curves, times=times, labels=["G1", "G2", "G3"])

Fit#

from pykinbiont import ModelSpec, FitOptions, fit

spec = ModelSpec(
    models=[custom_gompertz],
    params=[[1.0, 0.5, 2.0]],
    lower=[[0.0, 0.0, 0.0]],
    upper=[[3.0, 3.0, 10.0]],
)
opts = FitOptions(multistart=True, n_restart=30)

results = fit(data, spec, opts)

for r in results:
    p = dict(zip(r.param_names, r.best_params))
    print(f"{r.label}:  A={p['A']:.3f}  mu={p['mu']:.3f}  lam={p['lam']:.3f}"
          f"  AIC={r.best_aic:.2f}")

Compare custom vs built-in#

from pykinbiont import MODEL_REGISTRY

logistic_model = MODEL_REGISTRY["NL_logistic"]

spec_both = ModelSpec(
    models=[custom_gompertz, logistic_model],
    params=[[1.0, 0.5, 2.0], [1.2, 0.5, 0.01]],
    lower=[[0.0, 0.0, 0.0], [0.0, 0.0, 0.0]],
    upper=[[3.0, 3.0, 10.0], [5.0, 5.0, 1.0]],
)

results_both = fit(data, spec_both, opts)
df = results_both.to_dataframe()
print(df[["label", "best_model", "aic", "loss"]])

Custom ODE model#

For models defined as differential equations, use ODEModel:

from pykinbiont import ODEModel

def logistic_ode(du, u, p, t):
    """Logistic growth ODE: dN/dt = mu*N*(1 - N/K)"""
    mu, K = p[0], p[1]
    du[0] = mu * u[0] * (1.0 - u[0] / K)

ode_model = ODEModel(
    name="logistic_ode",
    func=logistic_ode,
    param_names=["mu", "K"],
    n_eq=1,
)

spec_ode = ModelSpec(
    models=[ode_model],
    params=[[0.5, 1.2]],
    lower=[[0.0, 0.1]],
    upper=[[5.0, 5.0]],
)

results_ode = fit(data, spec_ode, opts)