Bristle patterning by lateral inhibition on a ring
A ring of cells that inhibit the neural fate of their neighbours, leaving sparse, evenly spaced bristles.
- Name:
bristle_lateral_inhibition - Field:
biology/systems_biology - Formalism: ordinary differential equations
- References: Corson et al. 2017; Rand et al. 2021
Select this system
from skcomplex.systems import create_simulation, create_system, describe
system = create_system('bristle_lateral_inhibition')
entry = describe('bristle_lateral_inhibition')
trajectory = create_simulation('bristle_lateral_inhibition').prepare().run().trajectory_
Factory
@register_system(
field='biology/systems_biology',
tags=('lateral inhibition', 'pattern formation', 'bristles', 'notch-delta', 'ode'),
references=('Corson et al. 2017', 'Rand et al. 2021'),
defaults={'until': 60.0, 'dt': 0.5},
)
def bristle_lateral_inhibition(
cells: int = 40,
*,
strength: float = 1.0,
length: float = 2.5,
width: float = 0.127,
noise: float = 0.05,
seed: int | None = 0,
) -> System:
"""Return a ring of cells that inhibit the neural fate of their neighbours, leaving sparse, evenly spaced bristles.
Following Eq. (2) of Rand et al. (2021), every cell relaxes to `sigma(a_i - h_i)` under the inhibition `h_i = sum_j
K_ij d(a_j)`, with the Gaussian kernel `K_ij = strength exp(-d_ij^2 / (2 length^2))` of the distance `d_ij` along
the ring. All cells start near the epidermal state and rise together, until small differences break the symmetry
at a saddle on the diagonal: the leading cells turn neural and silence their neighbours, so the neural cells end
several cell diameters apart.
Args:
cells:
The number of cells on the ring.
strength:
The inhibition between neighbouring cells at zero distance.
length:
The range of the inhibition, in cell diameters.
width:
The width of the sigmoids.
noise:
The largest initial neural state, drawn uniformly from zero.
seed:
The seed of the initial states.
Returns:
system:
The system, with the neural states `cells.a`.
Examples:
>>> from skcomplex.systems.biology.systems_biology import bristle_lateral_inhibition
>>> bristle_lateral_inhibition(10).elements[0].prepare().size_
10
"""
index = np.arange(cells)
distance = np.abs(index[:, None] - index[None, :])
distance = np.minimum(distance, cells - distance)
kernel = strength * np.exp(-(distance**2) / (2 * length**2))
np.fill_diagonal(kernel, 0.0)
states = np.random.default_rng(seed).uniform(0, noise, cells)
group = GroupElement('cells', element=ProneuralCellElement, size=cells, values={'a': states, 'width': width})
inhibition = LateralInhibitionInteraction('inhibition', cells='cells', kernel=tuple(map(tuple, kernel.tolist())))
return System(elements=[group], interactions=[inhibition])
Components
Elements:
ProneuralCellElement, fromElement
Interactions:
LateralInhibitionInteraction, fromInteraction
class ProneuralCellElement(Element):
"""Cell of an epithelium that adopts the neural fate when the inhibition from its neighbours is weak.
The state `a` runs from 0, the epidermal fate, to 1, the neural fate of a sensory bristle. The sigmoids of the
lateral inhibition share the width `width`.
Args:
label:
The label of the cell or of the group of cells.
visual:
How the element is drawn, or `None` for `default_visual_`.
a:
The initial neural state.
width:
The width of the sigmoids.
Examples:
>>> from skcomplex.systems.biology.systems_biology import ProneuralCellElement
>>> ProneuralCellElement('cell', a=0.1).a
0.1
"""
def __init__(
self,
label: str,
visual: VisualSpec | None = None,
*,
a: Annotated[float, StateSpec(domain=RealSet.closed(0, 1))] = 0.0,
width: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 0.127,
) -> None:
super().__init__(label, visual=visual)
self.a = a
self.width = width
class LateralInhibitionInteraction(Interaction):
"""Lateral inhibition of the neural fate of cells by their neighbours, through a kernel of the distance.
Following Eq. (2) of Rand et al. (2021), `da_i/dt = sigma(a_i - h_i) - a_i` with the inhibition `h_i = sum_{j != i}
K_ij d(a_j)`, where `sigma(u) = 1 / (1 + exp(-u / width))` and `d(a) = sigma(a - 1 / 2)`. A cell under an
inhibition below 0.37 can only turn neural, above 0.63 only epidermal, and in between it is bistable.
Args:
label:
The label of the interaction.
cells:
The path of the group of cells.
kernel:
The inhibition `K_ij` of every cell by every other, with a zero diagonal.
Examples:
>>> from skcomplex.systems.biology.systems_biology import LateralInhibitionInteraction
>>> LateralInhibitionInteraction('inhibition', cells='cells', kernel=((0.0, 1.0), (1.0, 0.0))).kernel
((0.0, 1.0), (1.0, 0.0))
"""
def __init__(
self,
label: str,
*,
cells: Annotated[str, RoleSpec()],
kernel: Sequence[Sequence[float]] = ((0.0,),),
) -> None:
super().__init__(label)
self.cells = cells
self.kernel = kernel
def _get_laws(self, s: SimpleNamespace) -> list[BaseLaw]:
"""Return the relaxation of every cell towards the fate its inhibition allows.
Args:
s:
The namespace of the symbols the laws can use.
Returns:
laws:
The laws of the component.
"""
cell = s.cells
inhibition = total(self._sigmoid(cell.a - 1 / 2, cell.width), weights=np.asarray(self.kernel, dtype=float))
return [RateLaw(cell.a, self._sigmoid(cell.a - inhibition, cell.width) - cell.a)]
def _sigmoid(self, u: Expression, width: Expression) -> Expression:
"""Return the logistic sigmoid `1 / (1 + exp(-u / width))`."""
return 1 / (1 + exp(-u / width))
Equations
cells.a[i]: diff(a_cells(t), t) == 1/(e^((_reduced_0 - a_cells(t))/width_cells) + 1) - a_cells(t)