************************** Advanced: Base Model Class ************************** The :class:`~mgrowthctrl.models.base.BaseODEModel` class provides the foundation for adding ODE-based models to `mGrowthCtrl`. It handles numerical integration, backend selection (`NumPy` or `PyTorch`), and provides hooks for control inputs. The :class:`~mgrowthctrl.models.crm.model.CRModel` is currently the main concrete subclass (see :doc:`consumer_resource_model`). Overview ======== :class:`~mgrowthctrl.models.base.BaseODEModel` is an abstract base class that: * Manages species (:math:`X`) and metabolite (:math:`S`) state vectors via :class:`~mgrowthctrl.models.base.ModelNames` * Supports both `NumPy` (`scipy.integrate `_) and `PyTorch` (`torchdiffeq `_) backends * Provides a unified :meth:`~mgrowthctrl.models.base.BaseODEModel.predict` interface for simulation * Allows plugging in controllers via the :meth:`~mgrowthctrl.models.base.BaseODEModel.control_rhs` hook (see :doc:`../control/introduction`) Example ======= Custom ODE models can be implemented by subclassing :class:`~mgrowthctrl.models.base.BaseODEModel` and defining the system dynamics via :meth:`~mgrowthctrl.models.base.BaseODEModel.compute_rhs`. .. code-block:: python from mgrowthctrl.models.base import BaseODEModel, ModelNames class MyModel(BaseODEModel): def __init__(self, names: ModelNames, mu_max: float = 0.5, K_s: float = 1.0): super().__init__(backend="numpy", names=names) self.mu_max = mu_max self.K_s = K_s def compute_rhs(self, t, X, S): """Compute dX/dt and dS/dt.""" mu = self.mu_max * S[0] / (self.K_s + S[0]) dX = X * mu dS = -10.0 * X * mu return dX, dS def simulate(self, y0, t): """Simulate the model using the NumPy backend.""" return self.predict( y0=y0, t_eval=t, ) We can now instantiate the model and simulate its dynamics. .. code-block:: python import numpy as np from mgrowthctrl.models.base import ModelNames names = ModelNames(["biomass"], ["substrate"]) model = MyModel(names=names) y0 = [0.1, 10.0] t_eval = np.linspace(0, 50, 100) sim = model.simulate(y0, t_eval) Finally, we visualize the resulting trajectories. .. code-block:: python import matplotlib.pyplot as plt plt.figure(figsize=(6, 3.2)) plt.plot(sim.t, sim.X[0, :], label="biomass") plt.plot(sim.t, sim.S[0, :], label="substrate") plt.xlabel("time (a.u.)") plt.ylabel("concentration (a.u.)") plt.legend() plt.tight_layout() plt.savefig("base_class_simple_example.png") plt.legend() plt.show() Here's what the simulation result looks like: .. image:: ../_static/base_class_simple_example.png :alt: Base class example :align: center Backend Selection ================= The default "numpy" backend places the data in numpy arrays and performs operations through those. Using the "torch" backend lets you offload operations on the GPU, for instance. You can set the backend when constructing a model by providing the string "numpy" or "torch", or by instantiating one of the two ``ArrayBackend`` subclasses from the ``mgrowthctrl.backends`` module, documented below. These classes have a number of public methods that are used to unify handling between torch-based and numpy-based implementations, but these can be considered internal to the workings of the package. If you want to implement your own backend or to reuse the logic in some way, we recommend reading the source code directly. .. automodule:: mgrowthctrl.backends.array :members: :member-order: bysource :show-inheritance: API Documentation ================= .. automodule:: mgrowthctrl.models.base :members: :member-order: bysource :show-inheritance: