Skip to content

Logistic growth with a varying capacity

Logistic growth under a seasonal carrying capacity, which settles on a cycle of the season's period.

  • Name: varying_capacity_logistic
  • Field: biology/ecology
  • Formalism: ordinary differential equations
  • References: Coleman 1979; Trzaska 2023

Factory

@register_model(
    field='biology/ecology',
    tags=('population', 'ode', 'forcing', 'periodic'),
    references=('Coleman 1979', 'Trzaska 2023'),
    defaults={'until': 20.0, 'dt': 0.05},
)
def varying_capacity_logistic(
    n: float = 10.0,
    *,
    r: float = 2.0,
    K: float = 100.0,
    amplitude: float = 0.5,
    period: float = 1.0,
) -> System:
    """Return logistic growth under a seasonal carrying capacity, which settles on a cycle of the season's period.

    Whatever its initial size, the population tends to a unique periodic solution, which lags behind the capacity and
    varies less than it.

    Args:
        n:
            The initial size.

        r:
            The intrinsic growth rate.

        K:
            The mean carrying capacity.

        amplitude:
            The relative amplitude of the seasonal variation of the capacity, below 1.

        period:
            The period of the seasons.

    Returns:
        system:
            The model, with the population `population`.

    Examples:
        >>> from skcomplex.models.biology.ecology import varying_capacity_logistic
        >>> varying_capacity_logistic(amplitude=0.2).interactions[0].amplitude
        0.2
    """
    return System(
        elements=[PopulationElement('population', n=n)],
        interactions=[
            SeasonalLogisticGrowthInteraction(
                'growth',
                target='population',
                r=r,
                K=K,
                amplitude=amplitude,
                period=period,
            ),
        ],
    )

Components

class PopulationElement(CompartmentElement):
    """Population of one species, a compartment whose state `n` holds its size.

    Args:
        label:
            The label of the population.

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

        n:
            The initial size.

    Examples:
        >>> from skcomplex.models.biology.common.populations import PopulationElement
        >>> PopulationElement('rabbits', n=40).n
        40
    """

    def __init__(
        self,
        label: str,
        visual: VisualSpec | None = None,
        *,
        n: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
    ) -> None:
        super().__init__(label, visual=visual, n=n)

class SeasonalLogisticGrowthInteraction(LogisticGrowthInteraction):
    """Logistic growth under a carrying capacity that varies periodically, `K(t) = K (1 + amplitude sin(2 pi t / T))`.

    Args:
        label:
            The label of the interaction.

        target:
            The path of the population.

        r:
            The per capita growth rate at low density.

        K:
            The mean carrying capacity.

        amplitude:
            The relative amplitude of the variation of the capacity, below 1.

        period:
            The period `T` of the variation.

    Examples:
        >>> from skcomplex.models.biology.ecology import SeasonalLogisticGrowthInteraction
        >>> SeasonalLogisticGrowthInteraction('growth', target='rabbits', period=12.0).period
        12.0
    """

    def __init__(
        self,
        label: str,
        *,
        target: Annotated[str, RoleSpec()],
        r: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
        K: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 100.0,
        amplitude: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.5,
        period: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 1.0,
    ) -> None:
        super().__init__(label, target=target, r=r, K=K)
        self.amplitude = amplitude
        self.period = period

    def _get_capacity(self, namespace: SimpleNamespace) -> Expression:
        """Return the capacity `K (1 + amplitude sin(2 pi t / T))` at the time of the namespace.

        Args:
            namespace:
                The namespace of the symbols the rate can use.

        Returns:
            capacity:
                The carrying capacity at the time `t`.
        """
        s = namespace
        capacity: Expression = s.K * (1 + s.amplitude * sin(2 * pi * s.t / s.period))
        return capacity

Equations

population.n: diff(n_population(t), t) == -r_growth*(n_population(t)/((amplitude_growth*sin(2*pi*t/period_growth) + 1)*K_growth) - 1)*n_population(t)