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))