Skip to content

Exponential-linear tumour growth

A tumour that grows exponentially while small and linearly once its growth reaches a limiting rate.

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

Factory

@register_model(
    field='biology/oncology',
    tags=('tumour growth', 'ode', 'piecewise'),
    references=GROWTH_REFERENCES,
    defaults={'until': 40.0, 'dt': 0.2},
)
def exponential_linear_growth(n: float = 2.0, *, r0: float = 0.31, r: float = 67.8) -> System:
    """Return a tumour that grows exponentially while small and linearly once its growth reaches a limiting rate.

    The defaults are the fit to breast cancer volumes in mm³ and days, which switches near day 15.

    Args:
        n:
            The initial volume.

        r0:
            The exponential growth rate.

        r:
            The linear growth rate.

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

    Examples:
        >>> from skcomplex.models.biology.oncology import exponential_linear_growth
        >>> exponential_linear_growth().interactions[0].r0
        0.31
    """
    return System(
        elements=[PopulationElement('tumour', n=n)],
        interactions=[ExponentialLinearGrowthInteraction('growth', target='tumour', r0=r0, r=r)],
    )

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 ExponentialLinearGrowthInteraction(RateInteraction):
    """Growth that is exponential, `dn/dt = r0 n`, until it reaches the linear rate `r`, and linear after.

    The switch happens at `tau = log(r / (r0 n0)) / r0`, where the two rates meet, so the solution is continuously
    differentiable.

    Args:
        label:
            The label of the interaction.

        target:
            The path of the tumour.

        r0:
            The exponential growth rate.

        r:
            The linear growth rate.

    Examples:
        >>> from skcomplex.models.biology.oncology import ExponentialLinearGrowthInteraction
        >>> ExponentialLinearGrowthInteraction('growth', target='tumour', r=67.8).r
        67.8
    """

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

    def _get_rate(self, s: SimpleNamespace) -> Expression:
        """Return the rate `min(r0 n, r)`.

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

        Returns:
            rate:
                The rate of the size of the tumour.
        """
        exponential = s.r0 * s.target.n
        return minimum(exponential, s.r)

Equations

tumour.n: diff(n_tumour(t), t) == where(r_growth < r0_growth*n_tumour(t), r_growth, r0_growth*n_tumour(t))