Source code for pykinbiont.models
"""Growth model types and the built-in model registry."""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Callable, Optional
[docs]
class AbstractGrowthModel:
"""Base class for all growth models. Do not instantiate directly."""
[docs]
@dataclass
class NLModel(AbstractGrowthModel):
"""Non-linear (closed-form) growth model.
Parameters
----------
name:
Unique identifier used in results.
func:
Python callable with signature ``(p, t) -> y`` where p is a 1-D array
of parameters and t is a scalar or 1-D array of time points.
None for built-in registry models (Julia has the function).
param_names:
Human-readable name for each parameter.
"""
name: str
func: Optional[Callable] = None
param_names: list[str] = field(default_factory=list)
[docs]
@dataclass
class ODEModel(AbstractGrowthModel):
"""ODE growth model (SciML in-place form ``f!(du, u, p, t)``).
Parameters
----------
name:
Unique identifier used in results.
func:
Python callable with signature ``f(du, u, p, t)`` (in-place).
None for built-in registry models.
param_names:
Human-readable name for each parameter.
n_eq:
Number of state equations.
"""
name: str
func: Optional[Callable] = None
param_names: list[str] = field(default_factory=list)
n_eq: int = 1
[docs]
@dataclass
class LogLinModel(AbstractGrowthModel):
"""Sentinel for log-linear (exponential phase) fitting. No parameters."""
[docs]
@dataclass
class DDDEModel(AbstractGrowthModel):
"""Data-Driven Differential Equation discovery model (sparse regression).
Parameters
----------
max_degree:
Maximum polynomial degree in the candidate basis.
lambda_min:
log₁₀ of the minimum STLSQ sparsity threshold.
lambda_max:
log₁₀ of the maximum STLSQ sparsity threshold.
lambda_step:
Step in log₁₀ space between threshold values.
Note: requires DataDrivenDiffEq + DataDrivenSparse + ModelingToolkit in
the Julia environment. Manage these with Pkg.add in Julia.
"""
max_degree: int = 4
lambda_min: float = -5.0
lambda_max: float = -1.0
lambda_step: float = 0.5
class _LazyRegistry(dict):
"""dict subclass that populates itself from Julia on first access."""
def __init__(self):
super().__init__()
self._populated: bool = False
def _populate(self) -> None:
if self._populated:
return
from pykinbiont._core import get_jl
jl = get_jl()
# Keys with "NL_" prefix are closed-form NLModel; all others are ODEModel.
# This mirrors the naming convention in KinBiont.jl's MODEL_REGISTRY.
for name, jl_model in jl.Kinbiont.MODEL_REGISTRY.items():
name_str = str(name)
param_names = [str(p) for p in jl_model.param_names]
if name_str.startswith("NL_"):
self[name_str] = NLModel(name=name_str, param_names=param_names)
else:
n_eq = int(jl_model.n_eq) if hasattr(jl_model, "n_eq") else 1
self[name_str] = ODEModel(name=name_str, param_names=param_names, n_eq=n_eq)
self._populated = True
def __getitem__(self, key):
self._populate()
return super().__getitem__(key)
def __contains__(self, key):
self._populate()
return super().__contains__(key)
def __len__(self):
self._populate()
return super().__len__()
def __iter__(self):
self._populate()
return super().__iter__()
def get(self, key, default=None):
self._populate()
return super().get(key, default)
def keys(self):
self._populate()
return super().keys()
def values(self):
self._populate()
return super().values()
def items(self):
self._populate()
return super().items()
MODEL_REGISTRY: dict[str, NLModel | ODEModel] = _LazyRegistry()
"""Built-in model registry. Keys with 'NL_' prefix are NLModel; others are ODEModel.
Populated lazily on first access (triggers Julia startup if not already running)."""