Skipper-Schabel-Wilcox log-kill
The log-kill model, an exponentially growing tumour of which every dose kills a fixed fraction.
- Name:
skipper_log_kill - Field:
biology/oncology - Formalism: ordinary differential equations
- References: Padmanabhan, Meskin, and Al Moustafa 2021; Skipper, Schabel, and Wilcox 1964
Factory
@register_model(
field='biology/oncology',
tags=('chemotherapy', 'ode', 'log kill', 'dosing'),
references=('Padmanabhan, Meskin, and Al Moustafa 2021', 'Skipper, Schabel, and Wilcox 1964'),
defaults={'until': 42.0, 'dt': 0.1},
)
def skipper_log_kill(
n: float = 1e6,
*,
r: float = 0.223,
a: float = 2.0,
dose: float = 1.0,
period: float = 7.0,
width: float = 1.0,
) -> System:
"""Return the log-kill model, an exponentially growing tumour of which every dose kills a fixed fraction.
The tumour follows `dn/dt = r n - a n U(t)`, where the exposure `U` is `dose` for `width` days every `period`
days. Every dose lowers `log n` by `a dose width`, whatever the size, and the tumour regrows between doses. The
growth rate is the exponential fit to breast cancer, per day.
Args:
n:
The initial number of cells.
r:
The growth rate.
a:
The kill rate per unit of exposure.
dose:
The exposure during a dose.
period:
The time between the starts of doses.
width:
The duration of a dose.
Returns:
system:
The model, with the cells `tumour.n`.
Examples:
>>> from skcomplex.models.biology.oncology import skipper_log_kill
>>> skipper_log_kill(period=21.0).interactions[1].period
21.0
"""
return System(
elements=[PopulationElement('tumour', n=n)],
interactions=[
ExponentialGrowthInteraction('growth', target='tumour', r=r),
LogKillInteraction('kill', target='tumour', a=a, dose=dose, period=period, width=width),
],
)
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 ExponentialGrowthInteraction(GrowthInteraction):
"""Growth at a constant per capita rate, `dn/dt = r n`, the exponential form of a growth without harvest.
Args:
label:
The label of the interaction.
target:
The path of the element or group whose population grows.
r:
The per capita growth rate, negative for decay.
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 ExponentialGrowthInteraction
>>> system = System(
... elements=[CompartmentElement('rabbits', n=1.0)],
... interactions=[ExponentialGrowthInteraction('growth', target='rabbits', r=0.5)],
... )
>>> trajectory = Simulation(system, dt=0.01).check().prepare().run(until=2.0).trajectory_
>>> round(float(trajectory.values('rabbits.n')[-1]), 3)
2.718
"""
def __init__(
self,
label: str,
*,
target: Annotated[str, RoleSpec()],
r: Annotated[float, ParameterSpec()] = 1.0,
state: str = 'n',
) -> None:
super().__init__(label, target=target, r=r, form='exponential', state=state)
def _get_rate(self, namespace: SimpleNamespace) -> Expression:
"""Return the growth `r n`, 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 LogKillInteraction(RateInteraction):
"""Cell kill proportional to the tumour and to a drug exposure given in a train of rectangular doses.
The kill is `dn/dt = -a n U(t)`, where the exposure `U` is `dose` for `width` time units every `period`, from
`start` on. Without growth, every dose lowers `log n` by `a dose width`.
Args:
label:
The label of the interaction.
target:
The path of the tumour.
a:
The kill rate per unit of exposure.
dose:
The exposure during a dose.
period:
The time between the starts of doses.
width:
The duration of a dose.
start:
The time of the first dose.
Examples:
>>> from skcomplex.models.biology.oncology import LogKillInteraction
>>> LogKillInteraction('kill', target='tumour', period=21.0).period
21.0
"""
def __init__(
self,
label: str,
*,
target: Annotated[str, RoleSpec()],
a: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.5,
dose: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
period: float = 7.0,
width: float = 1.0,
start: float = 0.0,
) -> None:
super().__init__(label, target=target, state='n')
self.a = a
self.dose = dose
self.period = period
self.width = width
self.start = start
def _get_rate(self, s: SimpleNamespace) -> Expression:
"""Return the rate `-a n U(t)`.
Args:
s:
The namespace of the symbols the laws can use.
Returns:
rate:
The rate of the size of the tumour.
"""
exposure = pulse_train(s.t, self.period, self.width, start=self.start)
return -s.a * s.dose * exposure * s.target.n
Equations
tumour.n: diff(n_tumour(t), t) == -a_kill*dose_kill*n_tumour(t)*where(both(t >= 0.0, mod(t, 7.0) < 1.0), 1.0, 0) + r_growth*n_tumour(t)