Skip to content

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, from Element

Interactions:

  • LateralInhibitionInteraction, from Interaction
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)