Skip to content

Normal, tumour, and immune cells under chemotherapy

The de Pillis-Radunskaya model of immune, tumour, and normal cells under a constant drug infusion.

  • Name: de_pillis_radunskaya
  • Field: biology/oncology
  • Formalism: ordinary differential equations
  • References: Padmanabhan, Meskin, and Al Moustafa 2021; de Pillis and Radunskaya 2003

Factory

@register_model(
    field='biology/oncology',
    tags=('chemotherapy', 'ode', 'tumour-immune', 'competition'),
    references=(BOOK, 'de Pillis and Radunskaya 2003'),
    defaults={'until': 150.0, 'dt': 0.5},
)
def de_pillis_radunskaya(
    E: float = 0.15,
    A: float = 0.25,
    N: float = 1.0,
    *,
    u: float = 0.0,
    s: float = 0.33,
    r1: float = 1.5,
    a2: float = 0.3,
) -> System:
    """Return the de Pillis-Radunskaya model of immune, tumour, and normal cells under a constant drug infusion.

    The cells are normalised so that the carrying capacity of normal cells is 1, and time is in days. Untreated, a
    tumour that starts at 0.25 escapes the immune cells and settles at the high equilibrium where it outnumbers the
    normal cells. A constant infusion `u = 1` clears it, while the normal cells stay above 0.7. The parameters are those
    of Table 3.3 of Padmanabhan et al. The book's initial tumour of `1e-5` lies in the basin of the tumour-free
    equilibrium, which is stable since `r1 < c2 s / d1 + c3`, so the defaults are the initial conditions of de Pillis
    and Radunskaya 2003, `E = 0.15`, `A = 0.25`, `N = 1`. The book prints no infusion schedule, so the infusion is a
    constant rate `u` per day, untreated by default.

    Args:
        E:
            The initial immune cells.

        A:
            The initial tumour cells.

        N:
            The initial normal cells.

        u:
            The constant infusion rate of the drug.

        s:
            The influx of immune cells.

        r1:
            The growth rate of the tumour.

        a2:
            The fraction of tumour cells the drug kills.

    Returns:
        system:
            The model, with the cells `patient.E`, `patient.A`, `patient.N` and the drug `patient.U`.

    Examples:
        >>> from skcomplex.models.biology.oncology import de_pillis_radunskaya
        >>> de_pillis_radunskaya(u=1.0).elements[0].u
        1.0
    """
    return System(elements=[TumourImmuneChemotherapyElement('patient', E=E, A=A, N=N, u=u, s=s, r1=r1, a2=a2)])

Components

class TumourImmuneChemotherapyElement(Element):
    """Immune, tumour, and normal cells that compete and that a drug kills, after de Pillis and Radunskaya.

    The immune cells follow `dE/dt = s + rho E A / (alpha + A) - d1 E - c1 E A - a1 (1 - exp(-U)) E`, the tumour
    `dA/dt = r1 A (1 - b1 A) - c2 E A - c3 N A - a2 (1 - exp(-U)) A`, the normal cells
    `dN/dt = r2 N (1 - b2 N) - c4 N A - a3 (1 - exp(-U)) N`, and the drug `dU/dt = u - k U` under a constant infusion
    `u`.

    Args:
        label:
            The label of the patient.

        visual:
            How the element is drawn, or `None` for `default_visual_`.

        E:
            The initial immune cells.

        A:
            The initial tumour cells.

        N:
            The initial normal cells.

        U:
            The initial drug concentration.

        s:
            The influx of immune cells.

        rho:
            The immune response rate.

        alpha:
            The immune threshold.

        c1:
            The rate at which the tumour inactivates immune cells.

        c2:
            The rate at which immune cells kill the tumour.

        c3:
            The rate at which normal cells compete with the tumour.

        c4:
            The rate at which the tumour competes with normal cells.

        d1:
            The death rate of immune cells.

        r1:
            The growth rate of the tumour.

        r2:
            The growth rate of normal cells.

        b1:
            The reciprocal carrying capacity of the tumour.

        b2:
            The reciprocal carrying capacity of normal cells.

        a1:
            The fraction of immune cells the drug kills.

        a2:
            The fraction of tumour cells the drug kills.

        a3:
            The fraction of normal cells the drug kills.

        k:
            The decay rate of the drug, `dU` in the book.

        u:
            The constant infusion rate of the drug.

    Examples:
        >>> from skcomplex.models.biology.oncology import TumourImmuneChemotherapyElement
        >>> TumourImmuneChemotherapyElement('patient', u=1.0).u
        1.0
    """

    def __init__(
        self,
        label: str,
        visual: VisualSpec | None = None,
        *,
        E: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 0.15,
        A: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 0.25,
        N: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
        U: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 0.0,
        s: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.33,
        rho: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.01,
        alpha: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 0.3,
        c1: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
        c2: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.5,
        c3: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
        c4: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
        d1: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.2,
        r1: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 1.5,
        r2: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
        b1: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
        b2: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
        a1: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.2,
        a2: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.3,
        a3: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.1,
        k: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
        u: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.0,
    ) -> None:
        super().__init__(label, visual=visual)
        self.E = E
        self.A = A
        self.N = N
        self.U = U
        self.s = s
        self.rho = rho
        self.alpha = alpha
        self.c1 = c1
        self.c2 = c2
        self.c3 = c3
        self.c4 = c4
        self.d1 = d1
        self.r1 = r1
        self.r2 = r2
        self.b1 = b1
        self.b2 = b2
        self.a1 = a1
        self.a2 = a2
        self.a3 = a3
        self.k = k
        self.u = u

    def _get_laws(self, s: SimpleNamespace) -> list[BaseLaw]:
        """Return the balances of the immune, tumour, and normal cells and of the drug.

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

        Returns:
            laws:
                The laws of the component.
        """
        E, A, N, U = s.E, s.A, s.N, s.U
        kill = 1 - exp(-U)
        return [
            RateLaw(E, s.s + s.rho * E * A / (s.alpha + A) - s.d1 * E - s.c1 * E * A - s.a1 * kill * E),
            RateLaw(A, s.r1 * A * (1 - s.b1 * A) - s.c2 * E * A - s.c3 * N * A - s.a2 * kill * A),
            RateLaw(N, s.r2 * N * (1 - s.b2 * N) - s.c4 * N * A - s.a3 * kill * N),
            RateLaw(U, s.u - s.k * U),
        ]

Equations

patient.E: diff(E_patient(t), t) == a1_patient*(e^(-U_patient(t)) - 1)*E_patient(t) - c1_patient*A_patient(t)*E_patient(t) - d1_patient*E_patient(t) + rho_patient*A_patient(t)*E_patient(t)/(alpha_patient + A_patient(t)) + s_patient
patient.A: diff(A_patient(t), t) == -(b1_patient*A_patient(t) - 1)*r1_patient*A_patient(t) + a2_patient*(e^(-U_patient(t)) - 1)*A_patient(t) - c2_patient*A_patient(t)*E_patient(t) - c3_patient*A_patient(t)*N_patient(t)
patient.N: diff(N_patient(t), t) == -(b2_patient*N_patient(t) - 1)*r2_patient*N_patient(t) + a3_patient*(e^(-U_patient(t)) - 1)*N_patient(t) - c4_patient*A_patient(t)*N_patient(t)
patient.U: diff(U_patient(t), t) == -k_patient*U_patient(t) + u_patient