Skip to content

Substance exchange between logistic cells in a closed contour

The exchange of signalling molecules between logistic cells arranged in a closed contour.

  • Name: closed_contour_cell_exchange
  • Field: biology/physiology
  • Formalism: discrete map
  • References: Mihailović et al. 2023

Factory

@register_model(
    field='biology/physiology',
    tags=('map', 'logistic', 'coupled maps', 'ring', 'synchronization', 'cell communication'),
    references=('Mihailović et al. 2023',),
    defaults={'until': 200, 'dt': 1},
)
def closed_contour_cell_exchange(
    n_cells: int = 8,
    *,
    r: float = 2.5,
    c: float | Sequence[float] = 0.1,
    p: float | Sequence[float] = 0.5,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return the exchange of signalling molecules between logistic cells arranged in a closed contour.

    Every cell runs the logistic map on its concentration and takes up a share `c_i` of its next state from the next
    cell through the power map `x_{i+1}^{p_i}`, `x_i <- (1 - c_i) r x_i (1 - x_i) + c_i x_{i+1}^{p_i}`. Mihailović et
    al. show that an equilibrium is asymptotically stable for every coupling when every `x_i` lies in
    `((r - 1) / 2r, (r + 1) / 2r)` and above `p_{i-1}^(1 / (1 - p_{i-1}))`. In this domain the cells synchronize to a
    common concentration, and a chaotic environment, `r` near 4, desynchronizes them.

    Args:
        n_cells:
            The number of cells of the contour.

        r:
            The logistic parameter of every cell.

        c:
            The coupling of every cell to the next, one value or one per cell.

        p:
            The affinity of every cell for the uptake, one value or one per cell.

        random_state:
            The seed or generator of the initial concentrations, uniform between 0.2 and 0.8.

    Returns:
        system:
            The model, with the concentrations `cells.x`, cell `i + 1` next to cell `i`.

    Examples:
        >>> from skcomplex.models.biology.physiology import closed_contour_cell_exchange
        >>> closed_contour_cell_exchange(5).elements[0].check().prepare().size_
        5
    """
    rng = np.random.default_rng(random_state)
    cells = GroupElement(
        'cells',
        element=ContourCellElement,
        size=n_cells,
        values={
            'x': rng.uniform(0.2, 0.8, n_cells),
            'r': r,
            'c': np.broadcast_to(np.asarray(c, dtype=float), n_cells).copy(),
            'p': np.broadcast_to(np.asarray(p, dtype=float), n_cells).copy(),
        },
    )
    uptake = ContourUptakeInteraction('uptake', target='cells')
    return System(elements=[cells], interactions=[uptake], time=DiscreteTime())

Components

class ContourCellElement(Element):
    """Logistic cell of a closed contour that takes up the signalling molecules of the next cell through a power map.

    `ContourUptakeInteraction` iterates `x_i <- (1 - c_i) r x_i (1 - x_i) + c_i x_{i+1}^{p_i}`, where the logistic map
    is the intracellular dynamics, `c_i` the share of the exchange with the environment between the cells, and `p_i`
    the affinity of the cell for the uptake. The last cell of the contour takes up from the first.

    Args:
        label:
            The label of the group of cells.

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

        x:
            The initial concentration of signalling molecules in the cell, between 0 and 1.

        r:
            The logistic parameter, the disposition of the environment to communication.

        c:
            The coupling of the cell to the next one.

        p:
            The affinity of the cell for the uptake of signalling molecules.

    Examples:
        >>> from skcomplex.models.biology.physiology import ContourCellElement
        >>> ContourCellElement('cells').r
        2.5
    """

    def __init__(
        self,
        label: str,
        visual: VisualSpec | None = None,
        *,
        x: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 0.5,
        r: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 2.5,
        c: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.1,
        p: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.5,
    ) -> None:
        super().__init__(label, visual=visual)
        self.x = x
        self.r = r
        self.c = c
        self.p = p

class ContourUptakeInteraction(CouplingInteraction):
    """Uptake of every cell of a contour from the next cell, `x_i <- (1 - c_i) r x_i (1 - x_i) + c_i x_{i+1}^p_i`.

    It is the map form of coupling to the next member of the group, with the power `x_{i+1}^{p_i}` of the next cell
    combined with the logistic map of the cell. The last cell of the contour takes up from the first.

    Args:
        label:
            The label of the interaction, unique among the parts of its system.

        target:
            The path of the group of cells.

    Examples:
        >>> from skcomplex.core.elements import GroupElement
        >>> from skcomplex.core.spaces import DiscreteTime
        >>> from skcomplex.core.system import System
        >>> from skcomplex.engine.runtime import Simulation
        >>> from skcomplex.models.biology.physiology import ContourCellElement, ContourUptakeInteraction
        >>> cells = GroupElement('cells', element=ContourCellElement, size=2, values={'x': [0.25, 1.0], 'c': 0.5})
        >>> uptake = ContourUptakeInteraction('uptake', target='cells')
        >>> system = System(elements=[cells], interactions=[uptake], time=DiscreteTime())
        >>> Simulation(system).check().prepare().run(until=1).trajectory_.values('cells.x')[-1].round(4).tolist()
        [0.7344, 0.25]
    """

    def __init__(self, label: str, *, target: Annotated[str, RoleSpec()]) -> None:
        super().__init__(
            label,
            target=target,
            read='x',
            write='x',
            function=self._get_power,
            mode='next',
            form='map',
            combine=self._get_mix,
        )

    @staticmethod
    def _get_power(namespace: SimpleNamespace) -> Target:
        """Return the power uptake of the next cell, with the affinity of the cell."""
        return namespace.other.x**namespace.member.p

    @staticmethod
    def _get_mix(namespace: SimpleNamespace, reduction: Target) -> Target:
        """Return the logistic map of the cell mixed with the uptake from the next cell."""
        member = namespace.member
        return (1 - member.c) * member.r * member.x * (1 - member.x) + member.c * reduction

Equations

cells.x[i]: x_cells(t + 1) == (c_cells - 1)*r_cells*(x_cells(t) - 1)*x_cells(t) + c_cells*x_cells__shift0^p_cells