Skip to content

Droop cell-quota growth

The Droop model, a phytoplankton whose growth rate follows the nutrient stored in its cells.

  • Name: droop_cell_quota
  • Field: biology/ecology
  • Formalism: ordinary differential equations
  • References: Padmanabhan, Meskin, and Al Moustafa 2021; Droop 1973

Factory

@register_model(
    field='biology/ecology',
    tags=('population growth', 'ode', 'nutrient', 'cell quota', 'phytoplankton'),
    references=('Padmanabhan, Meskin, and Al Moustafa 2021', 'Droop 1973'),
    defaults={'until': 20.0, 'dt': 0.1},
)
def droop_cell_quota(
    Q: float = 1.5,
    P: float = 1.0,
    *,
    r_inf: float = 1.0,
    q: float = 1.0,
    vmax: float = 2.0,
    g: float = 1.0,
    R: float = 1.0,
    d: float = 0.1,
) -> System:
    """Return the Droop model, a phytoplankton whose growth rate follows the nutrient stored in its cells.

    The growth rate `r_inf (1 - q / Q)` rises with the quota `Q` and saturates at `r_inf`. Under a constant external
    nutrient the quota settles at `Q* = q + v / r_inf`, where `v = vmax R / (g + R)` is the uptake, and the biomass
    then grows exponentially at `r_inf (1 - q / Q*) - d`. The book gives no values for this model, so the defaults are
    in units of the minimum quota, the maximum growth rate, and the half saturation, with time in days.

    Args:
        Q:
            The initial cell quota.

        P:
            The initial biomass.

        r_inf:
            The growth rate for an infinite quota.

        q:
            The minimum quota, at which growth stops.

        vmax:
            The maximum uptake rate.

        g:
            The external nutrient at which the uptake is half its maximum.

        R:
            The external nutrient concentration.

        d:
            The death rate.

    Returns:
        system:
            The model, with the quota `algae.Q` and the biomass `algae.P`.

    Examples:
        >>> from skcomplex.models.biology.ecology import droop_cell_quota
        >>> droop_cell_quota(R=4.0).elements[0].R
        4.0
    """
    algae = QuotaPhytoplanktonElement('algae', Q=Q, P=P, r_inf=r_inf, q=q, vmax=vmax, g=g, R=R, d=d)
    return System(elements=[algae])

Components

class QuotaPhytoplanktonElement(Element):
    """Phytoplankton that grows on the nutrient stored in its cells, the Droop cell-quota model.

    The quota follows `dQ/dt = vmax R / (g + R) - r_inf (1 - q / Q) Q`, the Michaelis-Menten uptake from the
    external nutrient `R` less the dilution by growth, and the biomass `dP/dt = r_inf (1 - q / Q) P - d P`.

    Args:
        label:
            The label of the population.

        visual:
            How the element is drawn, or `None` for `default_visual_`.

        Q:
            The initial cell quota.

        P:
            The initial biomass.

        r_inf:
            The growth rate for an infinite quota.

        q:
            The minimum quota, at which growth stops.

        vmax:
            The maximum uptake rate.

        g:
            The external nutrient at which the uptake is half its maximum.

        R:
            The external nutrient concentration.

        d:
            The death rate.

    Examples:
        >>> from skcomplex.models.biology.ecology import QuotaPhytoplanktonElement
        >>> QuotaPhytoplanktonElement('algae', R=2.0).R
        2.0
    """

    def __init__(
        self,
        label: str,
        visual: VisualSpec | None = None,
        *,
        Q: Annotated[float, StateSpec(domain=RealSet.open(0, oo))] = 1.5,
        P: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
        r_inf: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
        q: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 1.0,
        vmax: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 2.0,
        g: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 1.0,
        R: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
        d: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.1,
    ) -> None:
        super().__init__(label, visual=visual)
        self.Q = Q
        self.P = P
        self.r_inf = r_inf
        self.q = q
        self.vmax = vmax
        self.g = g
        self.R = R
        self.d = d

    def _get_laws(self, s: SimpleNamespace) -> list[BaseLaw]:
        """Return the uptake and dilution of the quota and the growth and death of the biomass.

        Args:
            s:
                The namespace of the symbols the laws can use.

        Returns:
            laws:
                The laws of the component.
        """
        growth = s.r_inf * (1 - s.q / s.Q)
        return [
            RateLaw(s.Q, s.vmax * s.R / (s.g + s.R) - growth * s.Q),
            RateLaw(s.P, (growth - s.d) * s.P),
        ]

Equations

algae.Q: diff(Q_algae(t), t) == r_inf_algae*(q_algae/Q_algae(t) - 1)*Q_algae(t) + R_algae*vmax_algae/(R_algae + g_algae)
algae.P: diff(P_algae(t), t) == -(r_inf_algae*(q_algae/Q_algae(t) - 1) + d_algae)*P_algae(t)