Skip to content

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)