Skip to content

Von Bertalanffy growth

A tumour that grows by the balance of anabolism and catabolism, dn/dt = r n^p - d n.

  • Name: von_bertalanffy
  • Field: biology/oncology
  • Formalism: ordinary differential equations
  • References: Padmanabhan, Meskin, and Al Moustafa 2021; Benzekry et al. 2014; von Bertalanffy 1957

Factory

@register_model(
    field='biology/oncology',
    tags=('tumour growth', 'ode', 'allometry'),
    references=(*GROWTH_REFERENCES, 'von Bertalanffy 1957'),
    defaults={'until': 80.0, 'dt': 0.4},
)
def von_bertalanffy(n: float = 1.0, *, r: float = 2.32, p: float = 0.918, d: float = 0.808) -> System:
    """Return a tumour that grows by the balance of anabolism and catabolism, `dn/dt = r n^p - d n`.

    The volume tends to `(r / d)^(1/(1-p))`. The defaults are the fit to breast cancer volumes in mm³ and days.

    Args:
        n:
            The initial volume.

        r:
            The growth rate.

        p:
            The exponent of the growth, below 1.

        d:
            The loss rate.

    Returns:
        system:
            The model, with the volume `tumour.n`.

    Examples:
        >>> from skcomplex.models.biology.oncology import von_bertalanffy
        >>> von_bertalanffy().interactions[1].d
        0.808
    """
    return System(
        elements=[PopulationElement('tumour', n=n)],
        interactions=[
            PowerLawGrowthInteraction('growth', target='tumour', r=r, p=p),
            MortalityInteraction('loss', target='tumour', d=d),
        ],
    )

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 PowerLawGrowthInteraction(GrowthInteraction):
    """Growth that scales as a power of the size, `dn/dt = r n ** p`, without harvest.

    Args:
        label:
            The label of the interaction.

        target:
            The path of the element or group whose population grows.

        r:
            The growth rate.

        p:
            The exponent, `2/3` for a growing rim and 1 for exponential growth.

        state:
            The name of the state of the target that holds the population.

    Examples:
        >>> from skcomplex.core.elements import CompartmentElement
        >>> from skcomplex.core.system import System
        >>> from skcomplex.engine.runtime import Simulation
        >>> from skcomplex.models.biology.common.populations import PowerLawGrowthInteraction
        >>> system = System(
        ...     elements=[CompartmentElement('tumour', n=1.0)],
        ...     interactions=[PowerLawGrowthInteraction('growth', target='tumour', r=1.0, p=0.5)],
        ... )
        >>> trajectory = Simulation(system, dt=0.01).check().prepare().run(until=2.0).trajectory_
        >>> round(float(trajectory.values('tumour.n')[-1]), 3)
        4.0
    """

    def __init__(
        self,
        label: str,
        *,
        target: Annotated[str, RoleSpec()],
        r: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 1.32,
        p: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 0.58,
        state: str = 'n',
    ) -> None:
        super().__init__(label, target=target, r=r, p=p, form='power', state=state)

    def _get_rate(self, namespace: SimpleNamespace) -> Expression:
        """Return the power-law growth, with no harvest.

        Args:
            namespace:
                The namespace of the symbols the rate can use, with the namespace of the target.

        Returns:
            rate:
                The rate of the population.
        """
        return self._get_growth(namespace)

class MortalityInteraction(RateInteraction):
    """Death or loss at a constant per capita rate, `dn/dt = -d n`.

    Args:
        label:
            The label of the interaction.

        target:
            The path of the element or group whose population dies.

        d:
            The per capita death rate.

        state:
            The name of the state of the target that holds the population.

    Examples:
        >>> from skcomplex.core.elements import CompartmentElement
        >>> from skcomplex.core.system import System
        >>> from skcomplex.engine.runtime import Simulation
        >>> from skcomplex.models.biology.common.populations import MortalityInteraction
        >>> system = System(
        ...     elements=[CompartmentElement('foxes', n=8.0)],
        ...     interactions=[MortalityInteraction('death', target='foxes', d=0.5)],
        ... )
        >>> trajectory = Simulation(system, dt=0.01).check().prepare().run(until=2.0).trajectory_
        >>> round(float(trajectory.values('foxes.n')[-1]), 3)
        2.943
    """

    def __init__(
        self,
        label: str,
        *,
        target: Annotated[str, RoleSpec()],
        d: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.1,
        state: str = 'n',
    ) -> None:
        super().__init__(label, target=target, state=state)
        self.d = d

    def _get_rate(self, namespace: SimpleNamespace) -> Expression:
        """Return the loss `-d n`.

        Args:
            namespace:
                The namespace of the symbols the rate can use, with the namespace of the target.

        Returns:
            rate:
                The rate of the population.
        """
        loss: Expression = -namespace.d * getattr(namespace.target, self.state)
        return loss

Equations

tumour.n: diff(n_tumour(t), t) == r_growth*n_tumour(t)^p_growth - d_loss*n_tumour(t)