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