Three-way fate decision on the compactified elliptic umbilic
Cells that choose among three fates on the compactified elliptic umbilic landscape of Rand et al.
- Name:
three_way_fate_decision - Field:
biology/systems_biology - Formalism: ordinary differential equations
- References: Rand et al. 2021
Select this system
from skcomplex.systems import create_simulation, create_system, describe
system = create_system('three_way_fate_decision')
entry = describe('three_way_fate_decision')
trajectory = create_simulation('three_way_fate_decision').prepare().run().trajectory_
Factory
@register_system(
field='biology/systems_biology',
tags=('cell fate', 'morse-smale', 'elliptic umbilic', 'landscape', 'ode'),
references=('Rand et al. 2021',),
defaults={'until': 40.0, 'dt': 0.2},
)
def three_way_fate_decision(
cells: int = 30,
*,
a: float = -0.2,
theta1: float = 0.0,
theta2: float = 0.0,
v1: float = 0.0,
v2: float = 0.0,
spread: float = 0.6,
seed: int | None = 0,
) -> System:
"""Return cells that choose among three fates on the compactified elliptic umbilic landscape of Rand et al.
Every cell descends the potential `x^4 + y^4 + x^3 - 2 x y^2 + a (x^2 + y^2) + theta1 x + theta2 y`. With `a < 0`
and no signal the centre is a repellor and three attractors, the fates, lie around it, separated by three saddles,
so cells that start near the centre split among the three fates. Signals that drift at the rates `v1` and `v2`
move the system across the fold curves of its parameter space, where a fate disappears and its cells switch to
another.
Args:
cells:
The number of cells.
a:
The curvature of the potential at the centre.
theta1:
The first signal at the start.
theta2:
The second signal at the start.
v1:
The rate of change of the first signal.
v2:
The rate of change of the second signal.
spread:
The half-width of the uniform initial states around the centre.
seed:
The seed of the initial states.
Returns:
system:
The system, with the states `cells.x` and `cells.y`.
Examples:
>>> from skcomplex.systems.biology.systems_biology import three_way_fate_decision
>>> three_way_fate_decision(5).elements[0].prepare().size_
5
"""
rng = np.random.default_rng(seed)
start = rng.uniform(-spread, spread, (2, cells))
values = {'x': start[0], 'y': start[1], 'a': a, 'theta1': theta1, 'theta2': theta2, 'v1': v1, 'v2': v2}
return System(elements=[GroupElement('cells', element=FateLandscapeElement, size=cells, values=values)])
Components
Elements:
FateLandscapeElement, fromElement
class FateLandscapeElement(Element):
"""Cell state in the compactified elliptic umbilic, the minimal landscape of a decision among three fates.
The state `(x, y)` descends the potential `f = x^4 + y^4 + x^3 - 2 x y^2 + a (x^2 + y^2) + theta1 x + theta2 y` of
Rand et al. (2021, figure 5D), `dx/dt = -df/dx` and `dy/dt = -df/dy`. The signals `theta1` and `theta2` change
in time at the rates `v1` and `v2`, and the sign of `a` makes the central rest point an attractor or a repellor.
Args:
label:
The label of the cell or of the group of cells.
visual:
How the element is drawn, or `None` for `default_visual_`.
x:
The initial first coordinate.
y:
The initial second coordinate.
a:
The curvature of the potential at the centre.
theta1:
The first signal at the start.
theta2:
The second signal at the start.
v1:
The rate of change of the first signal.
v2:
The rate of change of the second signal.
Examples:
>>> from skcomplex.systems.biology.systems_biology import FateLandscapeElement
>>> FateLandscapeElement('cell', a=-0.2).a
-0.2
"""
def __init__(
self,
label: str,
visual: VisualSpec | None = None,
*,
x: Annotated[float, StateSpec()] = 0.0,
y: Annotated[float, StateSpec()] = 0.0,
a: Annotated[float, ParameterSpec()] = -0.2,
theta1: Annotated[float, ParameterSpec()] = 0.0,
theta2: Annotated[float, ParameterSpec()] = 0.0,
v1: Annotated[float, ParameterSpec()] = 0.0,
v2: Annotated[float, ParameterSpec()] = 0.0,
) -> None:
super().__init__(label, visual=visual)
self.x = x
self.y = y
self.a = a
self.theta1 = theta1
self.theta2 = theta2
self.v1 = v1
self.v2 = v2
def _get_laws(self, s: SimpleNamespace) -> list[BaseLaw]:
"""Return the gradient descent of the potential under the drifting signals.
Args:
s:
The namespace of the symbols the laws can use.
Returns:
laws:
The laws of the component.
"""
theta1, theta2 = s.theta1 + s.v1 * s.t, s.theta2 + s.v2 * s.t
return [
RateLaw(s.x, -(4 * s.x**3 + 3 * s.x**2 - 2 * s.y**2 + 2 * s.a * s.x + theta1)),
RateLaw(s.y, -(4 * s.y**3 - 4 * s.x * s.y + 2 * s.a * s.y + theta2)),
]
Equations
cells.x[i]: diff(x_cells(t), t) == -4*x_cells(t)^3 - t*v1_cells - 2*a_cells*x_cells(t) - 3*x_cells(t)^2 + 2*y_cells(t)^2 - theta1_cells
cells.y[i]: diff(y_cells(t), t) == -4*y_cells(t)^3 - t*v2_cells - 2*a_cells*y_cells(t) + 4*x_cells(t)*y_cells(t) - theta2_cells